5611 lines
226 KiB
C++
5611 lines
226 KiB
C++
/////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
//
|
|
// Name: WiFi.cpp
|
|
// Created: Sep 2024
|
|
// 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, configuration and ethernet connectivity for the
|
|
// tzpuPico 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 tzpuPico interface.
|
|
// Credits:
|
|
// Copyright: (c) 2019-2026 Philip Smart <philip.smart@net2net.org>
|
|
//
|
|
// History: Sep 2024 - Initial write based on the SharpKey Wifi class.
|
|
//
|
|
// Notes: See Makefile to enable/disable conditional components
|
|
//
|
|
/////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
// This source file is free software: you can redistribute it and#or modify
|
|
// it under the terms of the GNU General Public License as published
|
|
// by the Free Software Foundation, either version 3 of the License, or
|
|
// (at your option) any later version.
|
|
//
|
|
// This source file is distributed in the hope that it will be useful,
|
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
// GNU General Public License for more details.
|
|
//
|
|
// You should have received a copy of the GNU General Public License
|
|
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
/////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
|
|
#include "sdkconfig.h"
|
|
#if defined(CONFIG_IF_WIFI_ENABLED) || defined(CONFIG_IF_USB_NCM_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 <map>
|
|
#include <memory>
|
|
#include "freertos/FreeRTOS.h"
|
|
#include "freertos/task.h"
|
|
#include "esp_log.h"
|
|
#include "esp_app_format.h"
|
|
#include "esp_mac.h"
|
|
#include "freertos/event_groups.h"
|
|
#include "esp_system.h"
|
|
#if defined(CONFIG_IF_WIFI_ENABLED)
|
|
#include "esp_wifi.h"
|
|
#endif
|
|
#include "esp_event.h"
|
|
#include "esp_ota_ops.h"
|
|
#include "esp_timer.h"
|
|
#include "esp_flash.h"
|
|
#include "esp_psram.h"
|
|
#include "lwip/err.h"
|
|
#include "lwip/sys.h"
|
|
#include "driver/uart.h"
|
|
#include "driver/gpio.h"
|
|
#include "soc/timer_group_struct.h"
|
|
#include "soc/timer_group_reg.h"
|
|
#include <sys/param.h>
|
|
#include <unistd.h>
|
|
#include "esp_tls_crypto.h"
|
|
#include <esp_http_server.h>
|
|
extern "C"
|
|
{
|
|
#include "untar.h"
|
|
}
|
|
#include "IO.h"
|
|
#include "zlib.h"
|
|
#include "FSPI.h"
|
|
#include "WiFi.h"
|
|
#include "tzpuPico.h"
|
|
|
|
// SPI reverse-channel command queue — delivers commands to RP2350 via NOP poll.
|
|
// Replaces CP_queueCmd() (UART) for all application-level commands.
|
|
extern void CP_queueCmd(const char *cmd);
|
|
extern bool CP_sendCmd(const char *cmd, uint32_t timeoutMs);
|
|
|
|
#if defined(CONFIG_IF_WIFI_ENABLED)
|
|
// FreeRTOS event group to signal when we are connected
|
|
static EventGroupHandle_t sWifiEventGroup;
|
|
#endif
|
|
|
|
// globals (consider protecting with mutex if worried about concurrent users)
|
|
std::string gLastSelectedFloppy; // only used briefly between ?file= and processing
|
|
|
|
// Helper method for consistent error logging
|
|
void WiFi::logError(const char *msg, esp_err_t err)
|
|
{
|
|
// Locals.
|
|
char errorMsg[256];
|
|
|
|
if (err != ESP_OK)
|
|
{
|
|
snprintf(errorMsg, sizeof(errorMsg), "%s: %s (%d)", msg, esp_err_to_name(err), err);
|
|
}
|
|
else
|
|
{
|
|
snprintf(errorMsg, sizeof(errorMsg), "%s", msg);
|
|
}
|
|
ESP_LOGE(WIFITAG, "%s", errorMsg);
|
|
}
|
|
|
|
// 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;
|
|
std::string retVal;
|
|
|
|
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;
|
|
}
|
|
}
|
|
retVal = os.str();
|
|
return (retVal);
|
|
}
|
|
|
|
// 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.
|
|
static const std::map<esp_partition_type_t, std::string> typeMap = {{ESP_PARTITION_TYPE_APP, "App"}, {ESP_PARTITION_TYPE_DATA, "Data"}};
|
|
auto it = typeMap.find(type);
|
|
std::string retVal;
|
|
|
|
// Return the string version of the enum.
|
|
retVal = (it != typeMap.end() ? it->second : "n/a");
|
|
return (retVal);
|
|
}
|
|
|
|
// 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.
|
|
static const std::map<esp_partition_subtype_t, std::string> subTypeMap = {{ESP_PARTITION_SUBTYPE_APP_FACTORY, "Factory"},
|
|
{ESP_PARTITION_SUBTYPE_DATA_PHY, "phy"},
|
|
{ESP_PARTITION_SUBTYPE_DATA_NVS, "nvs"},
|
|
{ESP_PARTITION_SUBTYPE_DATA_COREDUMP, "core"},
|
|
{ESP_PARTITION_SUBTYPE_DATA_NVS_KEYS, "nvs_keys"},
|
|
{ESP_PARTITION_SUBTYPE_DATA_EFUSE_EM, "efuse"},
|
|
{ESP_PARTITION_SUBTYPE_DATA_ESPHTTPD, "httpd"},
|
|
{ESP_PARTITION_SUBTYPE_DATA_FAT, "fat"},
|
|
{ESP_PARTITION_SUBTYPE_DATA_SPIFFS, "spiffs"}};
|
|
auto it = subTypeMap.find(subtype);
|
|
std::string retVal;
|
|
|
|
if (subtype >= ESP_PARTITION_SUBTYPE_APP_OTA_MIN && subtype <= ESP_PARTITION_SUBTYPE_APP_OTA_MAX)
|
|
{
|
|
retVal = "ota_" + to_str(subtype - ESP_PARTITION_SUBTYPE_APP_OTA_MIN, 0, 10);
|
|
}
|
|
else
|
|
{
|
|
retVal = (it != subTypeMap.end() ? it->second : to_str(subtype, 0, 10));
|
|
}
|
|
|
|
// Return the string version of the enum.
|
|
return (retVal);
|
|
}
|
|
|
|
// Method to return the version number of a given module.
|
|
float WiFi::getVersionNumber(const std::string &name)
|
|
{
|
|
// Locals.
|
|
int idx = 0;
|
|
float retVal = 0.0f;
|
|
|
|
if (wifiCtrl.run.versionList)
|
|
{
|
|
// Loop through the version list looking for the module.
|
|
while (idx < wifiCtrl.run.versionList->elements && wifiCtrl.run.versionList->item[idx]->object != name)
|
|
{
|
|
idx++;
|
|
}
|
|
|
|
// Return the version number if found.
|
|
if (idx < wifiCtrl.run.versionList->elements)
|
|
{
|
|
retVal = wifiCtrl.run.versionList->item[idx]->version;
|
|
}
|
|
}
|
|
return (retVal);
|
|
}
|
|
|
|
// Helper method to split the URI into components for easier use.
|
|
void WiFi::splitURI(httpd_req_t *req, std::string &uri, std::string &url, std::string &urq, std::vector<t_kvPair> &pairs)
|
|
{
|
|
// Locals.
|
|
int queryDelim;
|
|
t_kvPair keyValue;
|
|
|
|
// Retrieve pointer to object in order to access data.
|
|
WiFi *pThis = (WiFi *) req->user_ctx;
|
|
|
|
// Decode the uri and split off parameters.
|
|
uri = pThis->urlDecode(req->uri);
|
|
queryDelim = uri.find("?");
|
|
url = queryDelim != std::string::npos ? uri.substr(0, queryDelim) : uri;
|
|
urq = queryDelim != std::string::npos ? uri.substr(queryDelim + 1) : "";
|
|
|
|
// Split the query string into pairs.
|
|
pairs.clear();
|
|
for (const auto &key : pThis->split(urq, "&"))
|
|
{
|
|
size_t pos = key.find('=');
|
|
if (pos != std::string::npos)
|
|
{
|
|
keyValue.name = key.substr(0, pos);
|
|
keyValue.value = key.substr(pos + 1);
|
|
pairs.push_back(keyValue);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Helper method to encode a URL string.
|
|
std::string WiFi::urlEncode(const std::string &str)
|
|
{
|
|
// Locals.
|
|
std::string retVal;
|
|
char bufHex[10];
|
|
size_t i;
|
|
unsigned char c;
|
|
size_t len = str.length();
|
|
|
|
retVal.reserve(len * 3);
|
|
for (i = 0; i < len; i++)
|
|
{
|
|
c = str[i];
|
|
if (isalnum(c) || c == '-' || c == '_' || c == '.' || c == '~')
|
|
{
|
|
retVal += c;
|
|
}
|
|
else
|
|
{
|
|
sprintf(bufHex, "%%%02X", c);
|
|
retVal += bufHex;
|
|
}
|
|
}
|
|
return (retVal);
|
|
}
|
|
|
|
// Helper method to decode a URL string.
|
|
std::string WiFi::urlDecode(const std::string &str)
|
|
{
|
|
// Locals.
|
|
std::string retVal;
|
|
char ch;
|
|
size_t i;
|
|
unsigned int ii;
|
|
size_t len = str.length();
|
|
|
|
retVal.reserve(len);
|
|
for (i = 0; i < len; i++)
|
|
{
|
|
if (str[i] != '%')
|
|
{
|
|
retVal += (str[i] == '+' ? ' ' : str[i]);
|
|
}
|
|
else if (i + 2 < len)
|
|
{
|
|
sscanf(str.substr(i + 1, 2).c_str(), "%x", &ii);
|
|
ch = static_cast<char>(ii);
|
|
retVal += ch;
|
|
i += 2;
|
|
}
|
|
}
|
|
return (retVal);
|
|
}
|
|
|
|
// Overloaded method to decode a C char buf containing a URL string.
|
|
std::string WiFi::urlDecode(const char *buf)
|
|
{
|
|
// Locals.
|
|
std::string retVal;
|
|
|
|
retVal = urlDecode(std::string(buf));
|
|
return (retVal);
|
|
}
|
|
|
|
// Method to split a string based on a delimiter and store in a vector.
|
|
std::vector<std::string> WiFi::split(const std::string &s, const std::string &delimiter)
|
|
{
|
|
// Locals.
|
|
size_t posStart = 0;
|
|
size_t posEnd;
|
|
size_t delimLen = delimiter.length();
|
|
std::string token;
|
|
std::vector<std::string> retVal;
|
|
|
|
// Loop through the string locating delimiters and split on each occurrence.
|
|
while ((posEnd = s.find(delimiter, posStart)) != std::string::npos)
|
|
{
|
|
token = s.substr(posStart, posEnd - posStart);
|
|
posStart = posEnd + delimLen;
|
|
retVal.push_back(token);
|
|
}
|
|
|
|
// Store last item in vector.
|
|
retVal.push_back(s.substr(posStart));
|
|
return (retVal);
|
|
}
|
|
|
|
// Check if a given string is a numeric string or not
|
|
bool WiFi::isNumber(const std::string &str)
|
|
{
|
|
// Locals.
|
|
bool retVal = !str.empty() && (str.find_first_not_of("0123456789") == std::string::npos);
|
|
|
|
// `std::find_first_not_of` searches the string for the first character
|
|
// that does not match any of the characters specified in its arguments
|
|
return (retVal);
|
|
}
|
|
|
|
// Function to split string `str` using a given delimiter
|
|
std::vector<std::string> WiFi::split(const std::string &str, char delim)
|
|
{
|
|
// Locals.
|
|
int i = 0;
|
|
std::vector<std::string> retVal;
|
|
size_t pos;
|
|
|
|
pos = str.find(delim);
|
|
while (pos != std::string::npos)
|
|
{
|
|
retVal.push_back(str.substr(i, pos - i));
|
|
i = ++pos;
|
|
pos = str.find(delim, pos);
|
|
}
|
|
retVal.push_back(str.substr(i, str.length()));
|
|
return (retVal);
|
|
}
|
|
|
|
// Function to validate an IP address
|
|
bool WiFi::validateIP(const std::string &ip)
|
|
{
|
|
// Locals.
|
|
bool retVal = false;
|
|
std::vector<std::string> list = split(ip, '.');
|
|
|
|
// If the token size is equal to four
|
|
if (list.size() == 4)
|
|
{
|
|
// Validate each token
|
|
retVal = true;
|
|
for (const std::string &str : list)
|
|
{
|
|
if (!isNumber(str) || std::stoi(str) > 255 || std::stoi(str) < 0)
|
|
{
|
|
retVal = false;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
return (retVal);
|
|
}
|
|
|
|
// Method to split an IP4 address into its components, checking each for validity.
|
|
bool WiFi::splitIP(const std::string &ip, int *a, int *b, int *c, int *d)
|
|
{
|
|
// Locals.
|
|
bool retVal = false;
|
|
std::vector<std::string> list = split(ip, '.');
|
|
|
|
// Init.
|
|
*a = *b = *c = *d = 0;
|
|
|
|
// If the token size is equal to four
|
|
if (list.size() == 4)
|
|
{
|
|
// Loop through vector and check each number for validity before assigning.
|
|
retVal = true;
|
|
for (int idx = 0; idx < 4; idx++)
|
|
{
|
|
if (!isNumber(list.at(idx)) || std::stoi(list.at(idx)) > 255 || std::stoi(list.at(idx)) < 0)
|
|
{
|
|
retVal = false;
|
|
break;
|
|
}
|
|
int frag = std::stoi(list.at(idx));
|
|
if (idx == 0)
|
|
*a = frag;
|
|
else if (idx == 1)
|
|
*b = frag;
|
|
else if (idx == 2)
|
|
*c = frag;
|
|
else if (idx == 3)
|
|
*d = frag;
|
|
}
|
|
}
|
|
|
|
return (retVal);
|
|
}
|
|
|
|
// Method to reset the RP2350 and optionally force it into firmware upload mode.
|
|
void WiFi::resetRP2350(bool uploadMode, bool disableUpload)
|
|
{
|
|
// Locals.
|
|
|
|
// Remove firmware upload toggle, ie. de-active line.
|
|
if (disableUpload)
|
|
{
|
|
gpio_set_level((gpio_num_t) CONFIG_BOOT, 1);
|
|
gpio_set_direction((gpio_num_t) CONFIG_BOOT, GPIO_MODE_INPUT);
|
|
}
|
|
else
|
|
{
|
|
// Toggle reset line.
|
|
gpio_set_direction((gpio_num_t) CONFIG_RP2350_RUN, GPIO_MODE_OUTPUT);
|
|
gpio_set_level((gpio_num_t) CONFIG_RP2350_RUN, 0);
|
|
|
|
// Force RP2350 into firmware upload mode?
|
|
if (uploadMode)
|
|
{
|
|
gpio_set_direction((gpio_num_t) CONFIG_BOOT, GPIO_MODE_OUTPUT);
|
|
gpio_set_level((gpio_num_t) CONFIG_BOOT, 0);
|
|
}
|
|
vTaskDelay(100);
|
|
|
|
// Remove reset.
|
|
gpio_set_level((gpio_num_t) CONFIG_RP2350_RUN, 1);
|
|
gpio_set_direction((gpio_num_t) CONFIG_RP2350_RUN, GPIO_MODE_INPUT);
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
// Method to replace a string with another string.
|
|
bool WiFi::stringReplace(std::string &str, const std::string &from, const std::string &to)
|
|
{
|
|
// Locals.
|
|
|
|
// Locate the 'from' string.
|
|
size_t startPos = str.find(from);
|
|
|
|
// End of string, no 'from' string found, so error exit.
|
|
if (startPos == std::string::npos)
|
|
return false;
|
|
|
|
// Replace the 'from' string with the 'to' string.
|
|
str.replace(startPos, from.length(), to);
|
|
|
|
// Success.
|
|
return true;
|
|
}
|
|
|
|
// Method to change the name of the floppy disk image indexed by 'diskNo'.
|
|
//
|
|
bool WiFi::setFloppyDiskFile(const std::string ¶m1, int diskNo)
|
|
{
|
|
if (diskNo < 0 || diskNo >= WIFI_MAX_FLOPPY_DISK_IMAGES)
|
|
return false;
|
|
|
|
if (this->wifiCtrl.run.floppyDiskImage[diskNo] != param1)
|
|
{
|
|
this->wifiCtrl.run.floppyDiskImage[diskNo] = param1;
|
|
ESP_LOGI(WIFITAG, "Floppy %d: %s", diskNo, param1.c_str());
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool WiFi::setQuickDiskFile(const std::string ¶m1, int diskNo)
|
|
{
|
|
if (diskNo < 0 || diskNo >= WIFI_MAX_QUICK_DISK_IMAGES)
|
|
return false;
|
|
|
|
if (this->wifiCtrl.run.quickDiskImage[diskNo] != param1)
|
|
{
|
|
this->wifiCtrl.run.quickDiskImage[diskNo] = param1;
|
|
ESP_LOGI(WIFITAG, "Quick Disk %d: %s", diskNo, param1.c_str());
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool WiFi::setRamFile(const std::string ¶m1, int ramfileNo)
|
|
{
|
|
if (ramfileNo < 0 || ramfileNo >= WIFI_MAX_RAMFILE_IMAGES)
|
|
return false;
|
|
|
|
if (this->wifiCtrl.run.ramFileImage[ramfileNo] != param1)
|
|
{
|
|
this->wifiCtrl.run.ramFileImage[ramfileNo] = param1;
|
|
ESP_LOGI(WIFITAG, "RAMFILE %d: %s", ramfileNo, param1.c_str());
|
|
}
|
|
return true;
|
|
}
|
|
|
|
// Method to unpack a gzipped and/or tar file. The extension of the srcFile determines the action taken.
|
|
esp_err_t WiFi::unpackFile(const std::string &srcDir, const std::string &srcFile, const std::string &dstDir, const std::string &dstFile)
|
|
{
|
|
// Locals.
|
|
esp_err_t result = ESP_OK;
|
|
std::string fqfnSrc = srcDir + "/" + srcFile;
|
|
std::string fqfnDst = dstDir + "/" + dstFile;
|
|
std::filesystem::path filePath(fqfnSrc);
|
|
int fd;
|
|
gzFile inFile = nullptr;
|
|
FILE *outFile = nullptr;
|
|
std::unique_ptr<char[]> chunk;
|
|
|
|
// Ensure file exists and is flushed.
|
|
fd = open(fqfnSrc.c_str(), O_RDONLY);
|
|
if (fd < 0)
|
|
{
|
|
result = ESP_FAIL;
|
|
logError(("Source file not valid (" + fqfnSrc + ")").c_str());
|
|
}
|
|
else
|
|
{
|
|
fsync(fd);
|
|
close(fd);
|
|
|
|
// Unpack a gzipped file?
|
|
if (filePath.extension() == ".gz")
|
|
{
|
|
ESP_LOGI(WIFITAG, "File to gunzip %s => %s", fqfnSrc.c_str(), fqfnDst.c_str());
|
|
|
|
// Unzip the file
|
|
inFile = gzopen(fqfnSrc.c_str(), "rb");
|
|
if (!inFile)
|
|
{
|
|
result = ESP_FAIL;
|
|
logError("gzopen failed");
|
|
}
|
|
else
|
|
{
|
|
// Prepare the output file to receive the decompressed data.
|
|
outFile = fopen(fqfnDst.c_str(), "wb");
|
|
if (!outFile)
|
|
{
|
|
result = ESP_FAIL;
|
|
logError(("Failed to open " + fqfnDst).c_str());
|
|
}
|
|
else
|
|
{
|
|
// Decompress the file to extract the tar file.
|
|
chunk.reset(new char[MAX_CHUNK_SIZE]);
|
|
if (chunk == nullptr)
|
|
{
|
|
result = ESP_FAIL;
|
|
logError("Memory exhausted in unpackFile");
|
|
}
|
|
else
|
|
{
|
|
int chunkSize;
|
|
while ((chunkSize = gzread(inFile, chunk.get(), MAX_CHUNK_SIZE)) > 0)
|
|
{
|
|
fwrite(chunk.get(), 1, chunkSize, outFile);
|
|
}
|
|
|
|
// Check for errors and report.
|
|
if (chunkSize == -1)
|
|
{
|
|
result = ESP_FAIL;
|
|
logError("gzread error");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
// Unpack a tar file?
|
|
else if (filePath.extension() == ".tar")
|
|
{
|
|
ESP_LOGI(WIFITAG, "File to untar %s", fqfnSrc.c_str());
|
|
if (untar(fqfnDst.c_str(), fqfnSrc.c_str()) != 0)
|
|
{
|
|
result = ESP_FAIL;
|
|
logError(("Failed to untar FilePack " + fqfnSrc + " onto SD Card root " + fqfnDst).c_str());
|
|
}
|
|
}
|
|
else
|
|
{
|
|
result = ESP_FAIL;
|
|
logError(("Unrecognised extension in Source File " + fqfnSrc + " => " + fqfnDst).c_str());
|
|
}
|
|
}
|
|
|
|
// Clean up resources
|
|
if (outFile)
|
|
{
|
|
fclose(outFile);
|
|
}
|
|
if (inFile)
|
|
{
|
|
gzclose(inFile);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
// Method to expand variable macros into variable values within a string buffer. The buffer will contain HTML/CSS text prior to despatch to a browser.
|
|
esp_err_t WiFi::expandVarsAndSend(httpd_req_t *req, std::string str)
|
|
{
|
|
// Locals.
|
|
bool largeMacroDetected = false;
|
|
esp_err_t result = ESP_OK;
|
|
std::vector<t_kvPair> pairs;
|
|
t_kvPair keyValue;
|
|
|
|
// Retrieve pointer to object in order to access data.
|
|
WiFi *pThis = (WiFi *) req->user_ctx;
|
|
|
|
// 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.
|
|
// This handles CSS lines with "100%", JS modulo/regex, etc. that previously
|
|
// triggered the slow path on every occurrence of '%'.
|
|
if (str.find("%SK_") == std::string::npos)
|
|
{
|
|
str += "\n";
|
|
return httpd_resp_send_chunk(req, str.c_str(), str.length());
|
|
}
|
|
|
|
// Build up the list of pairs, place holder to value, this is used to expand the given string with latest runtime values.
|
|
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 ? "checked" : "");
|
|
pairs.push_back(keyValue);
|
|
keyValue.name = "%SK_CLIENTDHCPOFF%";
|
|
keyValue.value = (!wifiConfig.clientParams.useDHCP ? "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);
|
|
{
|
|
static char txPwrStr[8];
|
|
static char txPwrDbmStr[16];
|
|
static char rssiStr[8];
|
|
#if defined(CONFIG_IF_WIFI_ENABLED)
|
|
snprintf(txPwrStr, sizeof(txPwrStr), "%d", wifiConfig.params.txPower);
|
|
int8_t actualPwr = 0;
|
|
esp_wifi_get_max_tx_power(&actualPwr);
|
|
snprintf(txPwrDbmStr, sizeof(txPwrDbmStr), "%.1f", actualPwr * 0.25f);
|
|
// Current RSSI (signal strength). Only meaningful in client mode when connected.
|
|
wifi_ap_record_t apInfo;
|
|
if (wifiCtrl.client.connected && esp_wifi_sta_get_ap_info(&apInfo) == ESP_OK)
|
|
snprintf(rssiStr, sizeof(rssiStr), "%d", apInfo.rssi);
|
|
else
|
|
snprintf(rssiStr, sizeof(rssiStr), "N/A");
|
|
#else
|
|
snprintf(txPwrStr, sizeof(txPwrStr), "0");
|
|
snprintf(txPwrDbmStr, sizeof(txPwrDbmStr), "N/A");
|
|
snprintf(rssiStr, sizeof(rssiStr), "N/A");
|
|
#endif
|
|
keyValue.name = "%SK_TXPOWER%";
|
|
keyValue.value = txPwrStr;
|
|
pairs.push_back(keyValue);
|
|
keyValue.name = "%SK_TXPOWERDBM%";
|
|
keyValue.value = txPwrDbmStr;
|
|
pairs.push_back(keyValue);
|
|
keyValue.name = "%SK_RSSI%";
|
|
keyValue.value = rssiStr;
|
|
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_REBOOTBUTTON%";
|
|
keyValue.value = (wifiCtrl.run.rebootButton ? "block" : "none");
|
|
pairs.push_back(keyValue);
|
|
keyValue.name = "%SK_ERRMSG%";
|
|
keyValue.value = wifiCtrl.run.errorMsg;
|
|
pairs.push_back(keyValue);
|
|
keyValue.name = "%SK_WIFINAVVISIBLE%";
|
|
#if defined(CONFIG_IF_WIFI_ENABLED)
|
|
keyValue.value = "";
|
|
#else
|
|
keyValue.value = "style=\"display:none\"";
|
|
#endif
|
|
pairs.push_back(keyValue);
|
|
keyValue.name = "%SK_USBNCMVISIBLE%";
|
|
#if defined(CONFIG_IF_USB_NCM_ENABLED)
|
|
keyValue.value = "";
|
|
#else
|
|
keyValue.value = "style=\"display:none\"";
|
|
#endif
|
|
pairs.push_back(keyValue);
|
|
keyValue.name = "%SK_NETPANELTITLE%";
|
|
#if defined(CONFIG_IF_WIFI_ENABLED) && defined(CONFIG_IF_USB_NCM_ENABLED)
|
|
keyValue.value = "Network Configuration";
|
|
#elif defined(CONFIG_IF_WIFI_ENABLED)
|
|
keyValue.value = "WiFi Configuration";
|
|
#else
|
|
keyValue.value = "Network Configuration";
|
|
#endif
|
|
pairs.push_back(keyValue);
|
|
keyValue.name = "%SK_NETPANELICON%";
|
|
#if defined(CONFIG_IF_WIFI_ENABLED)
|
|
keyValue.value = "fa-wifi";
|
|
#else
|
|
keyValue.value = "fa-usb";
|
|
#endif
|
|
pairs.push_back(keyValue);
|
|
#if defined(CONFIG_IF_USB_NCM_ENABLED)
|
|
keyValue.name = "%SK_USBNCMIP%";
|
|
keyValue.value = CONFIG_IF_USB_NCM_IP;
|
|
pairs.push_back(keyValue);
|
|
keyValue.name = "%SK_USBNCMNM%";
|
|
keyValue.value = CONFIG_IF_USB_NCM_NETMASK;
|
|
pairs.push_back(keyValue);
|
|
#endif
|
|
keyValue.name = "%SK_DEVICE%";
|
|
keyValue.value = wifiCtrl.run.hostDeviceDisplayName;
|
|
pairs.push_back(keyValue);
|
|
keyValue.name = "%SK_PROCESSOR%";
|
|
keyValue.value = wifiCtrl.run.hostDeviceName;
|
|
pairs.push_back(keyValue);
|
|
keyValue.name = "%SK_PRODNAME%";
|
|
keyValue.value = (wifiCtrl.run.versionList && 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 && 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 && 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++)
|
|
{
|
|
if (wifiCtrl.run.versionList->item[idx]->object.compare("tzpuPico") == 0 || wifiCtrl.run.versionList->item[idx]->object.compare("FilePack") == 0 ||
|
|
wifiCtrl.run.versionList->item[idx]->object.compare("WebFS") == 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 << "<td><span style=\"color: blue;\">WebFS</span> <i>(v" << to_str(getVersionNumber("WebFS"), 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 = 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_RP_PARTITIONS%";
|
|
{
|
|
std::ostringstream list;
|
|
for (int idx = 0; idx < FLASH_APP_MAX_INSTANCES; idx++)
|
|
{
|
|
std::string active = (idx == wifiCtrl.run.rp2350FlashHeader.activeApp) ? "Yes" : "No";
|
|
list << "<tr>"
|
|
<< "<td>" << to_str(idx, 0, 10) << "</td>"
|
|
<< "<td>" << to_str(wifiCtrl.run.rp2350FlashHeader.config[idx].addr, 0, 16) << "</td>"
|
|
<< "<td>" << to_str(wifiCtrl.run.rp2350FlashHeader.config[idx].size, 0, 16) << "</td>"
|
|
<< "<td>" << to_str(wifiCtrl.run.rp2350FlashHeader.config[idx].chksum, 0, 16) << "</td>"
|
|
<< "<td>" << active << "</td>"
|
|
<< "<td>" << wifiCtrl.run.rp2350FlashHeader.config[idx].license << "</td>"
|
|
<< "<td>" << wifiCtrl.run.rp2350FlashHeader.config[idx].author << "</td>"
|
|
<< "<td>" << wifiCtrl.run.rp2350FlashHeader.config[idx].description << "</td>"
|
|
<< "<td>" << wifiCtrl.run.rp2350FlashHeader.config[idx].version << "</td>"
|
|
<< "<td>" << wifiCtrl.run.rp2350FlashHeader.config[idx].versionDate << "</td>"
|
|
<< "<td>" << wifiCtrl.run.rp2350FlashHeader.config[idx].copyright << "</td>"
|
|
<< "</tr>";
|
|
}
|
|
keyValue.value = list.str();
|
|
}
|
|
pairs.push_back(keyValue);
|
|
// Current disk image filenames — displayed in the Actions menu.
|
|
keyValue.name = "%SK_FLOPPY1%";
|
|
keyValue.value = wifiCtrl.run.floppyDiskImage[0].empty() ? "none" : wifiCtrl.run.floppyDiskImage[0];
|
|
pairs.push_back(keyValue);
|
|
keyValue.name = "%SK_FLOPPY2%";
|
|
keyValue.value = wifiCtrl.run.floppyDiskImage[1].empty() ? "none" : wifiCtrl.run.floppyDiskImage[1];
|
|
pairs.push_back(keyValue);
|
|
keyValue.name = "%SK_QDDISK%";
|
|
keyValue.value = wifiCtrl.run.quickDiskImage[0].empty() ? "none" : wifiCtrl.run.quickDiskImage[0];
|
|
pairs.push_back(keyValue);
|
|
// System status items for the dashboard panel.
|
|
// Guard against uninitialised flash header (INF not yet received from RP2350).
|
|
{
|
|
uint8_t activeApp = wifiCtrl.run.rp2350FlashHeader.activeApp;
|
|
bool haveInfo = (wifiCtrl.run.rp2350FlashHeader.configured == FLASH_APP_CONFIGURED_FLAG &&
|
|
activeApp >= 1 && activeApp < FLASH_APP_MAX_INSTANCES);
|
|
t_FlashPartitionInstance *ai = haveInfo ? &wifiCtrl.run.rp2350FlashHeader.config[activeApp] : NULL;
|
|
|
|
keyValue.name = "%SK_FWVERSION%";
|
|
keyValue.value = (ai && ai->version[0]) ? std::string(ai->version) + " (" + std::string(ai->versionDate) + ")" : "N/A";
|
|
pairs.push_back(keyValue);
|
|
|
|
keyValue.name = "%SK_ESPVERSION%";
|
|
{
|
|
const esp_app_desc_t *desc = esp_app_get_description();
|
|
keyValue.value = std::string(desc->version) + " (" + std::string(desc->date) + ")";
|
|
}
|
|
pairs.push_back(keyValue);
|
|
|
|
keyValue.name = "%SK_ACTIVEPARTITION%";
|
|
keyValue.value = haveInfo ? std::to_string(activeApp) : "N/A";
|
|
pairs.push_back(keyValue);
|
|
|
|
keyValue.name = "%SK_PERSONA%";
|
|
keyValue.value = (ai && ai->description[0]) ? std::string(ai->description) : "N/A";
|
|
pairs.push_back(keyValue);
|
|
|
|
keyValue.name = "%SK_FWAUTHOR%";
|
|
keyValue.value = (ai && ai->author[0]) ? std::string(ai->author) : "N/A";
|
|
pairs.push_back(keyValue);
|
|
|
|
keyValue.name = "%SK_FWLICENSE%";
|
|
keyValue.value = (ai && ai->license[0]) ? std::string(ai->license) : "N/A";
|
|
pairs.push_back(keyValue);
|
|
|
|
keyValue.name = "%SK_FWCOPYRIGHT%";
|
|
keyValue.value = (ai && ai->copyright[0]) ? std::string(ai->copyright) : "N/A";
|
|
pairs.push_back(keyValue);
|
|
|
|
// Active drivers — populated by RP2350 via INF IPC command.
|
|
keyValue.name = "%SK_ACTIVEDRIVERS%";
|
|
keyValue.value = (wifiCtrl.run.rp2350DriverSummary[0]) ? std::string(wifiCtrl.run.rp2350DriverSummary) : "N/A";
|
|
pairs.push_back(keyValue);
|
|
|
|
keyValue.name = "%SK_CPUCLOCK%";
|
|
keyValue.value = (wifiCtrl.run.rp2350CpuFreq > 0) ?
|
|
std::to_string(wifiCtrl.run.rp2350CpuFreq / 1000000) + " MHz" : "N/A";
|
|
pairs.push_back(keyValue);
|
|
|
|
keyValue.name = "%SK_PSRAMCLOCK%";
|
|
keyValue.value = (wifiCtrl.run.rp2350PsramFreq > 0) ?
|
|
std::to_string(wifiCtrl.run.rp2350PsramFreq / 1000000) + " MHz" : "N/A";
|
|
pairs.push_back(keyValue);
|
|
|
|
// RP2350 Flash and PSRAM sizes — dynamic from INF payload.
|
|
keyValue.name = "%SK_RP2350FLASH%";
|
|
keyValue.value = (wifiCtrl.run.rp2350FlashSize > 0) ?
|
|
std::to_string(wifiCtrl.run.rp2350FlashSize / (1024 * 1024)) + " MB" : "N/A";
|
|
pairs.push_back(keyValue);
|
|
|
|
keyValue.name = "%SK_RP2350PSRAM%";
|
|
keyValue.value = (wifiCtrl.run.rp2350PsramSize > 0) ?
|
|
std::to_string(wifiCtrl.run.rp2350PsramSize / (1024 * 1024)) + " MB" : "N/A";
|
|
pairs.push_back(keyValue);
|
|
|
|
// ESP32 info — available directly from ESP-IDF APIs.
|
|
keyValue.name = "%SK_ESP32CLOCK%";
|
|
keyValue.value = std::to_string(CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ) + " MHz";
|
|
pairs.push_back(keyValue);
|
|
|
|
keyValue.name = "%SK_ESP32FLASH%";
|
|
{
|
|
// Use physical size (actual chip) not image header size (may be smaller).
|
|
uint32_t flashSize = 0;
|
|
if (esp_flash_get_physical_size(NULL, &flashSize) != ESP_OK)
|
|
esp_flash_get_size(NULL, &flashSize); // Fallback to image header size
|
|
keyValue.value = (flashSize > 0) ? std::to_string(flashSize / (1024 * 1024)) + " MB" : "N/A";
|
|
}
|
|
pairs.push_back(keyValue);
|
|
|
|
keyValue.name = "%SK_ESP32PSRAM%";
|
|
keyValue.value = std::to_string(esp_psram_get_size() / (1024 * 1024)) + " MB";
|
|
pairs.push_back(keyValue);
|
|
|
|
keyValue.name = "%SK_SDCARD%";
|
|
keyValue.value = (sdcard != NULL) ? "Mounted" : "Not available";
|
|
pairs.push_back(keyValue);
|
|
|
|
keyValue.name = "%SK_UPTIME%";
|
|
{
|
|
int64_t us = esp_timer_get_time();
|
|
int secs = (int)(us / 1000000);
|
|
int days = secs / 86400; secs %= 86400;
|
|
int hrs = secs / 3600; secs %= 3600;
|
|
int mins = secs / 60; secs %= 60;
|
|
char buf[32];
|
|
if (days > 0) snprintf(buf, sizeof(buf), "%dd %02d:%02d:%02d", days, hrs, mins, secs);
|
|
else snprintf(buf, sizeof(buf), "%02d:%02d:%02d", hrs, mins, secs);
|
|
keyValue.value = buf;
|
|
}
|
|
pairs.push_back(keyValue);
|
|
}
|
|
keyValue.name = "%SK_FILEDIR%";
|
|
keyValue.value = "";
|
|
pairs.push_back(keyValue);
|
|
|
|
// Go through list of place holders to expand and replace (all occurrences per line).
|
|
for (const auto &pair : pairs)
|
|
{
|
|
if (pair.name == "%SK_FILEDIR%")
|
|
{
|
|
if (str.find(pair.name) != std::string::npos)
|
|
largeMacroDetected = true;
|
|
continue;
|
|
}
|
|
size_t pos = 0;
|
|
while ((pos = str.find(pair.name, pos)) != std::string::npos)
|
|
{
|
|
str.replace(pos, pair.name.length(), pair.value);
|
|
pos += pair.value.length();
|
|
}
|
|
}
|
|
str += "\n";
|
|
|
|
// Normal macros have been expanded, if no large macros were detected, send the expanded string and return.
|
|
if (!largeMacroDetected)
|
|
{
|
|
if (!str.empty())
|
|
{
|
|
result = httpd_resp_send_chunk(req, str.c_str(), str.length());
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Repeat the key:value search, locating the large macro, only 1 is allowed per line.
|
|
for (const auto &pair : pairs)
|
|
{
|
|
size_t pos = str.find(pair.name);
|
|
if (pos != std::string::npos)
|
|
{
|
|
size_t endPos = pos + pair.name.length();
|
|
if (pos > 0)
|
|
{
|
|
if (httpd_resp_send_chunk(req, str.substr(0, pos).c_str(), pos) != ESP_OK)
|
|
{
|
|
result = ESP_FAIL;
|
|
}
|
|
}
|
|
if (result == ESP_OK)
|
|
{
|
|
if (pair.name == "%SK_FILEDIR%")
|
|
{
|
|
result = pThis->sendFileManagerDir(req);
|
|
}
|
|
if (result == ESP_OK && endPos < str.length())
|
|
{
|
|
result = httpd_resp_send_chunk(req, str.substr(endPos).c_str(), str.length() - endPos);
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Debug, track heap size.
|
|
ESP_LOGD(WIFITAG, "After expansion Free Heap (%d)", xPortGetFreeHeapSize());
|
|
|
|
// Return result of expansion/transmission.
|
|
return result;
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// versionedRename — rename src to dst, preserving any existing dst as dst;N.
|
|
//
|
|
// If dst already exists it is renamed to dst;1. If dst;1 exists, dst;2, etc.
|
|
// The source is then renamed to dst so the caller always ends up with their
|
|
// content at the expected path. Returns ESP_OK on success; on failure errMsg
|
|
// is set and dst is left unchanged (src is never moved if versioning fails).
|
|
// ---------------------------------------------------------------------------
|
|
esp_err_t WiFi::versionedRename(const std::string &src, const std::string &dst, std::string &errMsg)
|
|
{
|
|
struct stat st;
|
|
|
|
// If dst already exists, find the next free versioned slot and move it there.
|
|
if (stat(dst.c_str(), &st) == 0)
|
|
{
|
|
int counter = 1;
|
|
std::string versioned;
|
|
do
|
|
{
|
|
if (counter > 999)
|
|
{
|
|
errMsg = "Version counter exceeded 999 for: " + dst + " — manually purge old versions.";
|
|
return ESP_FAIL;
|
|
}
|
|
versioned = dst + ";" + std::to_string(counter++);
|
|
} while (stat(versioned.c_str(), &st) == 0);
|
|
|
|
if (rename(dst.c_str(), versioned.c_str()) != 0)
|
|
{
|
|
errMsg = "Failed to version " + dst + " -> " + versioned;
|
|
return ESP_FAIL;
|
|
}
|
|
}
|
|
|
|
// dst is now free — rename src into place.
|
|
// If src == dst this is a backup-only operation (the dst was already versioned
|
|
// above), so skip the rename — there's nothing to move.
|
|
if (src == dst)
|
|
{
|
|
return ESP_OK;
|
|
}
|
|
if (rename(src.c_str(), dst.c_str()) != 0)
|
|
{
|
|
errMsg = "Failed to rename " + src + " -> " + dst;
|
|
return ESP_FAIL;
|
|
}
|
|
|
|
return ESP_OK;
|
|
}
|
|
|
|
// 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, const std::string &fileName)
|
|
{
|
|
// Locals.
|
|
std::ifstream inFile;
|
|
esp_err_t result = ESP_OK;
|
|
std::string fqfn = std::string(basePath) + "/" + fileName;
|
|
|
|
// Ensure the content type is set correctly.
|
|
result = setContentTypeFromFileType(req, fileName);
|
|
if (result != ESP_OK)
|
|
return result;
|
|
|
|
inFile.open(fqfn.c_str());
|
|
if (!inFile.is_open())
|
|
{
|
|
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Failed to open template file.");
|
|
ESP_LOGI(WIFITAG, "Failed to open file => %s", fqfn.c_str());
|
|
return ESP_FAIL;
|
|
}
|
|
|
|
// Read the entire file in one SD card operation — dramatically faster than
|
|
// getline() per line (each getline triggers a separate SD card fread, ~2-3 ms
|
|
// per call; a 300-line file would cost 600-900 ms in SD I/O alone).
|
|
std::string contents((std::istreambuf_iterator<char>(inFile)), std::istreambuf_iterator<char>());
|
|
inFile.close();
|
|
ESP_LOGI(WIFITAG, "expandAndSendFile: %s loaded (%d bytes)", fileName.c_str(), (int)contents.length());
|
|
|
|
// Close connection after response — see defaultFileHandler for rationale.
|
|
httpd_resp_set_hdr(req, "Connection", "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);
|
|
if (result != ESP_OK)
|
|
{
|
|
int sockFd = httpd_req_to_sockfd(req);
|
|
if (sockFd != -1)
|
|
close(sockFd);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Template variables present — process line by line from the in-memory
|
|
// string (no SD card I/O per line).
|
|
std::istringstream stream(contents);
|
|
std::string line;
|
|
int lineNum = 0;
|
|
while (result == ESP_OK && std::getline(stream, line))
|
|
{
|
|
lineNum++;
|
|
result = expandVarsAndSend(req, line);
|
|
if (result != ESP_OK)
|
|
{
|
|
ESP_LOGE(WIFITAG, "expandAndSendFile: FAILED at line %d: %.60s", lineNum, line.c_str());
|
|
httpd_resp_send_chunk(req, NULL, 0);
|
|
int sockFd = httpd_req_to_sockfd(req);
|
|
if (sockFd != -1)
|
|
close(sockFd);
|
|
}
|
|
}
|
|
ESP_LOGI(WIFITAG, "expandAndSendFile: done, %d lines, result=%d", lineNum, (int)result);
|
|
if (result == ESP_OK)
|
|
result = httpd_resp_send_chunk(req, NULL, 0);
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
// Method to check if a file has a specific extension.
|
|
bool WiFi::isFileExt(const std::string &fileName, const std::string &extension)
|
|
{
|
|
// Locals.
|
|
bool retVal = 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.
|
|
retVal = true;
|
|
if (extension.find_first_of(".") != extension.find_last_of(".") &&
|
|
strcasecmp(fileName.substr(fileName.find_first_of(".")).c_str(), extension.c_str()) != 0)
|
|
{
|
|
retVal = false;
|
|
}
|
|
}
|
|
|
|
// Extension match?
|
|
return (retVal);
|
|
}
|
|
|
|
// Method to set the HTTP response content type according to file extension.
|
|
esp_err_t WiFi::setContentTypeFromFileType(httpd_req_t *req, const std::string &fileName)
|
|
{
|
|
// Locals.
|
|
static const std::map<std::string, std::string> contentTypes = {{".pdf", "application/pdf"},
|
|
{".html", "text/html"},
|
|
{".htm", "text/html"},
|
|
{".css", "text/css"},
|
|
{".js", "application/javascript"},
|
|
{".ico", "image/x-icon"},
|
|
{".jpeg", "image/jpeg"},
|
|
{".jpg", "image/jpeg"},
|
|
{".bin", "application/octet-stream"},
|
|
{".bmp", "image/bmp"},
|
|
{".gif", "image/gif"},
|
|
{".jar", "application/java-archive"},
|
|
{".json", "application/json"},
|
|
{".png", "image/png"},
|
|
{".php", "application/x-httod-php"},
|
|
{".rtf", "application/rtf"},
|
|
{".tif", "image/tiff"},
|
|
{".tiff", "image/tiff"},
|
|
{".txt", "text/plain"},
|
|
{".xml", "application/xml"}};
|
|
std::string ext = fileName.substr(fileName.find_last_of('.'));
|
|
auto it = contentTypes.find(ext);
|
|
const char *type = it != contentTypes.end() ? it->second.c_str() : "text/plain";
|
|
esp_err_t result = ESP_OK;
|
|
|
|
result = httpd_resp_set_type(req, type);
|
|
return result;
|
|
}
|
|
|
|
// Locates the path within URI and 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, '#');
|
|
esp_err_t result = ESP_OK;
|
|
|
|
// 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 = "";
|
|
if (pathlen > 1)
|
|
{
|
|
destFile.append(uri, 1, pathlen - 1);
|
|
}
|
|
|
|
// Result, fail if no path extracted.
|
|
if (destFile.empty())
|
|
{
|
|
result = ESP_FAIL;
|
|
}
|
|
return result;
|
|
}
|
|
|
|
// 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);
|
|
if (destPath.find(basePath) != std::string::npos)
|
|
{
|
|
destPath.erase(0, strlen(basePath));
|
|
}
|
|
else
|
|
{
|
|
result = ESP_FAIL;
|
|
}
|
|
|
|
// 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 = nullptr;
|
|
struct stat fileStat;
|
|
std::unique_ptr<char[]> buf;
|
|
int bufLen;
|
|
std::string gzipFile = "";
|
|
std::string disposition = "";
|
|
esp_err_t result = ESP_OK;
|
|
|
|
// 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
|
|
// (typically jquery.min.js) stuck forever in "pending" state.
|
|
std::string localFilePath;
|
|
std::string localFileName;
|
|
bool localGzip = false;
|
|
|
|
// Retrieve pointer to object in order to access data.
|
|
WiFi *pThis = (WiFi *) req->user_ctx;
|
|
|
|
// Get required Header values for processing.
|
|
bufLen = httpd_req_get_hdr_value_len(req, "Host") + 1;
|
|
if (bufLen > 1)
|
|
{
|
|
buf.reset(new char[bufLen]);
|
|
if (buf != nullptr && httpd_req_get_hdr_value_str(req, "Host", buf.get(), bufLen) == ESP_OK)
|
|
{
|
|
pThis->wifiCtrl.session.host = buf.get();
|
|
}
|
|
}
|
|
// Get encoding methods.
|
|
bufLen = httpd_req_get_hdr_value_len(req, "Accept-Encoding") + 1;
|
|
if (bufLen > 1)
|
|
{
|
|
buf.reset(new char[bufLen]);
|
|
if (buf != nullptr && httpd_req_get_hdr_value_str(req, "Accept-Encoding", buf.get(), bufLen) == ESP_OK)
|
|
{
|
|
localGzip = (strstr(buf.get(), "gzip") != NULL);
|
|
}
|
|
}
|
|
|
|
// Look for a filename in the URI and construct the file path returning both.
|
|
result = pThis->getPathFromURI(localFilePath, localFileName, pThis->wifiCtrl.run.basePath, req->uri);
|
|
if (result == ESP_FAIL)
|
|
{
|
|
if (strlen(req->uri) == 1 && req->uri[0] == '/')
|
|
{
|
|
localFileName = "/";
|
|
result = ESP_OK;
|
|
}
|
|
else
|
|
{
|
|
result = ESP_FAIL;
|
|
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Filename invalid");
|
|
}
|
|
}
|
|
|
|
if (result == ESP_OK)
|
|
{
|
|
// See if the provided name matches a static handler, such as root.
|
|
if (localFileName == "/" || localFileName == "index.html" || localFileName == "index.htm")
|
|
{
|
|
result = pThis->expandAndSendFile(req, pThis->wifiCtrl.run.basePath, "index.htm");
|
|
}
|
|
else
|
|
{
|
|
// Get details of the file.
|
|
if (stat(localFilePath.c_str(), &fileStat) == -1)
|
|
{
|
|
if (localGzip)
|
|
{
|
|
gzipFile = localFilePath + ".gz";
|
|
}
|
|
if (localGzip && stat(gzipFile.c_str(), &fileStat) == -1)
|
|
{
|
|
result = ESP_FAIL;
|
|
httpd_resp_send_err(req, HTTPD_404_NOT_FOUND, "File does not exist");
|
|
ESP_LOGE(WIFITAG, "File not found:%s", gzipFile.c_str());
|
|
}
|
|
else
|
|
{
|
|
ESP_LOGI(WIFITAG, "Opened zipped file : %s ", gzipFile.c_str());
|
|
if (httpd_resp_set_hdr(req, "Content-Encoding", "gzip") != ESP_OK)
|
|
{
|
|
result = ESP_FAIL;
|
|
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Set content encoding to gzip failed");
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
ESP_LOGI(WIFITAG, "Opened file : %s ", localFilePath.c_str());
|
|
}
|
|
|
|
if (result == ESP_OK)
|
|
{
|
|
// If the file is HTML, JS or CSS then process externally.
|
|
if ((pThis->isFileExt(localFileName, ".html") || pThis->isFileExt(localFileName, ".htm") ||
|
|
(pThis->isFileExt(localFileName, ".js") && !pThis->isFileExt(localFileName, ".min.js")) ||
|
|
(pThis->isFileExt(localFileName, ".css") && !pThis->isFileExt(localFileName, ".min.css"))) &&
|
|
gzipFile.empty())
|
|
{
|
|
result = pThis->expandAndSendFile(req, pThis->wifiCtrl.run.basePath, localFileName);
|
|
}
|
|
else
|
|
{
|
|
fd = fopen(gzipFile.empty() ? localFilePath.c_str() : gzipFile.c_str(), "r");
|
|
if (!fd)
|
|
{
|
|
result = ESP_FAIL;
|
|
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Failed to read existing file");
|
|
}
|
|
else
|
|
{
|
|
ESP_LOGI(WIFITAG,
|
|
"Sending %sfile : %s (%ld bytes)...",
|
|
gzipFile.empty() ? "" : "gzip ",
|
|
localFileName.c_str(),
|
|
fileStat.st_size);
|
|
// 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. With 20 sockets available this works well.
|
|
httpd_resp_set_hdr(req, "Connection", "close");
|
|
result = pThis->setContentTypeFromFileType(req, localFileName);
|
|
if (result == ESP_OK)
|
|
{
|
|
std::unique_ptr<char[]> chunk(new char[MAX_CHUNK_SIZE]);
|
|
if (chunk == nullptr)
|
|
{
|
|
// Memory exhaustion — no body bytes sent yet, so close the socket.
|
|
// Do NOT call httpd_resp_send_err() here: headers were already
|
|
// queued by set_hdr/setContentType; sending an error response now
|
|
// would corrupt the in-flight HTTP state.
|
|
result = ESP_FAIL;
|
|
ESP_LOGE(WIFITAG, "Memory exhausted in defaultFileHandler — closing socket");
|
|
int sockFd = httpd_req_to_sockfd(req);
|
|
if (sockFd != -1)
|
|
close(sockFd);
|
|
}
|
|
else
|
|
{
|
|
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);
|
|
}
|
|
}
|
|
if (result == ESP_OK)
|
|
{
|
|
result = httpd_resp_send_chunk(req, NULL, 0);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Respond with an empty chunk to signal HTTP response completion if successful.
|
|
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.
|
|
std::unique_ptr<char[]> buf;
|
|
int bufLen;
|
|
esp_err_t result = ESP_OK;
|
|
std::string directory;
|
|
std::string sdpath;
|
|
std::string uriStr;
|
|
std::string oldname;
|
|
std::string name;
|
|
std::string resp;
|
|
std::string uri;
|
|
std::string url;
|
|
std::string urq;
|
|
std::vector<t_kvPair> pairs;
|
|
struct stat s;
|
|
bool dirExists;
|
|
|
|
// Retrieve pointer to object in order to access data.
|
|
WiFi *pThis = (WiFi *) req->user_ctx;
|
|
|
|
// Split the URI into components for ease of processing.
|
|
pThis->splitURI(req, uri, url, urq, pairs);
|
|
|
|
// Get required Header values for processing.
|
|
bufLen = httpd_req_get_hdr_value_len(req, "Host") + 1;
|
|
if (bufLen > 1)
|
|
{
|
|
buf.reset(new char[bufLen]);
|
|
if (buf != nullptr && httpd_req_get_hdr_value_str(req, "Host", buf.get(), bufLen) == ESP_OK)
|
|
{
|
|
pThis->wifiCtrl.session.host = buf.get();
|
|
}
|
|
}
|
|
|
|
// Get the subpath from the URI.
|
|
result = pThis->getPathFromURI(uriStr, "/data/", req->uri);
|
|
if (result == ESP_FAIL)
|
|
{
|
|
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Failed to extract URI");
|
|
}
|
|
else
|
|
{
|
|
// Loop through all the URI key pairs, updating configuration values as necessary.
|
|
directory = "/";
|
|
for (auto pair : pairs)
|
|
{
|
|
if (pair.name == "dir")
|
|
{
|
|
directory = pair.value;
|
|
if (directory.length() > 1 && directory.back() == '/')
|
|
{
|
|
directory.pop_back();
|
|
}
|
|
}
|
|
else if (pair.name == "oldname")
|
|
{
|
|
oldname = pair.value;
|
|
}
|
|
else if (pair.name == "name")
|
|
{
|
|
name = pair.value;
|
|
}
|
|
}
|
|
sdpath = pThis->wifiCtrl.run.fsPath;
|
|
if (sdpath.back() != '/')
|
|
{
|
|
sdpath.append("/");
|
|
}
|
|
// If the name has a path assigned then dont add directory. Allows for provision of full target name.
|
|
if (name.substr(0, 1) != "/")
|
|
sdpath.append(directory);
|
|
pThis->stringReplace(sdpath, "//", "/");
|
|
|
|
// Match URI and execute required data retrieval and return.
|
|
if (uriStr == "config")
|
|
{
|
|
// Return the contents of config.json from the SD card root.
|
|
// The file is sent as-is (including any C-style comments and hex
|
|
// literals) — the browser-side JavaScript handles parsing.
|
|
std::string cfgPath = std::string(pThis->wifiCtrl.run.fsPath);
|
|
if (cfgPath.back() != '/') cfgPath += "/";
|
|
cfgPath += "config.json";
|
|
|
|
FILE *cfgFd = fopen(cfgPath.c_str(), "rb");
|
|
if (!cfgFd)
|
|
{
|
|
httpd_resp_send_err(req, HTTPD_404_NOT_FOUND, "config.json not found on SD card");
|
|
return ESP_FAIL;
|
|
}
|
|
|
|
// Send as chunked response to avoid allocating the entire file in memory.
|
|
httpd_resp_set_type(req, "application/json");
|
|
httpd_resp_set_hdr(req, "Cache-Control", "no-cache");
|
|
|
|
std::unique_ptr<char[]> cfgChunk(new char[MAX_CHUNK_SIZE]);
|
|
if (cfgChunk == nullptr)
|
|
{
|
|
fclose(cfgFd);
|
|
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Memory allocation failed");
|
|
return ESP_FAIL;
|
|
}
|
|
|
|
size_t bytesRead;
|
|
esp_err_t cfgResult = ESP_OK;
|
|
do
|
|
{
|
|
bytesRead = fread(cfgChunk.get(), 1, MAX_CHUNK_SIZE, cfgFd);
|
|
if (bytesRead > 0)
|
|
{
|
|
if (httpd_resp_send_chunk(req, cfgChunk.get(), bytesRead) != ESP_OK)
|
|
{
|
|
cfgResult = ESP_FAIL;
|
|
break;
|
|
}
|
|
}
|
|
} while (bytesRead > 0);
|
|
|
|
fclose(cfgFd);
|
|
|
|
if (cfgResult == ESP_OK)
|
|
httpd_resp_send_chunk(req, NULL, 0);
|
|
|
|
return cfgResult;
|
|
}
|
|
else if (uriStr == "wifistatus")
|
|
{
|
|
// JSON endpoint for AJAX polling of WiFi + system + RP2350 status.
|
|
// All fields that can change at runtime are included so the dashboard
|
|
// updates live without requiring a page refresh.
|
|
int8_t txPwr = 0;
|
|
int rssi = 0;
|
|
bool connected = pThis->wifiCtrl.client.connected;
|
|
const char *ip = (pThis->wifiCtrl.run.wifiMode == WIFI_CONFIG_AP)
|
|
? pThis->wifiCtrl.ap.ip : pThis->wifiCtrl.client.ip;
|
|
const char *nm = (pThis->wifiCtrl.run.wifiMode == WIFI_CONFIG_AP)
|
|
? pThis->wifiCtrl.ap.netmask : pThis->wifiCtrl.client.netmask;
|
|
const char *gw = (pThis->wifiCtrl.run.wifiMode == WIFI_CONFIG_AP)
|
|
? pThis->wifiCtrl.ap.gateway : pThis->wifiCtrl.client.gateway;
|
|
#if defined(CONFIG_IF_WIFI_ENABLED)
|
|
esp_wifi_get_max_tx_power(&txPwr);
|
|
wifi_ap_record_t apInfo;
|
|
if (connected && esp_wifi_sta_get_ap_info(&apInfo) == ESP_OK)
|
|
rssi = apInfo.rssi;
|
|
#endif
|
|
// Compute uptime string.
|
|
int64_t us = esp_timer_get_time();
|
|
int secs = (int) (us / 1000000);
|
|
int days = secs / 86400; secs %= 86400;
|
|
int hrs = secs / 3600; secs %= 3600;
|
|
int mins = secs / 60; secs %= 60;
|
|
char uptimeBuf[32];
|
|
if (days > 0)
|
|
snprintf(uptimeBuf, sizeof(uptimeBuf), "%dd %02d:%02d:%02d", days, hrs, mins, secs);
|
|
else
|
|
snprintf(uptimeBuf, sizeof(uptimeBuf), "%02d:%02d:%02d", hrs, mins, secs);
|
|
|
|
// RP2350 partition header data (populated by INF IPC from RP2350).
|
|
uint8_t activeApp = pThis->wifiCtrl.run.rp2350FlashHeader.activeApp;
|
|
bool haveInfo = (pThis->wifiCtrl.run.rp2350FlashHeader.configured == FLASH_APP_CONFIGURED_FLAG &&
|
|
activeApp >= 1 && activeApp < FLASH_APP_MAX_INSTANCES);
|
|
t_FlashPartitionInstance *ai = haveInfo ? &pThis->wifiCtrl.run.rp2350FlashHeader.config[activeApp] : NULL;
|
|
|
|
const char *fwVer = (ai && ai->version[0]) ? ai->version : "";
|
|
const char *fwDate = (ai && ai->versionDate[0]) ? ai->versionDate : "";
|
|
const char *persona = (ai && ai->description[0]) ? ai->description : "";
|
|
const char *drvSummary = pThis->wifiCtrl.run.rp2350DriverSummary[0] ? pThis->wifiCtrl.run.rp2350DriverSummary : "";
|
|
int cpuMHz = pThis->wifiCtrl.run.rp2350CpuFreq / 1000000;
|
|
int psramMHz = pThis->wifiCtrl.run.rp2350PsramFreq / 1000000;
|
|
int flashMB = pThis->wifiCtrl.run.rp2350FlashSize / (1024 * 1024);
|
|
int psramMB = pThis->wifiCtrl.run.rp2350PsramSize / (1024 * 1024);
|
|
uint32_t hostClkHz = pThis->wifiCtrl.run.rp2350HostClkHz;
|
|
uint32_t emulSpeedHz = pThis->wifiCtrl.run.rp2350EmulSpeedHz;
|
|
|
|
// Build RP2350 partition table HTML for live update.
|
|
std::ostringstream rpParts;
|
|
for (int idx = 0; idx < FLASH_APP_MAX_INSTANCES; idx++)
|
|
{
|
|
const t_FlashPartitionInstance *pi = &pThis->wifiCtrl.run.rp2350FlashHeader.config[idx];
|
|
std::string active = (idx == activeApp && haveInfo) ? "Yes" : "No";
|
|
rpParts << "<tr>"
|
|
<< "<td>" << idx << "</td>"
|
|
<< "<td>" << to_str(pi->addr, 0, 16) << "</td>"
|
|
<< "<td>" << to_str(pi->size, 0, 16) << "</td>"
|
|
<< "<td>" << to_str(pi->chksum, 0, 16) << "</td>"
|
|
<< "<td>" << active << "</td>"
|
|
<< "<td>" << pi->license << "</td>"
|
|
<< "<td>" << pi->author << "</td>"
|
|
<< "<td>" << pi->description << "</td>"
|
|
<< "<td>" << pi->version << "</td>"
|
|
<< "<td>" << pi->versionDate << "</td>"
|
|
<< "<td>" << pi->copyright << "</td>"
|
|
<< "</tr>";
|
|
}
|
|
// Escape the HTML for safe embedding in JSON string value.
|
|
std::string rpPartsHtml = rpParts.str();
|
|
std::string rpPartsEsc;
|
|
rpPartsEsc.reserve(rpPartsHtml.size() + 64);
|
|
for (char c : rpPartsHtml)
|
|
{
|
|
if (c == '"') rpPartsEsc += "\\\"";
|
|
else if (c == '\\') rpPartsEsc += "\\\\";
|
|
else if (c == '\n') rpPartsEsc += "\\n";
|
|
else rpPartsEsc += c;
|
|
}
|
|
|
|
// Build JSON response using std::string (partition HTML can be large).
|
|
std::ostringstream json;
|
|
json << "{\"rssi\":" << rssi
|
|
<< ",\"txPower\":" << (txPwr * 0.25f)
|
|
<< ",\"connected\":" << (connected ? "true" : "false")
|
|
<< ",\"uptime\":\"" << uptimeBuf << "\""
|
|
<< ",\"ip\":\"" << (ip[0] ? ip : "") << "\""
|
|
<< ",\"netmask\":\"" << (nm[0] ? nm : "") << "\""
|
|
<< ",\"gateway\":\"" << (gw[0] ? gw : "") << "\""
|
|
<< ",\"fwVersion\":\"" << fwVer << "\""
|
|
<< ",\"fwDate\":\"" << fwDate << "\""
|
|
<< ",\"activePartition\":" << (haveInfo ? std::to_string(activeApp) : "null")
|
|
<< ",\"persona\":\"" << persona << "\""
|
|
<< ",\"drivers\":\"" << drvSummary << "\""
|
|
<< ",\"rp2350Clock\":" << (cpuMHz > 0 ? std::to_string(cpuMHz) : "null")
|
|
<< ",\"psramClock\":" << (psramMHz > 0 ? std::to_string(psramMHz) : "null")
|
|
<< ",\"rp2350Flash\":" << (flashMB > 0 ? std::to_string(flashMB) : "null")
|
|
<< ",\"rp2350Psram\":" << (psramMB > 0 ? std::to_string(psramMB) : "null")
|
|
<< ",\"hostClkHz\":" << (hostClkHz > 0 ? std::to_string(hostClkHz) : "null")
|
|
<< ",\"emulSpeedHz\":" << (emulSpeedHz > 0 ? std::to_string(emulSpeedHz) : "null")
|
|
<< ",\"rpPartitions\":\"" << rpPartsEsc << "\""
|
|
<< ",\"floppy1\":\"" << (pThis->wifiCtrl.run.floppyDiskImage[0].empty() ? "none" : pThis->wifiCtrl.run.floppyDiskImage[0]) << "\""
|
|
<< ",\"floppy2\":\"" << (pThis->wifiCtrl.run.floppyDiskImage[1].empty() ? "none" : pThis->wifiCtrl.run.floppyDiskImage[1]) << "\""
|
|
<< ",\"qdisk\":\"" << (pThis->wifiCtrl.run.quickDiskImage[0].empty() ? "none" : pThis->wifiCtrl.run.quickDiskImage[0]) << "\""
|
|
<< "}";
|
|
httpd_resp_set_type(req, "application/json");
|
|
httpd_resp_sendstr(req, json.str().c_str());
|
|
return ESP_OK;
|
|
}
|
|
else if (uriStr.substr(0, 6) == "rename")
|
|
{
|
|
if (!oldname.empty() && !name.empty())
|
|
{
|
|
oldname.insert(0, sdpath);
|
|
std::string dst = sdpath + name;
|
|
std::string errMsg;
|
|
result = pThis->versionedRename(oldname, dst, errMsg);
|
|
if (result != ESP_OK)
|
|
ESP_LOGE(WIFITAG, "versionedRename: %s", errMsg.c_str());
|
|
}
|
|
else
|
|
{
|
|
result = ESP_FAIL;
|
|
}
|
|
}
|
|
else if (uriStr.substr(0, 6) == "delete")
|
|
{
|
|
if (!name.empty())
|
|
{
|
|
struct stat fileStat;
|
|
name.insert(0, "/").insert(0, sdpath);
|
|
|
|
if (stat(name.c_str(), &fileStat) == 0)
|
|
{
|
|
if (S_ISREG(fileStat.st_mode))
|
|
{
|
|
if (std::remove(name.c_str()) != 0)
|
|
{
|
|
result = ESP_FAIL;
|
|
}
|
|
}
|
|
else if (S_ISDIR(fileStat.st_mode))
|
|
{
|
|
if (!pThis->sdcard->deleteDir(name))
|
|
{
|
|
result = ESP_FAIL;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
result = ESP_FAIL;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
result = ESP_FAIL;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
result = ESP_FAIL;
|
|
}
|
|
}
|
|
else if (uriStr.substr(0, 5) == "mkdir")
|
|
{
|
|
directory.insert(0, pThis->wifiCtrl.run.fsPath);
|
|
dirExists = stat(directory.c_str(), &s) == 0;
|
|
if (!dirExists)
|
|
{
|
|
if (mkdir(directory.c_str(), 0775) != 0)
|
|
{
|
|
result = ESP_FAIL;
|
|
resp = "mkdir failed for directory:" + directory;
|
|
}
|
|
}
|
|
}
|
|
else if (uriStr.substr(0, 8) == "download")
|
|
{
|
|
result = pThis->downloadFileHandler(req, directory, name, resp);
|
|
if (result == ESP_OK)
|
|
{
|
|
resp = "File downloaded.";
|
|
}
|
|
}
|
|
|
|
// Check result and raise error or signal success. If resp has a message when in error, send it.
|
|
if (result != ESP_OK)
|
|
{
|
|
//httpd_resp_sendstr_chunk(req, NULL);
|
|
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, resp.empty() ? "Failed to send data." : resp.c_str());
|
|
}
|
|
else
|
|
{
|
|
result = httpd_resp_send_chunk(req, NULL, 0);
|
|
}
|
|
}
|
|
|
|
// Return result, successful data send == ESP_OK.
|
|
return result;
|
|
}
|
|
|
|
// Handler for to download a requested file.
|
|
esp_err_t WiFi::downloadFileHandler(httpd_req_t *req, std::string &directory, std::string &filename, std::string &resp)
|
|
{
|
|
// Locals.
|
|
bool dataError = false;
|
|
std::string srcDir;
|
|
std::string srcFile;
|
|
struct stat fileHdl;
|
|
FILE *inFile = nullptr;
|
|
esp_err_t result = ESP_OK;
|
|
|
|
// Retrieve pointer to object in order to access data.
|
|
WiFi *pThis = (WiFi *) req->user_ctx;
|
|
|
|
// Working directory for this file.
|
|
srcDir = pThis->wifiCtrl.run.fsPath;
|
|
srcDir += "/";
|
|
srcDir += directory;
|
|
srcDir += "/";
|
|
stringReplace(srcDir, "//", "/");
|
|
srcFile = srcDir + filename;
|
|
|
|
// Check if file exists
|
|
if (stat(srcFile.c_str(), &fileHdl) == -1)
|
|
{
|
|
resp = "File not found";
|
|
dataError = true;
|
|
}
|
|
else
|
|
{
|
|
inFile = fopen(srcFile.c_str(), "r");
|
|
if (!inFile)
|
|
{
|
|
resp = "File does not exist:" + srcFile;
|
|
dataError = true;
|
|
}
|
|
else
|
|
{
|
|
// Set response headers
|
|
httpd_resp_set_type(req, "application/octet-stream");
|
|
char content_disposition[100];
|
|
snprintf(content_disposition, sizeof(content_disposition), "attachment; filename=\"%s\"", filename.c_str());
|
|
httpd_resp_set_hdr(req, "Content-Disposition", content_disposition);
|
|
|
|
std::unique_ptr<char[]> chunk(new char[MAX_CHUNK_SIZE]);
|
|
if (chunk == nullptr)
|
|
{
|
|
resp = "Memory exhausted in downloadFileHandler.";
|
|
dataError = true;
|
|
}
|
|
else
|
|
{
|
|
size_t chunksize;
|
|
do
|
|
{
|
|
chunksize = fread(chunk.get(), 1, MAX_CHUNK_SIZE, inFile);
|
|
if (chunksize > 0)
|
|
{
|
|
if (httpd_resp_send_chunk(req, chunk.get(), chunksize) != ESP_OK)
|
|
{
|
|
resp = "Failed to send file.";
|
|
dataError = true;
|
|
httpd_resp_send_chunk(req, NULL, 0);
|
|
int sockFd = httpd_req_to_sockfd(req);
|
|
if (sockFd != -1)
|
|
close(sockFd);
|
|
}
|
|
}
|
|
} while (dataError == false && chunksize != 0);
|
|
}
|
|
}
|
|
fclose(inFile);
|
|
}
|
|
|
|
// Set exit based on processing result.
|
|
if (dataError)
|
|
{
|
|
result = ESP_FAIL;
|
|
}
|
|
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 within this ESP32 module.
|
|
esp_err_t WiFi::otaESP32FirmwareUpdatePOSTHandler(httpd_req_t *req)
|
|
{
|
|
// Locals.
|
|
esp_err_t result = 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 = esp_ota_get_last_invalid_partition();
|
|
const esp_partition_t *runningApp = esp_ota_get_running_partition();
|
|
const esp_partition_t *updatePartition = esp_ota_get_next_update_partition(NULL);
|
|
std::unique_ptr<char[]> chunk;
|
|
size_t chunkSize;
|
|
int remaining;
|
|
|
|
// Retrieve pointer to object in order to access data.
|
|
//WiFi* pThis = (WiFi*)req->user_ctx;
|
|
|
|
// Get current configuration and next available partition.
|
|
if (!runningApp || !updatePartition)
|
|
{
|
|
result = ESP_FAIL;
|
|
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Failed to resolve current/next NVS OTA partition information.");
|
|
}
|
|
else
|
|
{
|
|
chunk.reset(new char[MAX_CHUNK_SIZE]);
|
|
if (chunk == nullptr)
|
|
{
|
|
result = ESP_FAIL;
|
|
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Memory exhausted in otaESP32FirmwareUpdatePOSTHandler");
|
|
}
|
|
|
|
remaining = req->content_len;
|
|
while (remaining > 0 && result == ESP_OK)
|
|
{
|
|
chunkSize = httpd_req_recv(req, chunk.get(), MIN(remaining, MAX_CHUNK_SIZE));
|
|
if (chunkSize <= 0)
|
|
{
|
|
if (chunkSize == HTTPD_SOCK_ERR_TIMEOUT)
|
|
{
|
|
continue;
|
|
}
|
|
result = ESP_FAIL;
|
|
int sockFd = httpd_req_to_sockfd(req);
|
|
if (sockFd != -1)
|
|
close(sockFd);
|
|
}
|
|
else if (checkImageHeader)
|
|
{
|
|
if (chunkSize > sizeof(esp_image_header_t) + sizeof(esp_image_segment_header_t) + sizeof(esp_app_desc_t))
|
|
{
|
|
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)
|
|
{
|
|
result = ESP_FAIL;
|
|
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "File image is not a valid firmware file.");
|
|
}
|
|
else
|
|
{
|
|
if (esp_ota_get_partition_description(runningApp, &runningAppInfo) == ESP_OK)
|
|
{
|
|
ESP_LOGI(WIFITAG, "Running firmware version: %s, new: %s", runningAppInfo.version, newAppInfo.version);
|
|
}
|
|
if (strcmp(newAppInfo.version, runningAppInfo.version) == 0)
|
|
{
|
|
result = ESP_FAIL;
|
|
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Firmware version is same as current version.");
|
|
}
|
|
else if (lastInvalidApp && esp_ota_get_partition_description(lastInvalidApp, &invalidAppInfo) == ESP_OK)
|
|
{
|
|
ESP_LOGI(WIFITAG, "Last invalid firmware version: %s", invalidAppInfo.version);
|
|
if (memcmp(invalidAppInfo.version, newAppInfo.version, sizeof(newAppInfo.version)) == 0)
|
|
{
|
|
result = ESP_FAIL;
|
|
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Firmware version is known as bad, it previously failed to boot.");
|
|
}
|
|
}
|
|
if (result == ESP_OK)
|
|
{
|
|
checkImageHeader = false;
|
|
result = esp_ota_begin(updatePartition, OTA_WITH_SEQUENTIAL_WRITES, &updateHandle);
|
|
if (result != ESP_OK)
|
|
{
|
|
esp_ota_abort(updateHandle);
|
|
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Failed to initialise NVS OTA partition for writing.");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
result = ESP_FAIL;
|
|
esp_ota_abort(updateHandle);
|
|
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Failed to receive sufficient bytes from file to identify image.");
|
|
}
|
|
}
|
|
if (result == ESP_OK)
|
|
{
|
|
result = esp_ota_write(updateHandle, (const void *) chunk.get(), chunkSize);
|
|
if (result != ESP_OK)
|
|
{
|
|
esp_ota_abort(updateHandle);
|
|
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Failed to write data to NVS partition.");
|
|
}
|
|
else
|
|
{
|
|
remaining -= chunkSize;
|
|
}
|
|
}
|
|
}
|
|
if (result == ESP_OK)
|
|
{
|
|
result = esp_ota_end(updateHandle);
|
|
if (result != ESP_OK)
|
|
{
|
|
if (result == ESP_ERR_OTA_VALIDATE_FAILED)
|
|
{
|
|
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Image validation failed, image is corrupt.");
|
|
}
|
|
else
|
|
{
|
|
std::string errMsg = "Image completion failed:" + std::string(esp_err_to_name(result));
|
|
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, errMsg.c_str());
|
|
}
|
|
}
|
|
else
|
|
{
|
|
result = esp_ota_set_boot_partition(updatePartition);
|
|
if (result != ESP_OK)
|
|
{
|
|
std::string errMsg = "Set boot partition to new image failed:" + std::string(esp_err_to_name(result));
|
|
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, errMsg.c_str());
|
|
}
|
|
else
|
|
{
|
|
vTaskDelay(500);
|
|
httpd_resp_set_status(req, "200 OK");
|
|
httpd_resp_sendstr(req, "");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
// Method to process a file upload to the SD card.
|
|
esp_err_t WiFi::uploadFileHandler(httpd_req_t *req, std::string &file, std::string &resp)
|
|
{
|
|
// Locals.
|
|
bool dataError = false;
|
|
std::string directory;
|
|
std::string uri;
|
|
std::string url;
|
|
std::string urq;
|
|
std::string dstDir;
|
|
t_kvPair keyValue;
|
|
std::vector<t_kvPair> pairs;
|
|
esp_err_t result = ESP_OK;
|
|
|
|
// Retrieve pointer to object in order to access data.
|
|
WiFi *pThis = (WiFi *) req->user_ctx;
|
|
|
|
// Split the URI into components for ease of processing.
|
|
pThis->splitURI(req, uri, url, urq, pairs);
|
|
|
|
// Loop through all the URI key pairs, updating configuration values as necessary.
|
|
directory = "/";
|
|
for (auto pair : pairs)
|
|
{
|
|
if (pair.name == "dir")
|
|
{
|
|
directory = pair.value;
|
|
if (directory.length() > 1 && directory.back() == '/')
|
|
{
|
|
directory.pop_back();
|
|
}
|
|
}
|
|
}
|
|
|
|
// Working directory for this file.
|
|
dstDir = pThis->wifiCtrl.run.fsPath;
|
|
dstDir += "/";
|
|
dstDir += directory;
|
|
|
|
// Fetch the file.
|
|
if (pThis->otaFetchFile(req, file, directory) != ESP_OK)
|
|
{
|
|
dataError = true;
|
|
resp = "Failed to fetch file for upload => " + file + "\n";
|
|
}
|
|
else
|
|
{
|
|
std::filesystem::path filePath = file;
|
|
if (filePath.extension() == ".gz")
|
|
{
|
|
std::filesystem::path dstFile = file;
|
|
dstFile.replace_extension("");
|
|
if (pThis->unpackFile(dstDir, file, dstDir, dstFile) == ESP_OK)
|
|
{
|
|
filePath = dstFile;
|
|
file.insert(0, "/");
|
|
file.insert(0, dstDir);
|
|
remove(file.c_str());
|
|
file = dstFile;
|
|
}
|
|
else
|
|
{
|
|
dataError = true;
|
|
resp = "Failed to gunzip file.";
|
|
}
|
|
}
|
|
if (!dataError && filePath.extension() == ".tar")
|
|
{
|
|
if (pThis->unpackFile(dstDir, file, dstDir, "") == ESP_OK)
|
|
{
|
|
file.insert(0, "/").insert(0, dstDir);
|
|
remove(file.c_str());
|
|
}
|
|
else
|
|
{
|
|
dataError = true;
|
|
resp = "Failed to untar file.";
|
|
}
|
|
}
|
|
}
|
|
|
|
if (dataError)
|
|
{
|
|
result = ESP_FAIL;
|
|
}
|
|
return result;
|
|
}
|
|
|
|
// Overloaded method for files to be stored in the temp directory.
|
|
esp_err_t WiFi::otaFetchFile(httpd_req_t *req, const std::string &createFileName)
|
|
{
|
|
// Locals.
|
|
esp_err_t result = ESP_OK;
|
|
|
|
result = otaFetchFile(req, createFileName, WIFI_TEMP_DIR);
|
|
return result;
|
|
}
|
|
|
|
esp_err_t WiFi::otaFetchFile(httpd_req_t *req, const std::string &createFileName, const std::string &fileDir)
|
|
{
|
|
// Locals.
|
|
esp_err_t result = ESP_OK;
|
|
std::string resp;
|
|
std::ofstream outFile;
|
|
std::unique_ptr<char[]> chunk;
|
|
size_t chunkSize;
|
|
int remaining;
|
|
|
|
// Retrieve pointer to object in order to access data.
|
|
WiFi *pThis = (WiFi *) req->user_ctx;
|
|
|
|
// Build the FQFN of the file to create.
|
|
std::string fqfn = pThis->wifiCtrl.run.fsPath + fileDir + '/' + createFileName;
|
|
ESP_LOGI(WIFITAG, "FW_FETCH: file=%s content_len=%d heap=%lu", fqfn.c_str(), req->content_len, (unsigned long) esp_get_free_heap_size());
|
|
|
|
// Open a stream on the SD card temp directory in which to place the received data.
|
|
outFile.open(fqfn.c_str());
|
|
if (outFile.is_open())
|
|
{
|
|
ESP_LOGI(WIFITAG, "FW_FETCH: file opened OK");
|
|
chunk.reset(new char[MAX_CHUNK_SIZE]);
|
|
if (chunk == nullptr)
|
|
{
|
|
result = ESP_FAIL;
|
|
ESP_LOGE(WIFITAG, "FW_FETCH: chunk alloc failed");
|
|
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Memory exhausted in otaFetchFile");
|
|
}
|
|
|
|
remaining = req->content_len;
|
|
ESP_LOGI(WIFITAG, "FW_FETCH: starting recv loop, remaining=%d", remaining);
|
|
int chunkCount = 0;
|
|
while (remaining > 0 && result == ESP_OK)
|
|
{
|
|
chunkSize = httpd_req_recv(req, chunk.get(), MIN(remaining, MAX_CHUNK_SIZE));
|
|
if (chunkSize <= 0)
|
|
{
|
|
if (chunkSize == HTTPD_SOCK_ERR_TIMEOUT)
|
|
{
|
|
continue;
|
|
}
|
|
ESP_LOGE(WIFITAG, "FW_FETCH: recv error chunkSize=%d remaining=%d", (int) chunkSize, remaining);
|
|
result = ESP_FAIL;
|
|
int sockFd = httpd_req_to_sockfd(req);
|
|
if (sockFd != -1)
|
|
close(sockFd);
|
|
}
|
|
else
|
|
{
|
|
outFile.write(chunk.get(), chunkSize);
|
|
remaining -= chunkSize;
|
|
chunkCount++;
|
|
if ((chunkCount % 10) == 0)
|
|
ESP_LOGI(WIFITAG, "FW_FETCH: chunk %d, remaining=%d", chunkCount, remaining);
|
|
}
|
|
}
|
|
outFile.close();
|
|
ESP_LOGI(WIFITAG, "FW_FETCH: done, chunks=%d result=%d", chunkCount, result);
|
|
}
|
|
else
|
|
{
|
|
result = ESP_FAIL;
|
|
ESP_LOGE(WIFITAG, "FW_FETCH: failed to create file:%s", fqfn.c_str());
|
|
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Failed to create local temporary file");
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
esp_err_t WiFi::reqRP2350FWUpdate(char *errMsg,
|
|
std::ifstream &inFile,
|
|
uint32_t fileSize,
|
|
uint8_t instance,
|
|
uint8_t clearConfig,
|
|
uint8_t clearFlashHdr,
|
|
char *license,
|
|
char *author,
|
|
char *description,
|
|
char *version,
|
|
char *versionDate,
|
|
char *copyright,
|
|
uint32_t fwChkSum)
|
|
{
|
|
// Locals.
|
|
bool retVal = false;
|
|
bool resetMCU = true;
|
|
uint64_t retryTimer = 0;
|
|
uint64_t expireTimer = 1;
|
|
int bufIdx;
|
|
int retriesCnt = 0;
|
|
int resp;
|
|
int authMode = 1;
|
|
int filePos = 0;
|
|
uint8_t pageBuffer[WIFI_RP2350_PAGE_SIZE + 10];
|
|
uint8_t lastFrame = 0;
|
|
uint32_t rp2350Addr = WIFI_RP2350_FLASH_LOAD_ADDR;
|
|
esp_err_t result = ESP_OK;
|
|
|
|
// Basic protocol:
|
|
// AUTH MODE:
|
|
// Reset rp2350
|
|
// Send ID and configuration block to place rp2350 into flash upload mode.
|
|
// if ID_OK received, enter FRAME mode.
|
|
// if timer expires, resend ID max times before error exit. Reset rp2350 after 1/2 number of retries has been exceeded.
|
|
// FRAME MODE:
|
|
// if End of File, exit.
|
|
// Send Flash Frame (flash ram page with data and checksum). Start expiry timer to allow receipt of data and chksum calc.
|
|
// if FRAME_OK received Clear timer, increment frame counter, set flash expiry timer, wait for FLASH.
|
|
// if FRAME_CHKERR received or timer expires, clear timer, resend Flash Frame max times before error exit. Reset rp2350 after 1/2 number of retries has been exceeded.
|
|
// if FLASH_OK received, clear timer, send next frame.
|
|
// if FLASH_NOK received, error exit (user to make manual usb flash required).
|
|
|
|
do
|
|
{
|
|
if (resetMCU)
|
|
{
|
|
resetRP2350(true, false);
|
|
rp2350Addr = WIFI_RP2350_FLASH_LOAD_ADDR; // Default address, not used if instance is valid.
|
|
filePos = 0;
|
|
retryTimer = 0;
|
|
authMode = 1;
|
|
resetMCU = false;
|
|
expireTimer = esp_timer_get_time() + WIFI_RP2350_AUTH_TIMEOUT;
|
|
vTaskDelay(500);
|
|
|
|
// gpio_set_level((gpio_num_t)CONFIG_BOOT, 1);
|
|
// gpio_set_direction((gpio_num_t)CONFIG_BOOT, GPIO_MODE_INPUT);
|
|
}
|
|
|
|
if (authMode == 1)
|
|
{
|
|
if (retryTimer <= esp_timer_get_time())
|
|
{
|
|
// Halfway through number of retry counts, reset the RP2350 in case it is locked up.
|
|
if (++retriesCnt % (WIFI_RP2350_MAX_RETRIES / 2) == 0)
|
|
{
|
|
resetMCU = true;
|
|
}
|
|
else
|
|
{
|
|
bufIdx = 0;
|
|
memset(&pageBuffer[bufIdx], 0, sizeof(pageBuffer));
|
|
if (clearFlashHdr)
|
|
{
|
|
pageBuffer[bufIdx++] = (uint8_t) (WIFI_RP2350_HDRCLEAR_ID >> 24);
|
|
pageBuffer[bufIdx++] = (uint8_t) (WIFI_RP2350_HDRCLEAR_ID >> 16);
|
|
pageBuffer[bufIdx++] = (uint8_t) (WIFI_RP2350_HDRCLEAR_ID >> 8);
|
|
pageBuffer[bufIdx++] = (uint8_t) (WIFI_RP2350_HDRCLEAR_ID);
|
|
}
|
|
if (clearConfig)
|
|
{
|
|
pageBuffer[bufIdx++] = (uint8_t) (WIFI_RP2350_CFGCLEAR_ID >> 24);
|
|
pageBuffer[bufIdx++] = (uint8_t) (WIFI_RP2350_CFGCLEAR_ID >> 16);
|
|
pageBuffer[bufIdx++] = (uint8_t) (WIFI_RP2350_CFGCLEAR_ID >> 8);
|
|
pageBuffer[bufIdx++] = (uint8_t) (WIFI_RP2350_CFGCLEAR_ID);
|
|
}
|
|
pageBuffer[bufIdx++] = (uint8_t) (WIFI_RP2350_UPDATE_ID >> 24);
|
|
pageBuffer[bufIdx++] = (uint8_t) (WIFI_RP2350_UPDATE_ID >> 16);
|
|
pageBuffer[bufIdx++] = (uint8_t) (WIFI_RP2350_UPDATE_ID >> 8);
|
|
pageBuffer[bufIdx++] = (uint8_t) (WIFI_RP2350_UPDATE_ID);
|
|
pageBuffer[bufIdx++] = (uint8_t) (rp2350Addr >> 24);
|
|
pageBuffer[bufIdx++] = (uint8_t) (rp2350Addr >> 16);
|
|
pageBuffer[bufIdx++] = (uint8_t) (rp2350Addr >> 8);
|
|
pageBuffer[bufIdx++] = (uint8_t) (rp2350Addr);
|
|
pageBuffer[bufIdx++] = (uint8_t) (fileSize >> 24);
|
|
pageBuffer[bufIdx++] = (uint8_t) (fileSize >> 16);
|
|
pageBuffer[bufIdx++] = (uint8_t) (fileSize >> 8);
|
|
pageBuffer[bufIdx++] = (uint8_t) (fileSize);
|
|
pageBuffer[bufIdx++] = (uint8_t) (0);
|
|
pageBuffer[bufIdx++] = (uint8_t) (0);
|
|
pageBuffer[bufIdx++] = (uint8_t) (0);
|
|
pageBuffer[bufIdx++] = (uint8_t) (instance);
|
|
ESP_LOGI(WIFITAG, "FW_AUTH: instance=%d addr=%08lx size=%08lx chksum=%08lx",
|
|
instance, rp2350Addr, fileSize, fwChkSum);
|
|
pageBuffer[bufIdx++] = (uint8_t) (fwChkSum >> 24);
|
|
pageBuffer[bufIdx++] = (uint8_t) (fwChkSum >> 16);
|
|
pageBuffer[bufIdx++] = (uint8_t) (fwChkSum >> 8);
|
|
pageBuffer[bufIdx++] = (uint8_t) (fwChkSum);
|
|
if (license)
|
|
{
|
|
strcpy((char *) &pageBuffer[bufIdx], license);
|
|
bufIdx += strlen(license) + 1;
|
|
}
|
|
else
|
|
{
|
|
bufIdx++;
|
|
}
|
|
if (author)
|
|
{
|
|
strcpy((char *) &pageBuffer[bufIdx], author);
|
|
bufIdx += strlen(author) + 1;
|
|
}
|
|
else
|
|
{
|
|
bufIdx++;
|
|
}
|
|
if (description)
|
|
{
|
|
strcpy((char *) &pageBuffer[bufIdx], description);
|
|
bufIdx += strlen(description) + 1;
|
|
}
|
|
else
|
|
{
|
|
bufIdx++;
|
|
}
|
|
if (version)
|
|
{
|
|
strcpy((char *) &pageBuffer[bufIdx], version);
|
|
bufIdx += strlen(version) + 1;
|
|
}
|
|
else
|
|
{
|
|
bufIdx++;
|
|
}
|
|
if (versionDate)
|
|
{
|
|
strcpy((char *) &pageBuffer[bufIdx], versionDate);
|
|
bufIdx += strlen(versionDate) + 1;
|
|
}
|
|
else
|
|
{
|
|
bufIdx++;
|
|
}
|
|
if (copyright)
|
|
{
|
|
strcpy((char *) &pageBuffer[bufIdx], copyright);
|
|
bufIdx += strlen(copyright) + 1;
|
|
}
|
|
else
|
|
{
|
|
bufIdx++;
|
|
}
|
|
|
|
// Need to place the checksum into the packet but ignore the magic IDs prepended before the FW UPDATE ID.
|
|
uint8_t chkSum = 0;
|
|
int chkSumPos = 4 + (clearFlashHdr ? 4 : 0) + (clearConfig ? 4 : 0);
|
|
for (int idx = chkSumPos; idx < WIFI_RP2350_PAGE_SIZE + chkSumPos; idx++)
|
|
{
|
|
chkSum += pageBuffer[idx];
|
|
}
|
|
pageBuffer[WIFI_RP2350_PAGE_SIZE + chkSumPos] = chkSum;
|
|
IO_rp2350WriteString(pageBuffer, WIFI_RP2350_PAGE_SIZE + chkSumPos + 2);
|
|
retryTimer = esp_timer_get_time() + WIFI_RP2350_RETRYAUTH_TIMEOUT;
|
|
}
|
|
}
|
|
|
|
resp = IO_rp2350GetChar(false);
|
|
if (resp != EOF)
|
|
{
|
|
switch ((uint8_t) resp)
|
|
{
|
|
case WIFI_RP2350_RESP_ID_OK:
|
|
authMode = 0;
|
|
retryTimer = esp_timer_get_time();
|
|
expireTimer = esp_timer_get_time() + WIFI_RP2350_FRAME_TIMEOUT;
|
|
resp = 0;
|
|
ESP_LOGI(WIFITAG, "Firmware Header Sent and acknowledged");
|
|
break;
|
|
case WIFI_RP2350_RESP_FRAME_CHKERR:
|
|
retryTimer = 0;
|
|
ESP_LOGE(WIFITAG, "Firmware Header checksum error");
|
|
break;
|
|
default:
|
|
ESP_LOGE(WIFITAG, "Firmware Header response not recognised:%04x", resp);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (retryTimer <= esp_timer_get_time())
|
|
{
|
|
// If where half way through the number of retries, something is wrong so perform a restart
|
|
// and resend the header.
|
|
if (retryTimer > 1 && ++retriesCnt % (WIFI_RP2350_MAX_RETRIES / 2) == 0)
|
|
{
|
|
resetMCU = true;
|
|
}
|
|
else
|
|
{
|
|
inFile.seekg(filePos, inFile.beg);
|
|
uint8_t chkSum = 0;
|
|
for (int frameIdx = 0; frameIdx < WIFI_RP2350_PAGE_SIZE; frameIdx++)
|
|
{
|
|
pageBuffer[frameIdx] = 0;
|
|
if (inFile.peek() != EOF)
|
|
{
|
|
inFile.read((char *) &pageBuffer[frameIdx], 1);
|
|
}
|
|
chkSum += pageBuffer[frameIdx];
|
|
}
|
|
pageBuffer[WIFI_RP2350_PAGE_SIZE] = chkSum;
|
|
lastFrame = inFile.peek() == EOF ? 1 : 0;
|
|
pageBuffer[WIFI_RP2350_PAGE_SIZE + 1] = lastFrame;
|
|
IO_rp2350WriteString(pageBuffer, WIFI_RP2350_PAGE_SIZE + 2);
|
|
retryTimer = esp_timer_get_time() + WIFI_RP2350_RETRY_TIMEOUT;
|
|
expireTimer = esp_timer_get_time() + WIFI_RP2350_FRAME_TIMEOUT;
|
|
}
|
|
}
|
|
|
|
resp = IO_rp2350GetChar(false);
|
|
if (resp != EOF)
|
|
{
|
|
switch ((uint8_t) resp)
|
|
{
|
|
case WIFI_RP2350_RESP_FRAME_OK:
|
|
retryTimer = esp_timer_get_time() + WIFI_RP2350_FLASH_TIMEOUT;
|
|
expireTimer = esp_timer_get_time() + WIFI_RP2350_FLASH_TIMEOUT;
|
|
break;
|
|
case WIFI_RP2350_RESP_FLASH_OK:
|
|
rp2350Addr += WIFI_RP2350_PAGE_SIZE;
|
|
retriesCnt = 0;
|
|
if (lastFrame)
|
|
{
|
|
expireTimer = 0;
|
|
retVal = true;
|
|
ESP_LOGI(WIFITAG, "Flash OK - (Last Frame 0x%0lx, 0x%08x)", rp2350Addr, filePos);
|
|
}
|
|
else
|
|
{
|
|
filePos = inFile.tellg();
|
|
expireTimer = esp_timer_get_time() + WIFI_RP2350_FRAME_TIMEOUT;
|
|
retryTimer = 1;
|
|
ESP_LOGI(WIFITAG, "Flash OK (0x%0lx, 0x%08x)", rp2350Addr, filePos);
|
|
}
|
|
break;
|
|
case WIFI_RP2350_RESP_FW_CHKERR:
|
|
sprintf(&errMsg[strlen(errMsg)], "RP2350 Flash checksum mismatch, flash operation failed.\n");
|
|
ESP_LOGE(WIFITAG, "Firmware Checksum Error");
|
|
expireTimer = 0;
|
|
break;
|
|
case WIFI_RP2350_RESP_FW_OK:
|
|
ESP_LOGI(WIFITAG, "FirmWare OK");
|
|
expireTimer = 0;
|
|
retVal = true;
|
|
break;
|
|
case WIFI_RP2350_RESP_FLASH_NOK:
|
|
sprintf(&errMsg[strlen(errMsg)], "RP2350 Flash failed in frame address:0x%0lx.\n", rp2350Addr);
|
|
ESP_LOGE(WIFITAG, "Flash NOT OK, address:0x%0lx.", rp2350Addr);
|
|
expireTimer = 0;
|
|
break;
|
|
case WIFI_RP2350_RESP_FRAME_CHKERR:
|
|
sprintf(&errMsg[strlen(errMsg)], "Frame retry:%d, address:0x%0lx.\n", retriesCnt, rp2350Addr);
|
|
ESP_LOGE(WIFITAG, "Frame Checksum Error, retry:%d, address:0x%0lx.", retriesCnt, rp2350Addr);
|
|
retryTimer = 1;
|
|
expireTimer = esp_timer_get_time() + WIFI_RP2350_FRAME_TIMEOUT;
|
|
break;
|
|
case WIFI_RP2350_RESP_FRAME_ADDRERR:
|
|
sprintf(&errMsg[strlen(errMsg)], "RP2350 illegal frame address:0x%0lx.\n", rp2350Addr);
|
|
ESP_LOGE(WIFITAG, "Frame Address Error:0x%0lx.", rp2350Addr);
|
|
expireTimer = 0;
|
|
break;
|
|
default:
|
|
sprintf(&errMsg[strlen(errMsg)], "RP2350 unrecognised response (%x), flash failed.\n", resp);
|
|
expireTimer = 0;
|
|
ESP_LOGE(WIFITAG, "Unrecognised Response(%04x)", resp);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
} while (expireTimer > 0 && expireTimer > esp_timer_get_time() && retriesCnt < WIFI_RP2350_MAX_RETRIES && !retVal);
|
|
|
|
// Disable the upload signal.
|
|
resetRP2350(false, true);
|
|
|
|
if (retriesCnt >= WIFI_RP2350_MAX_RETRIES)
|
|
{
|
|
if (authMode == 1)
|
|
{
|
|
sprintf(&errMsg[strlen(errMsg)], "RP2350 failed to enter programming mode.\n");
|
|
}
|
|
else
|
|
{
|
|
sprintf(&errMsg[strlen(errMsg)], "Exceeded retries cnt at addr:%0lx.\n", rp2350Addr);
|
|
}
|
|
}
|
|
|
|
result = retVal ? ESP_OK : ESP_FAIL;
|
|
return result;
|
|
}
|
|
|
|
// A method, activated on a client side POST using AJAX file upload, to accept incoming data and write it into the RP2350 MPU flash ram.
|
|
esp_err_t WiFi::otaRP2350FirmwareUpdatePOSTHandler(httpd_req_t *req)
|
|
{
|
|
// Locals.
|
|
std::ifstream inFile;
|
|
bool result = false;
|
|
esp_err_t ret = ESP_OK;
|
|
uint8_t instance;
|
|
uint8_t clearConfig = 0;
|
|
uint8_t clearFlashHdr = 0;
|
|
std::string uri;
|
|
std::string url;
|
|
std::string urq;
|
|
std::vector<t_kvPair> pairs;
|
|
uint32_t fileSize;
|
|
uint32_t chkSum = 0;
|
|
char *errMsg = nullptr;
|
|
char *fileWindow = nullptr;
|
|
char *license = nullptr;
|
|
char *author = nullptr;
|
|
char *description = nullptr;
|
|
char *version = nullptr;
|
|
char *versionDate = nullptr;
|
|
char *copyright = nullptr;
|
|
|
|
// Retrieve pointer to object in order to access data.
|
|
WiFi *pThis = (WiFi *) req->user_ctx;
|
|
|
|
// Split the URI into components for ease of processing.
|
|
pThis->splitURI(req, uri, url, urq, pairs);
|
|
|
|
// Loop through all the URI key pairs, updating configuration values as necessary.
|
|
instance = 1;
|
|
for (auto pair : pairs)
|
|
{
|
|
if (pair.name == "partition")
|
|
{
|
|
instance = (uint8_t) atoi(pair.value.c_str());
|
|
ESP_LOGI(WIFITAG, "Instance %d", instance);
|
|
if (instance < FLASH_APP_INSTANCE1 || instance > FLASH_APP_INSTANCE2)
|
|
instance = FLASH_APP_INSTANCE1;
|
|
}
|
|
if (pair.name == "clearcfg")
|
|
{
|
|
clearConfig = atoi(pair.value.c_str());
|
|
ESP_LOGI(WIFITAG, "Clear Config %d", clearConfig);
|
|
if (clearConfig == 1)
|
|
clearConfig = 1;
|
|
}
|
|
if (pair.name == "clearhdr")
|
|
{
|
|
clearFlashHdr = atoi(pair.value.c_str());
|
|
ESP_LOGI(WIFITAG, "Clear Flash Header %d", clearFlashHdr);
|
|
}
|
|
}
|
|
|
|
// Build the FQFN of the binary firmware file to send to the RP2350.
|
|
std::string fqfnBIN = pThis->wifiCtrl.run.fsPath;
|
|
fqfnBIN.append(WIFI_TEMP_DIR).append("/").append(WIFI_RP2350_FW_FILENAME);
|
|
|
|
ESP_LOGI(WIFITAG, "Firmware update request (%s, %d, cfg=%d, hdr=%d)", fqfnBIN.c_str(), instance, clearConfig, clearFlashHdr);
|
|
|
|
errMsg = (char *) malloc(4096);
|
|
if (errMsg)
|
|
{
|
|
*(errMsg) = 0x00;
|
|
|
|
ESP_LOGI(WIFITAG, "FW_UPD: fetching file from browser...");
|
|
if (pThis->otaFetchFile(req, WIFI_RP2350_FW_FILENAME) != ESP_OK)
|
|
{
|
|
sprintf((errMsg + strlen(errMsg)), "Failed to fetch firmware file => %s\n", fqfnBIN.c_str());
|
|
}
|
|
else
|
|
{
|
|
ESP_LOGI(WIFITAG, "FW_UPD: file saved, opening binary...");
|
|
inFile.open(fqfnBIN.c_str(), std::ios::binary);
|
|
inFile.seekg(0, inFile.end);
|
|
fileSize = inFile.tellg();
|
|
inFile.seekg(0, inFile.beg);
|
|
if (inFile.is_open() && fileSize <= WIFI_RP2350_FLASH_SIZE)
|
|
{
|
|
inFile.seekg(WIFI_RP2350_BIN_CHECK_ADDR, inFile.beg);
|
|
uint32_t binSignature;
|
|
if (!inFile.read((char *) &binSignature, 4) || binSignature != WIFI_RP2350_BIN_CHECK_SIGNATURE)
|
|
{
|
|
sprintf((errMsg + strlen(errMsg)), "File is not a valid rp2350 binary (signature=%0lx).\n", binSignature);
|
|
}
|
|
else
|
|
{
|
|
fileWindow = (char *) malloc(256);
|
|
if (fileWindow)
|
|
{
|
|
// Create a sliding window to locate header within the binary and extract key information.
|
|
inFile.seekg(0, inFile.beg);
|
|
while (inFile.read((fileWindow + 255), 1))
|
|
{
|
|
chkSum += *(fileWindow + 255);
|
|
if (strncmp(fileWindow, "ROMIDS=", 7) == 0)
|
|
{
|
|
int stridx = 8;
|
|
license = strdup((fileWindow + stridx));
|
|
stridx += strlen(license) + 1;
|
|
author = strdup((fileWindow + stridx));
|
|
stridx += strlen(author) + 1;
|
|
description = strdup((fileWindow + stridx));
|
|
stridx += strlen(description) + 1;
|
|
version = strdup((fileWindow + stridx));
|
|
stridx += strlen(version) + 1;
|
|
versionDate = strdup((fileWindow + stridx));
|
|
stridx += strlen(versionDate) + 1;
|
|
copyright = strdup((fileWindow + stridx));
|
|
}
|
|
memcpy(fileWindow, (fileWindow + 1), 255);
|
|
if (inFile.peek() == EOF)
|
|
{
|
|
inFile.seekg(0, inFile.beg);
|
|
break;
|
|
}
|
|
}
|
|
if (pThis->reqRP2350FWUpdate(errMsg + strlen(errMsg),
|
|
inFile,
|
|
fileSize,
|
|
(instance == 1 ? FLASH_APP_INSTANCE1 : FLASH_APP_INSTANCE2),
|
|
clearConfig,
|
|
clearFlashHdr,
|
|
license,
|
|
author,
|
|
description,
|
|
version,
|
|
versionDate,
|
|
copyright,
|
|
chkSum) == ESP_OK)
|
|
{
|
|
result = true;
|
|
}
|
|
else
|
|
{
|
|
sprintf((errMsg + strlen(errMsg)), "Failed to update rp2350 firmware.\n");
|
|
}
|
|
|
|
if (fileWindow)
|
|
free(fileWindow);
|
|
if (license)
|
|
free(license);
|
|
if (author)
|
|
free(author);
|
|
if (description)
|
|
free(description);
|
|
if (version)
|
|
free(version);
|
|
if (versionDate)
|
|
free(versionDate);
|
|
if (copyright)
|
|
free(copyright);
|
|
}
|
|
else
|
|
{
|
|
sprintf((errMsg + strlen(errMsg)), "Memory exhaustion, cannot update firmware.\n");
|
|
}
|
|
}
|
|
inFile.close();
|
|
}
|
|
else if (inFile.is_open() && fileSize > WIFI_RP2350_FLASH_SIZE)
|
|
{
|
|
sprintf((errMsg + strlen(errMsg)), "Firmware file:%s is too large (%lx bytes) .\n", fqfnBIN.c_str(), fileSize);
|
|
inFile.close();
|
|
}
|
|
else
|
|
{
|
|
sprintf((errMsg + strlen(errMsg)), "Failed to open local firmware file => %s\n", fqfnBIN.c_str());
|
|
}
|
|
}
|
|
|
|
if (result)
|
|
{
|
|
vTaskDelay(500);
|
|
httpd_resp_set_status(req, "200 OK");
|
|
httpd_resp_sendstr(req, "");
|
|
}
|
|
else
|
|
{
|
|
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, errMsg);
|
|
}
|
|
free(errMsg);
|
|
}
|
|
else
|
|
{
|
|
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Memory exhaustion, cannot flash firmware, please reboot!");
|
|
}
|
|
|
|
ret = result ? ESP_OK : ESP_FAIL;
|
|
return ret;
|
|
}
|
|
|
|
// Method to update the SD filesystem OTA.
|
|
esp_err_t WiFi::otaFilepackUpdatePOSTHandler(httpd_req_t *req)
|
|
{
|
|
// Locals.
|
|
esp_err_t result = ESP_OK;
|
|
std::string gzFile;
|
|
std::string tarFile;
|
|
std::string tmpDir;
|
|
struct stat dirhdl;
|
|
|
|
// Retrieve pointer to object in order to access data.
|
|
WiFi *pThis = (WiFi *) req->user_ctx;
|
|
|
|
// Build directory where file will be stored and unpacked.
|
|
tmpDir = pThis->wifiCtrl.run.fsPath;
|
|
tmpDir += "/";
|
|
tmpDir += WIFI_TEMP_DIR;
|
|
|
|
// Build file names.
|
|
gzFile = WIFI_FILEPACK_FILENAME;
|
|
gzFile.append(".gz");
|
|
tarFile = WIFI_FILEPACK_FILENAME;
|
|
tarFile.append(".tar");
|
|
|
|
// Previous web filesystem dir.
|
|
std::string oldDir = pThis->wifiCtrl.run.basePath;
|
|
oldDir.append(".").append(to_str(pThis->getVersionNumber("WebFS"), 2, 10));
|
|
|
|
// Check to see if the previous directory exists, shouldnt be the case but maybe an old version was loaded then the user will need to use the File Manager to remove.
|
|
// The check is to prevent the same version being reloaded.
|
|
if (stat(oldDir.c_str(), &dirhdl) == 0)
|
|
{
|
|
result = ESP_FAIL;
|
|
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Filepack already installed, aborting.");
|
|
ESP_LOGI(WIFITAG, "Backup directory already exists, filepack previously installed %s -> %s", pThis->wifiCtrl.run.basePath, oldDir.c_str());
|
|
}
|
|
else
|
|
{
|
|
result = pThis->otaFetchFile(req, gzFile, WIFI_TEMP_DIR);
|
|
if (result == ESP_OK)
|
|
{
|
|
result = pThis->unpackFile(tmpDir, gzFile, tmpDir, tarFile);
|
|
if (result != ESP_OK)
|
|
{
|
|
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Failed to unpack gzip file.");
|
|
}
|
|
else
|
|
{
|
|
if (rename(pThis->wifiCtrl.run.basePath, oldDir.c_str()) != 0)
|
|
{
|
|
result = ESP_FAIL;
|
|
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Failed to rename old WebFS directory, SD card issue.");
|
|
ESP_LOGI(WIFITAG, "Failed to rename directory %s -> %s", pThis->wifiCtrl.run.basePath, oldDir.c_str());
|
|
}
|
|
else
|
|
{
|
|
result = pThis->unpackFile(tmpDir, tarFile, pThis->wifiCtrl.run.fsPath, "");
|
|
if (result != ESP_OK)
|
|
{
|
|
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Failed to untar FilePack into directory, SD card issue.");
|
|
ESP_LOGI(WIFITAG, "Failed to untar FilePack %s onto SD Card root %s.", tarFile.c_str(), pThis->wifiCtrl.run.fsPath);
|
|
if (rename(oldDir.c_str(), pThis->wifiCtrl.run.basePath) == 0)
|
|
{
|
|
ESP_LOGI(WIFITAG, "Reversed rename of directory %s -> %s", oldDir.c_str(), pThis->wifiCtrl.run.basePath);
|
|
}
|
|
else
|
|
{
|
|
ESP_LOGI(WIFITAG, "Failed to rename directory %s -> %s", oldDir.c_str(), pThis->wifiCtrl.run.basePath);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if (result == ESP_OK)
|
|
{
|
|
vTaskDelay(500);
|
|
httpd_resp_set_status(req, "200 OK");
|
|
httpd_resp_sendstr(req, "");
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
// 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;
|
|
esp_err_t result = ESP_OK;
|
|
|
|
// Loop retrieving the POST in chunks and assemble into a string.
|
|
while (rcvBytes > 0 && result == ESP_OK)
|
|
{
|
|
ret = httpd_req_recv(req, buf, MIN(rcvBytes, sizeof(buf) - 1));
|
|
if (ret <= 0)
|
|
{
|
|
if (ret == HTTPD_SOCK_ERR_TIMEOUT)
|
|
{
|
|
continue;
|
|
}
|
|
result = ESP_FAIL;
|
|
int sockFd = httpd_req_to_sockfd(req);
|
|
if (sockFd != -1)
|
|
close(sockFd);
|
|
break;
|
|
}
|
|
else
|
|
{
|
|
buf[ret] = '\0';
|
|
post += buf;
|
|
rcvBytes -= ret;
|
|
}
|
|
}
|
|
|
|
if (result == ESP_OK)
|
|
{
|
|
std::vector<std::string> keys = this->split(post, "&");
|
|
for (auto key : keys)
|
|
{
|
|
size_t pos = key.find('=');
|
|
if (pos != std::string::npos)
|
|
{
|
|
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 result;
|
|
}
|
|
|
|
// 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, const std::vector<t_kvPair> &pairs, std::string &resp)
|
|
{
|
|
// Locals.
|
|
bool dataError = false;
|
|
esp_err_t result = ESP_OK;
|
|
|
|
// 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)
|
|
{
|
|
if (pair.name == "wifiMode")
|
|
{
|
|
if (pair.value == "ap")
|
|
{
|
|
pThis->wifiConfig.params.wifiMode = WIFI_CONFIG_AP;
|
|
dataError = false;
|
|
}
|
|
else
|
|
{
|
|
pThis->wifiConfig.params.wifiMode = WIFI_CONFIG_CLIENT;
|
|
dataError = false;
|
|
}
|
|
}
|
|
else if (pair.name == "clientSSID")
|
|
{
|
|
strncpy(pThis->wifiConfig.clientParams.ssid, pair.value.c_str(), MAX_WIFI_SSID_LEN);
|
|
}
|
|
else if (pair.name == "apSSID")
|
|
{
|
|
strncpy(pThis->wifiConfig.apParams.ssid, pair.value.c_str(), MAX_WIFI_SSID_LEN);
|
|
}
|
|
else if (pair.name == "clientPWD")
|
|
{
|
|
strncpy(pThis->wifiConfig.clientParams.pwd, pair.value.c_str(), MAX_WIFI_PWD_LEN);
|
|
}
|
|
else if (pair.name == "apPWD")
|
|
{
|
|
strncpy(pThis->wifiConfig.apParams.pwd, pair.value.c_str(), MAX_WIFI_PWD_LEN);
|
|
}
|
|
else if (pair.name == "dhcpMode")
|
|
{
|
|
pThis->wifiConfig.clientParams.useDHCP = (pair.value == "on");
|
|
}
|
|
else if (pair.name == "clientIP")
|
|
{
|
|
strncpy(pThis->wifiConfig.clientParams.ip, pair.value.c_str(), MAX_WIFI_IP_LEN);
|
|
}
|
|
else if (pair.name == "apIP")
|
|
{
|
|
strncpy(pThis->wifiConfig.apParams.ip, pair.value.c_str(), MAX_WIFI_IP_LEN);
|
|
}
|
|
else if (pair.name == "clientNETMASK")
|
|
{
|
|
strncpy(pThis->wifiConfig.clientParams.netmask, pair.value.c_str(), MAX_WIFI_NETMASK_LEN);
|
|
}
|
|
else if (pair.name == "apNETMASK")
|
|
{
|
|
strncpy(pThis->wifiConfig.apParams.netmask, pair.value.c_str(), MAX_WIFI_NETMASK_LEN);
|
|
}
|
|
else if (pair.name == "clientGATEWAY")
|
|
{
|
|
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);
|
|
}
|
|
}
|
|
else if (pair.name == "apGATEWAY")
|
|
{
|
|
strncpy(pThis->wifiConfig.apParams.gateway, pThis->wifiConfig.apParams.ip, MAX_WIFI_GATEWAY_LEN + 1);
|
|
}
|
|
else if (pair.name == "txPower")
|
|
{
|
|
int val = atoi(pair.value.c_str());
|
|
if (val >= 8 && val <= 84)
|
|
pThis->wifiConfig.params.txPower = (int8_t) val;
|
|
else
|
|
pThis->wifiConfig.params.txPower = 0; // 0 = use default
|
|
}
|
|
}
|
|
|
|
// Validate the data if no error was raised for individual fields.
|
|
if (!dataError)
|
|
{
|
|
if (pThis->wifiConfig.params.wifiMode == WIFI_CONFIG_AP)
|
|
{
|
|
if (strlen(pThis->wifiConfig.apParams.ssid) == 0)
|
|
{
|
|
resp += (resp.size() > 0 ? "," : "") + std::string("SSID not given!");
|
|
dataError = true;
|
|
}
|
|
else if (strlen(pThis->wifiConfig.apParams.pwd) == 0)
|
|
{
|
|
resp += (resp.size() > 0 ? "," : "") + std::string("Password not given!");
|
|
dataError = true;
|
|
}
|
|
else if (!pThis->validateIP(pThis->wifiConfig.apParams.ip))
|
|
{
|
|
resp.append(resp.size() > 0 ? "," : "");
|
|
resp.append("Illegal AP IP address(").append(pThis->wifiConfig.apParams.ip).append(")");
|
|
dataError = true;
|
|
}
|
|
else if (!pThis->validateIP(pThis->wifiConfig.apParams.netmask))
|
|
{
|
|
resp.append(resp.size() > 0 ? "," : "");
|
|
resp.append("Illegal AP Netmask address(").append(pThis->wifiConfig.apParams.netmask).append(")");
|
|
dataError = true;
|
|
}
|
|
else if (strlen(pThis->wifiConfig.apParams.gateway) == 0 || !pThis->validateIP(pThis->wifiConfig.apParams.gateway))
|
|
{
|
|
resp.append(resp.size() > 0 ? "," : "");
|
|
resp.append("Illegal AP Gateway address(").append(pThis->wifiConfig.apParams.gateway).append(")");
|
|
dataError = true;
|
|
}
|
|
}
|
|
else if (pThis->wifiConfig.params.wifiMode == WIFI_CONFIG_CLIENT)
|
|
{
|
|
if (!pThis->wifiConfig.clientParams.useDHCP)
|
|
{
|
|
if (strlen(pThis->wifiConfig.clientParams.ssid) == 0)
|
|
{
|
|
resp += (resp.size() > 0 ? "," : "") + std::string("SSID not given!");
|
|
dataError = true;
|
|
}
|
|
else if (strlen(pThis->wifiConfig.clientParams.pwd) == 0)
|
|
{
|
|
resp += (resp.size() > 0 ? "," : "") + std::string("Password not given!");
|
|
dataError = true;
|
|
}
|
|
else if (!pThis->validateIP(pThis->wifiConfig.clientParams.ip))
|
|
{
|
|
resp.append(resp.size() > 0 ? "," : "");
|
|
resp.append("Illegal IP address(").append(pThis->wifiConfig.clientParams.ip).append(")");
|
|
dataError = true;
|
|
}
|
|
else if (!pThis->validateIP(pThis->wifiConfig.clientParams.netmask))
|
|
{
|
|
resp.append(resp.size() > 0 ? "," : "");
|
|
resp.append("Illegal Netmask address(").append(pThis->wifiConfig.clientParams.netmask).append(")");
|
|
dataError = true;
|
|
}
|
|
else if (strlen(pThis->wifiConfig.clientParams.gateway) > 0 && !pThis->validateIP(pThis->wifiConfig.clientParams.gateway))
|
|
{
|
|
resp.append(resp.size() > 0 ? "," : "");
|
|
resp.append("Illegal Gateway address(").append(pThis->wifiConfig.clientParams.gateway).append(")");
|
|
dataError = true;
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
resp.append(resp.size() > 0 ? "," : "");
|
|
resp.append("Unknown WiFi Mode (")
|
|
.append(to_str(pThis->wifiConfig.params.wifiMode, 0, 10))
|
|
.append("), internal coding error, please contact support.");
|
|
dataError = true;
|
|
}
|
|
}
|
|
|
|
// No errors, save wifi configuration.
|
|
if (!dataError)
|
|
{
|
|
pThis->wifiConfig.clientParams.valid = true;
|
|
if (!pThis->nvs->persistData(pThis->wifiCtrl.run.thisClass.c_str(), &pThis->wifiConfig, sizeof(t_wifiConfig)))
|
|
{
|
|
ESP_LOGI(WIFITAG,
|
|
"Persisting tzpuPico (%s) configuration data failed, updates will not persist in future power cycles.",
|
|
pThis->wifiCtrl.run.thisClass.c_str());
|
|
}
|
|
else if (!pThis->nvs->commitData())
|
|
{
|
|
ESP_LOGI(WIFITAG, "NVS Commit writes operation failed, some previous writes may not persist in future power cycles.");
|
|
}
|
|
}
|
|
|
|
result = dataError ? ESP_FAIL : ESP_OK;
|
|
return result;
|
|
}
|
|
|
|
// /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 result = ESP_OK;
|
|
std::string sdpath;
|
|
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.
|
|
result = pThis->getPathFromURI(uriStr, "/data/", req->uri);
|
|
if (result == ESP_FAIL)
|
|
{
|
|
resp = "Failed to extract URI";
|
|
}
|
|
else
|
|
{
|
|
sdpath = pThis->wifiCtrl.run.fsPath;
|
|
if (sdpath.back() != '/')
|
|
{
|
|
sdpath.append("/");
|
|
}
|
|
pThis->stringReplace(sdpath, "//", "/");
|
|
|
|
if (uriStr.substr(0, 6) == "upload")
|
|
{
|
|
std::string file = uriStr.substr(7);
|
|
result = pThis->uploadFileHandler(req, file, resp);
|
|
if (result == ESP_OK)
|
|
{
|
|
resp = "File uploaded.";
|
|
}
|
|
}
|
|
else if (uriStr == "config")
|
|
{
|
|
// Receive a JSON body and write it to /config.json on the SD card.
|
|
// The body is raw JSON (not form-encoded), so we read it directly
|
|
// rather than using getPOSTData().
|
|
int contentLen = req->content_len;
|
|
if (contentLen <= 0 || contentLen > (256 * 1024))
|
|
{
|
|
resp = "Invalid or missing JSON body (size=" + std::to_string(contentLen) + ")";
|
|
result = ESP_FAIL;
|
|
}
|
|
else
|
|
{
|
|
// Read the full JSON body.
|
|
std::string jsonBody;
|
|
jsonBody.reserve(contentLen);
|
|
char rcvBuf[512];
|
|
int remaining = contentLen;
|
|
|
|
while (remaining > 0)
|
|
{
|
|
int toRead = (remaining < (int)sizeof(rcvBuf)) ? remaining : (int)sizeof(rcvBuf);
|
|
int ret = httpd_req_recv(req, rcvBuf, toRead);
|
|
if (ret <= 0)
|
|
{
|
|
if (ret == HTTPD_SOCK_ERR_TIMEOUT)
|
|
continue;
|
|
result = ESP_FAIL;
|
|
resp = "Failed to receive POST body";
|
|
break;
|
|
}
|
|
jsonBody.append(rcvBuf, ret);
|
|
remaining -= ret;
|
|
}
|
|
|
|
if (result == ESP_OK)
|
|
{
|
|
// Basic validation: ensure it looks like JSON.
|
|
// Find the first non-whitespace character.
|
|
size_t firstNonWs = jsonBody.find_first_not_of(" \t\r\n");
|
|
if (firstNonWs == std::string::npos || jsonBody[firstNonWs] != '{')
|
|
{
|
|
resp = "Invalid JSON: must start with '{'";
|
|
result = ESP_FAIL;
|
|
}
|
|
}
|
|
|
|
if (result == ESP_OK)
|
|
{
|
|
// Write to a temp file, then atomically rename with versioned backup.
|
|
std::string trgFile = sdpath + "config.json";
|
|
std::string tmpFile = trgFile + ".tmp";
|
|
|
|
FILE *wf = fopen(tmpFile.c_str(), "wb");
|
|
if (!wf)
|
|
{
|
|
resp = "Failed to create temp file: " + tmpFile;
|
|
result = ESP_FAIL;
|
|
}
|
|
else
|
|
{
|
|
size_t written = fwrite(jsonBody.c_str(), 1, jsonBody.size(), wf);
|
|
fclose(wf);
|
|
|
|
if (written != jsonBody.size())
|
|
{
|
|
std::remove(tmpFile.c_str());
|
|
resp = "Failed to write config data (wrote " + std::to_string(written) + "/" + std::to_string(jsonBody.size()) + " bytes)";
|
|
result = ESP_FAIL;
|
|
}
|
|
else
|
|
{
|
|
// Version the existing config.json and rename temp into place.
|
|
std::string errMsg;
|
|
result = pThis->versionedRename(tmpFile, trgFile, errMsg);
|
|
if (result != ESP_OK)
|
|
{
|
|
resp = "Failed to save config: " + errMsg;
|
|
}
|
|
else
|
|
{
|
|
// Notify the RP2350 to reload its configuration.
|
|
pThis->reloadRP2350Config();
|
|
resp = "Configuration saved and RP2350 reload initiated.";
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
result = pThis->getPOSTData(req, &pairs);
|
|
if (result == ESP_OK)
|
|
{
|
|
if (uriStr.substr(0, 4) == "wifi")
|
|
{
|
|
result = pThis->wifiDataPOSTHandler(req, pairs, resp);
|
|
if (result == ESP_OK)
|
|
{
|
|
pThis->wifiCtrl.run.rebootButton = true;
|
|
resp = "Data values accepted. Press 'Reboot' to initiate network connection with the new configuration.";
|
|
}
|
|
}
|
|
else if (uriStr.substr(0, 6) == "rp2350")
|
|
{
|
|
std::string partition;
|
|
for (auto pair : pairs)
|
|
{
|
|
if (pair.name == "activepartition")
|
|
{
|
|
partition = pair.value;
|
|
}
|
|
}
|
|
if (partition == "1" || partition == "2")
|
|
{
|
|
// Send command to RP2350 to select active partition and reboot.
|
|
partition.insert(0, "APRT,");
|
|
CP_queueCmd(partition.c_str());
|
|
}
|
|
else
|
|
{
|
|
resp = "Unknown or illegal partition number:" + partition;
|
|
result = ESP_FAIL;
|
|
}
|
|
}
|
|
else
|
|
// Change persona of the picoZ80.
|
|
if (uriStr.substr(0, 7) == "persona")
|
|
{
|
|
std::string partition1, partition2;
|
|
struct stat fileStat;
|
|
for (auto pair : pairs)
|
|
{
|
|
if (pair.name == "partition1")
|
|
{
|
|
partition1 = pair.value;
|
|
}
|
|
if (pair.name == "partition2")
|
|
{
|
|
partition2 = pair.value;
|
|
}
|
|
}
|
|
// Using the provided partition names, look to see if a matching config file exists.
|
|
std::string cfgFile = sdpath + "config/config_" + partition1 + "_" + partition2 + ".json";
|
|
if (stat(cfgFile.c_str(), &fileStat) == -1)
|
|
{
|
|
resp = "Config file does not exist: " + cfgFile;
|
|
result = ESP_FAIL;
|
|
}
|
|
else
|
|
{
|
|
// Copy the config file to the root directory.
|
|
std::string trgFile = sdpath + "config.json";
|
|
|
|
// Version the target if it exists, then copy source over it.
|
|
// Use a temp file so versionedRename atomically puts it in place.
|
|
if (result == ESP_OK)
|
|
{
|
|
std::string tmpFile = trgFile + ".tmp";
|
|
std::filesystem::copy(cfgFile, tmpFile);
|
|
std::string errMsg;
|
|
result = pThis->versionedRename(tmpFile, trgFile, errMsg);
|
|
if (result != ESP_OK)
|
|
resp = errMsg;
|
|
|
|
// Send command to RP2350 to reload the active partition configuration.
|
|
CP_queueCmd("CFG0");
|
|
|
|
// Send command to RP2350 to reload the config.
|
|
resp = "Copy and reload of config file:" + cfgFile + " to:" + trgFile + " successful.";
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
resp = "Unknown data directive:" + uriStr + ".";
|
|
result = ESP_FAIL;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
resp = "<p>No values in POST, check browser!</p>";
|
|
result = ESP_FAIL;
|
|
}
|
|
}
|
|
}
|
|
|
|
pThis->wifiCtrl.run.errorMsg = "<font size=\"2\" face=\"verdana\" color=\"" + std::string(result == ESP_OK ? "green" : "red") + "\">" + resp + "</font>";
|
|
if (result == ESP_OK)
|
|
{
|
|
result = httpd_resp_send_chunk(req, pThis->wifiCtrl.run.errorMsg.c_str(), pThis->wifiCtrl.run.errorMsg.size() + 1);
|
|
result = httpd_resp_send_chunk(req, NULL, 0);
|
|
}
|
|
else
|
|
{
|
|
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, pThis->wifiCtrl.run.errorMsg.c_str());
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
// Function to escape HTML special characters
|
|
std::string WiFi::escapeHTML(const std::string &input)
|
|
{
|
|
// Locals.
|
|
std::string retVal;
|
|
|
|
retVal.reserve(input.size() * 2);
|
|
for (char c : input)
|
|
{
|
|
switch (c)
|
|
{
|
|
case '<':
|
|
retVal += "<";
|
|
break;
|
|
case '>':
|
|
retVal += ">";
|
|
break;
|
|
case '&':
|
|
retVal += "&";
|
|
break;
|
|
case '"':
|
|
retVal += """;
|
|
break;
|
|
case '\'':
|
|
retVal += "'";
|
|
break;
|
|
default:
|
|
retVal += c;
|
|
break;
|
|
}
|
|
}
|
|
return (retVal);
|
|
}
|
|
|
|
// Recursive copy of a file or directory. Returns true on success.
|
|
static bool copyFileOrDir(const std::string &src, const std::string &dst)
|
|
{
|
|
struct stat st;
|
|
if (stat(src.c_str(), &st) != 0)
|
|
return false;
|
|
|
|
if (S_ISREG(st.st_mode))
|
|
{
|
|
// Copy file
|
|
FILE *in = fopen(src.c_str(), "rb");
|
|
FILE *out = in ? fopen(dst.c_str(), "wb") : NULL;
|
|
if (!in || !out)
|
|
{
|
|
if (in) fclose(in);
|
|
if (out) fclose(out);
|
|
return false;
|
|
}
|
|
char *buf = (char *) malloc(4096);
|
|
if (!buf)
|
|
{
|
|
fclose(in);
|
|
fclose(out);
|
|
return false;
|
|
}
|
|
bool ok = true;
|
|
size_t n;
|
|
while (ok && (n = fread(buf, 1, 4096, in)) > 0)
|
|
{
|
|
if (fwrite(buf, 1, n, out) != n)
|
|
ok = false;
|
|
}
|
|
fclose(in);
|
|
fclose(out);
|
|
free(buf);
|
|
if (!ok)
|
|
std::remove(dst.c_str());
|
|
return ok;
|
|
}
|
|
else if (S_ISDIR(st.st_mode))
|
|
{
|
|
// Create destination directory
|
|
if (mkdir(dst.c_str(), 0775) != 0)
|
|
return false;
|
|
|
|
// Copy contents recursively
|
|
DIR *dir = opendir(src.c_str());
|
|
if (!dir)
|
|
return false;
|
|
|
|
struct dirent *entry;
|
|
bool ok = true;
|
|
while (ok && (entry = readdir(dir)) != NULL)
|
|
{
|
|
// Skip . and ..
|
|
if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0)
|
|
continue;
|
|
std::string childSrc = src + "/" + entry->d_name;
|
|
std::string childDst = dst + "/" + entry->d_name;
|
|
ok = copyFileOrDir(childSrc, childDst);
|
|
}
|
|
closedir(dir);
|
|
return ok;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
esp_err_t WiFi::sendFileManagerDir(httpd_req_t *req)
|
|
{
|
|
ESP_LOGI(WIFITAG, "sendFileManagerDir: ENTER");
|
|
// Locals.
|
|
esp_err_t result = ESP_OK;
|
|
std::string apply;
|
|
std::string command;
|
|
std::string directory;
|
|
std::string name;
|
|
std::string oldname;
|
|
std::string sdpath;
|
|
std::string htmlStr;
|
|
std::string uri;
|
|
std::string url;
|
|
std::string urq;
|
|
char entrysize[16];
|
|
const char *entrytype;
|
|
struct dirent *entry;
|
|
struct stat entryStat;
|
|
std::vector<t_kvPair> pairs;
|
|
|
|
// Retrieve pointer to object in order to access data.
|
|
WiFi *pThis = (WiFi *) req->user_ctx;
|
|
|
|
// Split the URI into components for ease of processing.
|
|
pThis->splitURI(req, uri, url, urq, pairs);
|
|
|
|
// Loop through all the URI key pairs, updating configuration values as necessary.
|
|
directory = "/";
|
|
command = "dir";
|
|
for (auto pair : pairs)
|
|
{
|
|
if (pair.name == "dir")
|
|
{
|
|
directory = pair.value;
|
|
if (directory.length() > 1 && directory.back() == '/')
|
|
{
|
|
directory.pop_back();
|
|
}
|
|
}
|
|
else if (pair.name == "cmd")
|
|
{
|
|
command = pair.value;
|
|
}
|
|
else if (pair.name == "oldname")
|
|
{
|
|
oldname = pair.value;
|
|
}
|
|
else if (pair.name == "name")
|
|
{
|
|
name = pair.value;
|
|
}
|
|
else if (pair.name == "apply")
|
|
{
|
|
apply = pair.value;
|
|
}
|
|
}
|
|
sdpath = pThis->wifiCtrl.run.fsPath;
|
|
if (sdpath.back() != '/')
|
|
sdpath.append("/");
|
|
sdpath.append(directory);
|
|
if (sdpath.back() != '/')
|
|
sdpath.append("/");
|
|
stringReplace(sdpath, "//", "/");
|
|
|
|
ESP_LOGI(WIFITAG, "sendFileManagerDir: cmd=%s dir=%s sdpath=%s", command.c_str(), directory.c_str(), sdpath.c_str());
|
|
|
|
// Sub command processing. A directory listing of the current directory is always performed to refresh the screen, but any callbacks
|
|
// with a command will be processed first.
|
|
if (command == "ren")
|
|
{
|
|
if (!oldname.empty() && !name.empty())
|
|
{
|
|
struct stat fileStat;
|
|
oldname.insert(0, sdpath);
|
|
if (name.substr(0, 1) == "/")
|
|
name.insert(0, pThis->wifiCtrl.run.fsPath);
|
|
else
|
|
name.insert(0, sdpath);
|
|
|
|
if (stat(oldname.c_str(), &fileStat) == 0)
|
|
{
|
|
// If target exists it is versioned (a.txt -> a.txt;1 etc.) before
|
|
// oldname is moved into place — consistent with all other renames.
|
|
std::string errMsg;
|
|
if (pThis->versionedRename(oldname, name, errMsg) != ESP_OK)
|
|
{
|
|
htmlStr
|
|
.append("<div id=\"filemanager-error\" style=\"color:red;font-size:16px;padding-top:8px;padding-bottom:8px;\"><b>Rename ")
|
|
.append(oldname)
|
|
.append(" -> ")
|
|
.append(name)
|
|
.append(" failed: ")
|
|
.append(errMsg)
|
|
.append("</b></div>");
|
|
}
|
|
}
|
|
else
|
|
{
|
|
htmlStr.append("<div id=\"filemanager-error\" style=\"color:red;font-size:16px;padding-top:8px;padding-bottom:8px;\"><b>Source not found: ")
|
|
.append(oldname)
|
|
.append("</b></div>");
|
|
}
|
|
}
|
|
else
|
|
{
|
|
htmlStr.append("<div id=\"filemanager-error\" style=\"color:red;font-size:16px;padding-top:8px;padding-bottom:8px;\"><b>Rename of file ")
|
|
.append(oldname)
|
|
.append(" -> ")
|
|
.append(name)
|
|
.append(" failed, internal error.</b></div>");
|
|
}
|
|
}
|
|
else if (command == "del")
|
|
{
|
|
if (!name.empty())
|
|
{
|
|
struct stat fileStat;
|
|
name.insert(0, "/").insert(0, sdpath);
|
|
|
|
if (stat(name.c_str(), &fileStat) == 0)
|
|
{
|
|
if (S_ISREG(fileStat.st_mode))
|
|
{
|
|
if (std::remove(name.c_str()) != 0)
|
|
{
|
|
htmlStr
|
|
.append("<div id=\"filemanager-error\" style=\"color:red;font-size:16px;padding-top:8px;padding-bottom:8px;\"><b>Delete of file ")
|
|
.append(name)
|
|
.append(" failed.</b></div>");
|
|
}
|
|
}
|
|
else if (S_ISDIR(fileStat.st_mode))
|
|
{
|
|
if (!pThis->sdcard->deleteDir(name))
|
|
{
|
|
htmlStr
|
|
.append(
|
|
"<div id=\"filemanager-error\" style=\"color:red;font-size:16px;padding-top:8px;padding-bottom:8px;\"><b>Delete of directory ")
|
|
.append(name)
|
|
.append(" failed.</b></div>");
|
|
}
|
|
}
|
|
else
|
|
{
|
|
htmlStr
|
|
.append(
|
|
"<div id=\"filemanager-error\" style=\"color:red;font-size:16px;padding-top:8px;padding-bottom:8px;\"><b>File type unknown. File ")
|
|
.append(name)
|
|
.append(" delete aborted.</b></div>");
|
|
}
|
|
}
|
|
else
|
|
{
|
|
htmlStr.append("<div id=\"filemanager-error\" style=\"color:red;font-size:16px;padding-top:8px;padding-bottom:8px;\"><b>Couldnt stat file ")
|
|
.append(name)
|
|
.append(". Delete aborted.</b></div>");
|
|
}
|
|
}
|
|
else
|
|
{
|
|
htmlStr.append("<div id=\"filemanager-error\" style=\"color:red;font-size:16px;padding-top:8px;padding-bottom:8px;\"><b>Delete of file ")
|
|
.append(name)
|
|
.append(" failed, internal error.</b></div>");
|
|
}
|
|
}
|
|
else if (command == "copy")
|
|
{
|
|
if (!name.empty())
|
|
{
|
|
std::string srcPath = sdpath + "/" + name;
|
|
stringReplace(srcPath, "//", "/");
|
|
|
|
// Find next available copy name: .copy, .copy;1, .copy;2, ...
|
|
std::string dstPath = srcPath + ".copy";
|
|
struct stat st;
|
|
if (stat(dstPath.c_str(), &st) == 0)
|
|
{
|
|
for (int ver = 1; ver < 1000; ver++)
|
|
{
|
|
dstPath = srcPath + ".copy;" + std::to_string(ver);
|
|
if (stat(dstPath.c_str(), &st) != 0)
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!copyFileOrDir(srcPath, dstPath))
|
|
{
|
|
htmlStr.append("<div id=\"filemanager-error\" style=\"color:red;font-size:16px;padding-top:8px;padding-bottom:8px;\"><b>Copy of ")
|
|
.append(name).append(" failed.</b></div>");
|
|
}
|
|
}
|
|
}
|
|
else if (command == "edit")
|
|
{
|
|
if (!name.empty())
|
|
{
|
|
name.insert(0, "/").insert(0, sdpath);
|
|
htmlStr.append("<div id=\"filemanager-info\" style=\"font-size:12px;padding-top:8px;padding-bottom:8px;\"><b>").append(name).append("</b></div>");
|
|
//htmlStr.append("<div id=\"filemanager-area\" style=\"padding-left:25px;\">");
|
|
|
|
htmlStr.append("<form id=\"editForm\" onsubmit=\"saveFile(); return false;\">");
|
|
htmlStr.append("<div class=\"edit-container\">");
|
|
htmlStr.append(" <div class=\"edit-frame\">");
|
|
htmlStr.append(" <div class=\"edit-inner\">");
|
|
htmlStr.append(" <textarea class=\"form-control\" id=\"editFrameText\" name=\"content\" style=\"font-family: ui-monospace, SFMono-Regular, "
|
|
"Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', Courier, monospace; font-size: 14px; line-height: 1.4; letter-spacing: "
|
|
"0; tab-size: 4;\">");
|
|
result = httpd_resp_send_chunk(req, htmlStr.c_str(), htmlStr.size());
|
|
if (result == ESP_OK)
|
|
{
|
|
std::string line;
|
|
std::ifstream inFile;
|
|
inFile.open(name.c_str());
|
|
if (inFile.is_open())
|
|
{
|
|
while (result == ESP_OK && std::getline(inFile, line))
|
|
{
|
|
htmlStr = escapeHTML(line) + "\n";
|
|
result = httpd_resp_send_chunk(req, htmlStr.c_str(), htmlStr.size());
|
|
if (result != ESP_OK)
|
|
{
|
|
// Partial response already sent — close socket, don't attempt another response.
|
|
httpd_resp_send_chunk(req, NULL, 0);
|
|
int sockFd = httpd_req_to_sockfd(req);
|
|
if (sockFd != -1)
|
|
close(sockFd);
|
|
}
|
|
}
|
|
htmlStr = "";
|
|
if (result == ESP_OK)
|
|
{
|
|
htmlStr.append("</textarea>");
|
|
htmlStr.append(" <div class=\"edit-buttons\">");
|
|
htmlStr.append(" <input type=\"hidden\" id=\"editFile\" name=\"filepath\" value=\"").append(name).append("\">");
|
|
htmlStr.append(" <td>");
|
|
htmlStr.append(" <button type=\"submit\" class=\"wm-button\" name=\"editSave\" id=\"editSave\" value=\"\" style=\"display: "
|
|
"yes;\" >Save</button>");
|
|
htmlStr.append(" </td>");
|
|
if (apply == "yes")
|
|
{
|
|
htmlStr.append(" <td>");
|
|
htmlStr.append(" <button type=\"button\" class=\"wm-button\" name=\"editApply\" id=\"editApply\" value=\"\" "
|
|
"style=\"display: yes;\" >Apply</button>");
|
|
htmlStr.append(" </td>");
|
|
}
|
|
else
|
|
{
|
|
htmlStr.append(" <td>");
|
|
htmlStr.append(" <button type=\"button\" class=\"wm-button\" name=\"editExit\" id=\"editExit\" value=\"\" style=\"display: "
|
|
"yes;\" >Exit</button>");
|
|
htmlStr.append(" </td>");
|
|
}
|
|
htmlStr.append(" <td>");
|
|
htmlStr.append(" <button type=\"button\" class=\"wm-button\" name=\"editQuit\" id=\"editQuit\" value=\"\" style=\"display: "
|
|
"yes;\" >Quit</button>");
|
|
htmlStr.append(" </td>");
|
|
//htmlStr.append(" <td>");
|
|
//htmlStr.append(" <button type=\"button\" class=\"fa fa-download wm-button\" name=\"editDownloadHtml\" id=\"editDownloadHtml\" value=\"\" style=\"display: yes;\" > HTML</button>");
|
|
//htmlStr.append(" </td>");
|
|
htmlStr.append(" <td>");
|
|
htmlStr.append(" <button type=\"button\" class=\"fa fa-download wm-button\" name=\"editDownloadText\" id=\"editDownloadText\" "
|
|
"value=\"\" style=\"display: yes;\" > Text</button>");
|
|
htmlStr.append(" </td>");
|
|
htmlStr.append(" </div>");
|
|
htmlStr.append(" </div>");
|
|
htmlStr.append(" </div>");
|
|
htmlStr.append("</div>");
|
|
htmlStr.append("<div id=\"saveStatus\" style=\"margin-top: 10px; padding: 8px; border-radius: 4px; min-height: 20px;\"></div>");
|
|
htmlStr.append("</form>");
|
|
}
|
|
inFile.close();
|
|
}
|
|
else
|
|
{
|
|
// Partial response (textarea opening) already sent — close socket.
|
|
result = ESP_FAIL;
|
|
ESP_LOGI(WIFITAG, "Failed to open file => %s", name.c_str());
|
|
httpd_resp_send_chunk(req, NULL, 0);
|
|
int sockFd = httpd_req_to_sockfd(req);
|
|
if (sockFd != -1)
|
|
close(sockFd);
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
htmlStr.append("<div id=\"filemanager-error\" style=\"color:red;font-size:16px;padding-top:8px;padding-bottom:8px;\"><b>Edit of file ")
|
|
.append(name)
|
|
.append(" failed, internal error.</b></div>");
|
|
result = ESP_FAIL;
|
|
}
|
|
}
|
|
|
|
// Always show directory unless edit of file.
|
|
ESP_LOGI(WIFITAG, "sendFileManagerDir: generating listing, result=%d", (int)result);
|
|
if (command != "edit" && result == ESP_OK)
|
|
{
|
|
DIR *dir = opendir(sdpath.c_str());
|
|
ESP_LOGI(WIFITAG, "sendFileManagerDir: opendir(%s) = %p", sdpath.c_str(), (void*)dir);
|
|
if (!dir)
|
|
{
|
|
ESP_LOGE(WIFITAG, "Failed to stat dir : %s", sdpath.c_str());
|
|
htmlStr = "Directory does not exist.";
|
|
}
|
|
else
|
|
{
|
|
htmlStr.append("<div id=\"filemanager-info\" style=\"font-size:12px;padding-top:8px;padding-bottom:8px;\"><b>").append(sdpath).append("</b></div>");
|
|
htmlStr.append("<div id=\"filemanager-area\" style=\"padding-left:25px;\">");
|
|
htmlStr.append(" <table class=\"table table-borderless table-sm\" style=\"width:100%\">");
|
|
htmlStr.append(" <thead>");
|
|
htmlStr.append(" <tr>");
|
|
htmlStr.append(" <th style=\"width:50%;min-width:200px\"><b>Name</b></th>");
|
|
htmlStr.append(" <th style=\"width:5%\"><b>Type</b></th>");
|
|
htmlStr.append(" <th style=\"width:5%\"><b>Size (Bytes)</b></th>");
|
|
htmlStr.append(" <th style=\"text-align:left;width:10%\"><b>Action</b></th>");
|
|
htmlStr.append(" </tr>");
|
|
htmlStr.append(" </thead>");
|
|
htmlStr.append(" <tbody>");
|
|
|
|
if (directory != "/" && directory != "")
|
|
{
|
|
auto delim = directory.rfind("/");
|
|
auto upDir = url;
|
|
upDir.append("?dir=").append(directory.substr(0, delim + 1));
|
|
if (upDir.length() > 6 && upDir.back() == '/')
|
|
{
|
|
upDir.pop_back();
|
|
}
|
|
htmlStr.append("<tr>");
|
|
htmlStr.append("<td style=\"text-align:center\"><a href=\"").append(upDir).append("\">").append("[up level]").append("</a></td>");
|
|
htmlStr.append("<td></td>");
|
|
htmlStr.append("<td></td>");
|
|
htmlStr.append("<td></td>");
|
|
htmlStr.append("</tr>\n");
|
|
htmlStr.append("<tr>");
|
|
htmlStr.append("<form action=\"/data/upload\" method=\"POST\" id=\"fileupload\">");
|
|
htmlStr.append("<input type=\"hidden\" id=\"basepath\" value=\"").append(directory).append("\">");
|
|
htmlStr.append("<td><input id=\"filepath\" type=\"text\" style=\"width:100%;\"></td>");
|
|
htmlStr.append(
|
|
"<td><button class=\"wm-button-small\" style=\"margin-left:0px\" type=\"button\"><label for=\"newfile\">Select File</label></button></td>");
|
|
htmlStr.append("</form>\n");
|
|
htmlStr.append("<td><input id=\"newfile\" type=\"file\" onchange=\"setpath()\"></td>");
|
|
//htmlStr.append("<td><button id=\"upload\" class=\"wm-button-small\" type=\"button\" onclick=\"uploadFile()\">Upload</button>");
|
|
htmlStr.append("<td><button id=\"upload\" class=\"fa fa-upload wm-button-small\" aria-hidden=\"true\" title=\"Upload\" type=\"button\" "
|
|
"onclick=\"uploadFile()\"></button>");
|
|
//htmlStr.append("<button id=\"mkdir\" class=\"wm-button-small\" type=\"button\" onclick=\"mkdir()\">MkDir</button>");
|
|
htmlStr.append("<button id=\"mkdir\" class=\"fa fa-folder-o wm-button-small\" aria-hidden=\"true\" title=\"Create Directory\" type=\"button\" "
|
|
"onclick=\"mkdir()\"></button>");
|
|
htmlStr.append("</td></tr>\n");
|
|
htmlStr.append("<tr id=\"uploadProgressRow\" style=\"display:none;\">"
|
|
"<td colspan=\"4\">"
|
|
"<div style=\"background:#444;border-radius:4px;height:22px;width:100%;position:relative;\">"
|
|
"<div id=\"uploadProgressBar\" style=\"background:#5cb85c;height:100%;border-radius:4px;width:0%;transition:width 0.2s;\"></div>"
|
|
"<span id=\"uploadProgressText\" style=\"position:absolute;top:0;left:0;width:100%;text-align:center;line-height:22px;color:#fff;font-size:12px;\"></span>"
|
|
"</div></td></tr>\n");
|
|
}
|
|
else
|
|
{
|
|
htmlStr.append("<tr>");
|
|
htmlStr.append("<form action=\"/data/upload\" method=\"POST\" id=\"fileupload\">");
|
|
htmlStr.append("<input type=\"hidden\" id=\"basepath\" value=\"").append(directory).append("\">");
|
|
htmlStr.append("<td><input id=\"filepath\" type=\"text\" style=\"width:100%;\"></td>");
|
|
htmlStr.append(
|
|
"<td><button class=\"wm-button-small\" style=\"margin-left:0px\" type=\"button\"><label for=\"newfile\">Select File</label></button></td>");
|
|
htmlStr.append("</form>\n");
|
|
htmlStr.append("<td><input id=\"newfile\" type=\"file\" onchange=\"setpath()\"></td>");
|
|
//htmlStr.append("<td><button id=\"upload\" class=\"wm-button-small\" type=\"button\" onclick=\"uploadFile()\">Upload</button>");
|
|
htmlStr.append("<td><button id=\"upload\" class=\"fa fa-upload wm-button-small\" aria-hidden=\"true\" title=\"Upload\" type=\"button\" "
|
|
"onclick=\"uploadFile()\"></button>");
|
|
//htmlStr.append("<button id=\"mkdir\" class=\"wm-button-small\" type=\"button\" onclick=\"mkdir()\">MkDir</button>");
|
|
htmlStr.append("<button id=\"mkdir\" class=\"fa fa-folder-o wm-button-small\" aria-hidden=\"true\" title=\"Create Directory\" type=\"button\" "
|
|
"onclick=\"mkdir()\"></button>");
|
|
htmlStr.append("</td></tr>\n");
|
|
htmlStr.append("<tr id=\"uploadProgressRow\" style=\"display:none;\">"
|
|
"<td colspan=\"4\">"
|
|
"<div style=\"background:#444;border-radius:4px;height:22px;width:100%;position:relative;\">"
|
|
"<div id=\"uploadProgressBar\" style=\"background:#5cb85c;height:100%;border-radius:4px;width:0%;transition:width 0.2s;\"></div>"
|
|
"<span id=\"uploadProgressText\" style=\"position:absolute;top:0;left:0;width:100%;text-align:center;line-height:22px;color:#fff;font-size:12px;\"></span>"
|
|
"</div></td></tr>\n");
|
|
}
|
|
|
|
int entryCnt = 1;
|
|
std::string fileToStat;
|
|
while ((entry = readdir(dir)) != NULL)
|
|
{
|
|
entrytype = (entry->d_type == DT_DIR ? "directory" : "file");
|
|
fileToStat = sdpath + "/" + entry->d_name;
|
|
if (stat(fileToStat.c_str(), &entryStat) == -1)
|
|
{
|
|
ESP_LOGE(WIFITAG, "Failed to subdir stat %s : %s", entrytype, fileToStat.c_str());
|
|
continue;
|
|
}
|
|
sprintf(entrysize, "%ld", entryStat.st_size);
|
|
|
|
htmlStr.append("<tr>");
|
|
htmlStr.append("<td>");
|
|
htmlStr.append("<form method=\"GET\" style=\"display:inline\" action=\"?cmd=ren\">");
|
|
htmlStr.append("<input style=\"width:100%;\" type=\"text\" name=\"name\" value=\"").append(entry->d_name).append("\">");
|
|
htmlStr.append("<input type=\"hidden\" name=\"oldname\" value=\"").append(entry->d_name).append("\">");
|
|
htmlStr.append("<input type=\"hidden\" name=\"id\" value=\"").append(std::to_string(entryCnt)).append("\">");
|
|
htmlStr.append("<input type=\"hidden\" name=\"dir\" value=\"").append(directory).append("\">");
|
|
htmlStr.append("</td>");
|
|
htmlStr.append("<td>").append(entrytype).append("</td>");
|
|
htmlStr.append("<td style=\"text-align:right\">").append(entrysize).append("</td>");
|
|
// Action icons in fixed-width slots so columns align across rows.
|
|
// Slot layout: [Rename] [Open/Download] [Edit] [Copy] [gap] [Delete]
|
|
// Each slot is 30px inline-block. Empty slots get a blank spacer.
|
|
htmlStr.append("<td style=\"white-space:nowrap;\">");
|
|
|
|
// Slot 1: Rename (always)
|
|
htmlStr.append("<span style=\"display:inline-block;width:30px;\">");
|
|
htmlStr.append("<button type=\"submit\" name=\"cmd\" value=\"ren\" class=\"fa fa-pencil-square-o wm-button-small\" "
|
|
"aria-hidden=\"true\" title=\"Rename\"></button>");
|
|
htmlStr.append("</span>");
|
|
htmlStr.append("</form>");
|
|
|
|
// Slot 2: Open Dir (dirs) or Download (files)
|
|
htmlStr.append("<span style=\"display:inline-block;width:30px;\">");
|
|
if (entry->d_type == DT_DIR)
|
|
{
|
|
htmlStr.append("<form method=\"GET\" style=\"display:inline\" action=\"?cmd=dir\">");
|
|
htmlStr.append("<input type=\"hidden\" name=\"id\" value=\"").append(std::to_string(entryCnt)).append("\">");
|
|
htmlStr.append("<input type=\"hidden\" name=\"dir\" value=\"").append(directory);
|
|
if (htmlStr.back() != '/')
|
|
htmlStr.append("/");
|
|
htmlStr.append(entry->d_name).append("\">");
|
|
htmlStr.append("<button type=\"submit\" name=\"cmd\" value=\"dir\" class=\"fa fa-folder-open-o wm-button-small\" "
|
|
"aria-hidden=\"true\" title=\"Open\"></button>");
|
|
htmlStr.append("</form>");
|
|
}
|
|
else
|
|
{
|
|
htmlStr.append("<form method=\"GET\" style=\"display:inline\" action=\"/data/download\">");
|
|
htmlStr.append("<input type=\"hidden\" name=\"id\" value=\"").append(std::to_string(entryCnt)).append("\">");
|
|
htmlStr.append("<input type=\"hidden\" name=\"name\" value=\"").append(entry->d_name).append("\">");
|
|
htmlStr.append("<input type=\"hidden\" name=\"dir\" value=\"").append(directory).append("\">");
|
|
htmlStr.append("<button type=\"submit\" class=\"fa fa-download wm-button-small\" aria-hidden=\"true\" "
|
|
"title=\"Download\"></button>");
|
|
htmlStr.append("</form>");
|
|
}
|
|
htmlStr.append("</span>");
|
|
|
|
// Slot 3: Edit (text files only, blank otherwise)
|
|
htmlStr.append("<span style=\"display:inline-block;width:30px;\">");
|
|
if (entry->d_type != DT_DIR)
|
|
{
|
|
std::filesystem::path filePath = entry->d_name;
|
|
if (filePath.extension() == ".txt" || filePath.extension() == ".htm" || filePath.extension() == ".js" ||
|
|
filePath.extension() == ".css" || filePath.extension() == ".json")
|
|
{
|
|
htmlStr.append("<form method=\"GET\" style=\"display:inline\" action=\"?cmd=edit\">");
|
|
htmlStr.append("<input type=\"hidden\" name=\"id\" value=\"").append(std::to_string(entryCnt)).append("\">");
|
|
htmlStr.append("<input type=\"hidden\" name=\"name\" value=\"").append(entry->d_name).append("\">");
|
|
htmlStr.append("<input type=\"hidden\" name=\"dir\" value=\"").append(directory).append("\">");
|
|
htmlStr.append("<button type=\"submit\" name=\"cmd\" value=\"edit\" class=\"fa fa-pencil wm-button-small\" "
|
|
"aria-hidden=\"true\" title=\"Edit\"></button>");
|
|
htmlStr.append("</form>");
|
|
}
|
|
}
|
|
htmlStr.append("</span>");
|
|
|
|
// Slot 4: Copy (always)
|
|
htmlStr.append("<span style=\"display:inline-block;width:30px;\">");
|
|
htmlStr.append("<form method=\"GET\" style=\"display:inline\" action=\"?cmd=copy\">");
|
|
htmlStr.append("<input type=\"hidden\" name=\"id\" value=\"").append(std::to_string(entryCnt)).append("\">");
|
|
htmlStr.append("<input type=\"hidden\" name=\"name\" value=\"").append(entry->d_name).append("\">");
|
|
htmlStr.append("<input type=\"hidden\" name=\"dir\" value=\"").append(directory).append("\">");
|
|
htmlStr.append("<button type=\"submit\" name=\"cmd\" value=\"copy\" class=\"fa fa-copy wm-button-small\" "
|
|
"aria-hidden=\"true\" title=\"Copy\"></button>");
|
|
htmlStr.append("</form>");
|
|
htmlStr.append("</span>");
|
|
|
|
// Slot 5: gap + Delete (always, one slot space from last icon)
|
|
htmlStr.append("<span style=\"display:inline-block;width:30px;\"></span>");
|
|
htmlStr.append("<span style=\"display:inline-block;width:30px;\">");
|
|
htmlStr.append("<form method=\"GET\" style=\"display:inline\">");
|
|
htmlStr.append("<input type=\"hidden\" name=\"cmd\" value=\"del\">");
|
|
htmlStr.append("<input type=\"hidden\" name=\"id\" value=\"").append(std::to_string(entryCnt)).append("\">");
|
|
htmlStr.append("<input type=\"hidden\" name=\"name\" value=\"").append(entry->d_name).append("\">");
|
|
htmlStr.append("<input type=\"hidden\" name=\"dir\" value=\"").append(directory).append("\">");
|
|
htmlStr.append("<button type=\"submit\" class=\"fa fa-trash wm-button-small\" "
|
|
"aria-hidden=\"true\" title=\"Delete\" onclick=\"return confirmDelete(event, '")
|
|
.append(entry->d_name)
|
|
.append("');\"></button>");
|
|
htmlStr.append("</form>");
|
|
htmlStr.append("</span>");
|
|
htmlStr.append("</td></tr>\n");
|
|
entryCnt++;
|
|
}
|
|
closedir(dir);
|
|
|
|
htmlStr.append(" </tbody>");
|
|
htmlStr.append(" </table>");
|
|
htmlStr.append("</div>");
|
|
}
|
|
}
|
|
|
|
ESP_LOGI(WIFITAG, "sendFileManagerDir: sending chunk, htmlStr.size=%d, result=%d", (int)htmlStr.size(), (int)result);
|
|
if (result == ESP_OK)
|
|
{
|
|
result = httpd_resp_send_chunk(req, htmlStr.c_str(), htmlStr.size());
|
|
ESP_LOGI(WIFITAG, "sendFileManagerDir: chunk sent, result=%d", (int)result);
|
|
if (result != ESP_OK)
|
|
{
|
|
// Send failed — close the socket. Do NOT send zero-length terminator
|
|
// here: sendFileManagerDir() is called from expandVarsAndSend() as a
|
|
// template sub-handler; expandAndSendFile() sends the final terminator
|
|
// after ALL template variables are expanded. Adding one here would
|
|
// terminate the response mid-page, cutting off any HTML that follows
|
|
// the %SK_FILEDIR% expansion (e.g. sidebar navigation).
|
|
int sockFd = httpd_req_to_sockfd(req);
|
|
if (sockFd != -1)
|
|
close(sockFd);
|
|
}
|
|
}
|
|
|
|
// Send result, ESP_OK = all successful, anything else a file or data error occurred.
|
|
return result;
|
|
}
|
|
|
|
// /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 result = 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.
|
|
result = pThis->getPathFromURI(uriStr, "/reboot/", req->uri);
|
|
if (result == ESP_FAIL)
|
|
{
|
|
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Failed to extract URI");
|
|
}
|
|
else
|
|
{
|
|
if (uriStr.substr(0, 5) == "esp32")
|
|
{
|
|
// Determine the redirect IP from the request's Host header so we return
|
|
// to the same interface (WiFi or USB NCM) the user is currently using.
|
|
char hostBuf[64] = {};
|
|
std::string redirectIP;
|
|
if (httpd_req_get_hdr_value_str(req, "Host", hostBuf, sizeof(hostBuf)) == ESP_OK)
|
|
{
|
|
redirectIP = hostBuf;
|
|
// Strip port number if present (e.g. "192.168.7.1:80" -> "192.168.7.1")
|
|
size_t colonPos = redirectIP.find(':');
|
|
if (colonPos != std::string::npos)
|
|
redirectIP = redirectIP.substr(0, colonPos);
|
|
}
|
|
else if (!pThis->wifiConfig.clientParams.useDHCP)
|
|
{
|
|
redirectIP = pThis->wifiConfig.clientParams.ip;
|
|
}
|
|
else
|
|
{
|
|
redirectIP = pThis->wifiCtrl.client.ip;
|
|
}
|
|
|
|
// Build a response with a meta-refresh back to the originating IP.
|
|
resp = "<head> <meta http-equiv=\"refresh\" content=\"5; URL=http://" + redirectIP +
|
|
"/\" /> </head><body style=\"display:flex;justify-content:center;align-items:center;min-height:100vh;margin:0;"
|
|
"background:radial-gradient(ellipse at center,#f5f6f8 0%,#e8eaef 60%,#dcdfe6 100%);\">"
|
|
"<div style=\"text-align:center;\">"
|
|
"<font size=\"6\" face=\"verdana\" color=\"red\"/>Rebooting... </font><br><font size=\"4\" face=\"verdana\" "
|
|
"color=\"black\"/><p>Redirecting to http://" + redirectIP + "/ in 5 seconds. "
|
|
"If this screen doesnt auto-refresh, please reload manually.</p></font></div></body>";
|
|
|
|
// Send the response and wait a while, then request reboot.
|
|
result = httpd_resp_send(req, resp.c_str(), resp.size() + 1);
|
|
if (result == ESP_OK)
|
|
{
|
|
vTaskDelay(100);
|
|
pThis->wifiCtrl.run.reboot = true;
|
|
}
|
|
}
|
|
else if (uriStr.substr(0, 6) == "rp2350")
|
|
{
|
|
// Reboot the RP2350 by lowering the reset line.
|
|
pThis->resetRP2350(false, false);
|
|
|
|
// Indicate reboot successful.
|
|
httpd_resp_set_status(req, "200 OK");
|
|
httpd_resp_sendstr(req, "");
|
|
}
|
|
else if (uriStr.substr(0, 4) == "host")
|
|
{
|
|
// Send command to RP2350 to force the HOST reset line active.
|
|
CP_queueCmd("HRST");
|
|
|
|
// Indicate host reboot request successful.
|
|
httpd_resp_set_status(req, "200 OK");
|
|
httpd_resp_sendstr(req, "");
|
|
}
|
|
else
|
|
{
|
|
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Unknown reboot directive");
|
|
}
|
|
}
|
|
|
|
// Get out, a reboot will occur very soon.
|
|
return result;
|
|
}
|
|
|
|
bool WiFi::isValidPath(const char *path, const char *mountPath)
|
|
{
|
|
if (!path || strlen(path) == 0)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// Must start with the mount point
|
|
if (strncmp(path, mountPath, strlen(mountPath)) != 0)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// Prevent path traversal attempts
|
|
if (strstr(path, "..") != NULL)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// Optional: reject paths that are too long or contain suspicious characters
|
|
// (you can add more rules depending on your needs)
|
|
if (strlen(path) > 380)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
// ────────────────────────────────────────────────
|
|
// /list?path=... → JSON array for AJAX
|
|
esp_err_t WiFi::listDirectoryHandler(httpd_req_t *req)
|
|
{
|
|
char query[512];
|
|
char paramPath[384];
|
|
|
|
// Retrieve pointer to object in order to access data.
|
|
WiFi *pThis = (WiFi *) req->user_ctx;
|
|
strcpy(paramPath, pThis->wifiCtrl.run.fsPath);
|
|
|
|
// Get the query string (without leading '?')
|
|
if (httpd_req_get_url_query_str(req, query, sizeof(query)) == ESP_OK)
|
|
{
|
|
char value[384];
|
|
if (httpd_query_key_value(query, "path", value, sizeof(value)) == ESP_OK)
|
|
{
|
|
// Simple URL decode for %2F to /
|
|
char decoded[384];
|
|
char *p = value;
|
|
char *q = decoded;
|
|
while (*p)
|
|
{
|
|
if (*p == '%' && *(p + 1) == '2' && *(p + 2) == 'F')
|
|
{
|
|
*q++ = '/';
|
|
p += 3;
|
|
}
|
|
else
|
|
{
|
|
*q++ = *p++;
|
|
}
|
|
}
|
|
*q = '\0';
|
|
|
|
strcpy(paramPath, decoded);
|
|
|
|
if (!isValidPath(paramPath, pThis->wifiCtrl.run.fsPath))
|
|
{
|
|
httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Invalid path");
|
|
return ESP_OK;
|
|
}
|
|
}
|
|
}
|
|
|
|
ESP_LOGI(WIFITAG, "Opening directory: %s", paramPath);
|
|
|
|
DIR *dir = opendir(paramPath);
|
|
if (!dir)
|
|
{
|
|
httpd_resp_send_err(req, HTTPD_404_NOT_FOUND, "Cannot open directory");
|
|
return ESP_OK;
|
|
}
|
|
|
|
std::string json = "[";
|
|
bool first = true;
|
|
|
|
std::string curPath = paramPath;
|
|
if (curPath != pThis->wifiCtrl.run.fsPath)
|
|
{
|
|
json += R"({"name":"..","is_dir":true})";
|
|
first = false;
|
|
}
|
|
|
|
struct dirent *ent;
|
|
while ((ent = readdir(dir)) != NULL)
|
|
{
|
|
if (strcmp(ent->d_name, ".") == 0 || strcmp(ent->d_name, "..") == 0)
|
|
continue;
|
|
|
|
std::string full = std::string(paramPath) + "/" + ent->d_name;
|
|
struct stat st;
|
|
bool isDir = (stat(full.c_str(), &st) == 0 && S_ISDIR(st.st_mode));
|
|
|
|
if (!first)
|
|
json += ",";
|
|
first = false;
|
|
|
|
json += "{\"name\":\"" + std::string(ent->d_name) + "\",\"is_dir\":" + (isDir ? "true" : "false") + "}";
|
|
}
|
|
|
|
json += "]";
|
|
closedir(dir);
|
|
|
|
httpd_resp_set_type(req, "application/json");
|
|
httpd_resp_send(req, json.c_str(), json.length());
|
|
return ESP_OK;
|
|
}
|
|
|
|
// Change disk handler [?dir=…|?file=…|?cancel=1]|?diskno=...
|
|
esp_err_t WiFi::changeDiskHandler(httpd_req_t *req, enum DRIVETYPES driveType)
|
|
{
|
|
// Locals.
|
|
WiFi *pThis = (WiFi *) req->user_ctx;
|
|
std::string uri;
|
|
std::string url;
|
|
std::string urq;
|
|
std::vector<t_kvPair> pairs;
|
|
|
|
// Split the URI into components for ease of processing.
|
|
pThis->splitURI(req, uri, url, urq, pairs);
|
|
|
|
// Extract parameters.
|
|
for (auto pair : pairs)
|
|
{
|
|
if (pair.name == "diskno")
|
|
{
|
|
pThis->wifiCtrl.run.activeDiskImageNo = atoi(pair.value.c_str());
|
|
}
|
|
|
|
if (pair.name == "ramfileno")
|
|
{
|
|
pThis->wifiCtrl.run.activeRamFileImageNo = atoi(pair.value.c_str());
|
|
}
|
|
|
|
// Cancel disk change?
|
|
if (pair.name == "cancel")
|
|
{
|
|
pThis->wifiCtrl.run.floppyDiskImage[pThis->wifiCtrl.run.activeDiskImageNo].clear();
|
|
pThis->wifiCtrl.run.floppyDiskImage[pThis->wifiCtrl.run.activeRamFileImageNo].clear();
|
|
httpd_resp_send(req, NULL, 0);
|
|
return ESP_OK;
|
|
}
|
|
|
|
// File has been specified? Send to RP2350 and it will invoke a file update.
|
|
if (pair.name == "file")
|
|
{
|
|
ESP_LOGI(WIFITAG, "FILE: %s", pair.value.c_str());
|
|
if (isValidPath(pair.value.c_str(), pThis->wifiCtrl.run.fsPath))
|
|
{
|
|
// Send change command to RP2350.
|
|
switch (driveType)
|
|
{
|
|
case FLOPPY_DISK:
|
|
// The RP2350 will request image once command received.
|
|
pThis->wifiCtrl.run.floppyDiskImage[pThis->wifiCtrl.run.activeDiskImageNo] = pair.value;
|
|
pair.value.erase(0, strlen(pThis->wifiCtrl.run.fsPath) + 1);
|
|
ESP_LOGI(WIFITAG, "FILEFLOPPY: %s", pair.value.c_str());
|
|
pair.value.insert(0, "CHGD,,");
|
|
pair.value.insert(5, std::to_string(pThis->wifiCtrl.run.activeDiskImageNo));
|
|
ESP_LOGI(WIFITAG, "Queued floppy change: %s", pair.value.c_str());
|
|
CP_queueCmd(pair.value.c_str());
|
|
break;
|
|
|
|
case QUICK_DISK:
|
|
// The RP2350 will request image once command received.
|
|
pair.value.erase(0, strlen(pThis->wifiCtrl.run.fsPath) + 1);
|
|
ESP_LOGI(WIFITAG, "FILEQD: %s", pair.value.c_str());
|
|
pair.value.insert(0, "CHGQ,0,");
|
|
ESP_LOGI(WIFITAG, "Queued QD change: %s", pair.value.c_str());
|
|
CP_queueCmd(pair.value.c_str());
|
|
break;
|
|
|
|
case RAMFILE:
|
|
// The RP2350 will request image once command received.
|
|
pThis->wifiCtrl.run.ramFileImage[pThis->wifiCtrl.run.activeRamFileImageNo] = pair.value;
|
|
pair.value.erase(0, strlen(pThis->wifiCtrl.run.fsPath) + 1);
|
|
ESP_LOGI(WIFITAG, "FILERAM: %s", pair.value.c_str());
|
|
pair.value.insert(0, "CHGR,,");
|
|
pair.value.insert(5, std::to_string(pThis->wifiCtrl.run.activeRamFileImageNo));
|
|
ESP_LOGI(WIFITAG, "Queued RAMFILE change: %s", pair.value.c_str());
|
|
CP_queueCmd(pair.value.c_str());
|
|
break;
|
|
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
httpd_resp_send(req, NULL, 0);
|
|
return ESP_OK;
|
|
}
|
|
}
|
|
|
|
// Show file picker
|
|
std::string currentDir = pThis->wifiCtrl.run.fsPath;
|
|
|
|
char qdir[384];
|
|
if (httpd_query_key_value(req->uri + 1, "dir", qdir, sizeof(qdir)) == ESP_OK)
|
|
{
|
|
if (strncmp(qdir, pThis->wifiCtrl.run.fsPath, strlen(pThis->wifiCtrl.run.fsPath)) == 0 && strstr(qdir, "..") == NULL)
|
|
{
|
|
currentDir = qdir;
|
|
}
|
|
}
|
|
|
|
// Build HTML with embedded current path
|
|
char *html = (char *) malloc(8192);
|
|
if (html)
|
|
{
|
|
snprintf((html),
|
|
8192,
|
|
R"raw(<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<title>Change %s Disk</title>
|
|
<style>
|
|
body { font-family: Arial, sans-serif; background: #f4f4f9; margin: 0; padding: 20px; }
|
|
.modal { position: fixed; inset: 0; background: rgba(0,0,0,0.6); display: flex; justify-content: center; align-items: center; z-index: 9999; transition: opacity 0.2s ease; }
|
|
.modal-content { background: #fff; padding: 0; border-radius: 8px; width: 90%%; max-width: 720px; max-height: 50vh; overflow: hidden; box-shadow: 0 4px 20px rgba(0,0,0,0.25); display: flex; flex-direction: column; }
|
|
.modal-header { background: #428bca; color: white; padding: 10px 20px; font-size: 1.4em; font-weight: 600; border-bottom: 1px solid #1565c0; }
|
|
.modal-body { padding: 20px 24px; overflow-y: auto; flex: 1; }
|
|
.modal-footer { padding: 0 24px 24px 24px; text-align: right; }
|
|
table { width: 100%%; border-collapse: collapse; }
|
|
th, td { padding: 10px 12px; text-align: left; border-bottom: 1px solid #eee; }
|
|
th { background: #f8f9fa; font-weight: 600; color: #333; }
|
|
tr:hover { background: #f0f8ff; }
|
|
.dir { font-weight: bold; color: #1a3c5e; }
|
|
.dir::after { content: "/"; color: #666; }
|
|
.file-name { color: #0066cc; cursor: pointer; }
|
|
.cancel-btn { float: right; background: #d32f2f; color: white; border: none; padding: 5px 20px; border-radius: 25px; cursor: pointer; width: 20%%; margin-right: 5%%; margin-left: auto; margin-bottom: 10px; }
|
|
.cancel-btn:hover { background: #b71c1c; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="modal">
|
|
<div class="modal-content">
|
|
<div class="modal-header">Select %s Image</div>
|
|
|
|
<div style="padding: 12px 24px; background: #e8f4ff; border-bottom: 1px solid #d0e0f0; font-size: 0.95em;">
|
|
<strong>Currently loaded:</strong> %s
|
|
</div>
|
|
|
|
<div class="modal-body">
|
|
<table id="fileTable">
|
|
<thead><tr><th>Filename</th></tr></thead>
|
|
<tbody id="listing"><tr><td>Loading...</td></tr></tbody>
|
|
</table>
|
|
</div>
|
|
<button class="cancel-btn" id="cancelBtn">Cancel</button>
|
|
</div>
|
|
</div>
|
|
<script>
|
|
let curPath = '%s';
|
|
|
|
function esc(s) {
|
|
return s.replace(/[&<>"']/g, m => ({'&':'&','<':'<','>':'>','"':'"','\'':'''} )[m]);
|
|
}
|
|
|
|
function load(path) {
|
|
console.log("Loading:", path);
|
|
curPath = path;
|
|
history.replaceState(null, '', '/tasks/change%s?dir=' + encodeURIComponent(path));
|
|
fetch('/list?path=' + encodeURIComponent(path))
|
|
.then(r => {
|
|
if (!r.ok) throw new Error('HTTP ' + r.status);
|
|
return r.json();
|
|
})
|
|
.then(arr => {
|
|
const tbody = document.getElementById('listing');
|
|
tbody.innerHTML = '';
|
|
|
|
arr.forEach(e => {
|
|
const tr = document.createElement('tr');
|
|
const td = document.createElement('td');
|
|
|
|
let target = path + (path.endsWith('/') ? '' : '/') + e.name;
|
|
if (e.name === '..') {
|
|
let p = path.split('/'); p.pop();
|
|
target = p.join('/') || '/sdcard';
|
|
}
|
|
|
|
const link = document.createElement('span');
|
|
link.className = 'file-name';
|
|
link.textContent = esc(e.name);
|
|
link.style.cursor = 'pointer';
|
|
|
|
if (e.is_dir) {
|
|
td.className = 'dir';
|
|
link.addEventListener('click', () => load(target));
|
|
} else {
|
|
link.addEventListener('click', () => select(target));
|
|
}
|
|
|
|
td.appendChild(link);
|
|
tr.appendChild(td);
|
|
tbody.appendChild(tr);
|
|
});
|
|
|
|
if (arr.length === 0) {
|
|
tbody.innerHTML = '<tr><td style="color:#777;">Empty directory</td></tr>';
|
|
}
|
|
|
|
tbody.style.display = 'none';
|
|
void tbody.offsetHeight;
|
|
tbody.style.display = '';
|
|
})
|
|
.catch(e => {
|
|
console.error('Fetch failed:', e);
|
|
document.getElementById('listing').innerHTML = '<tr><td style="color:#d32f2f;">Error: ' + e.message + '</td></tr>';
|
|
});
|
|
}
|
|
|
|
function select(path) {
|
|
const filename = path.split('/').pop();
|
|
|
|
// 1. Immediately hide the modal (smooth fade optional)
|
|
const modal = document.querySelector('.modal');
|
|
if (modal) {
|
|
modal.style.opacity = '0';
|
|
setTimeout(() => {
|
|
modal.style.display = 'none';
|
|
modal.style.opacity = '1'; // reset for next open
|
|
}, 200);
|
|
}
|
|
|
|
// 2. Send the selection to server via AJAX (fire-and-forget)
|
|
fetch('/tasks/change%s?file=' + encodeURIComponent(path), {
|
|
method: 'GET',
|
|
cache: 'no-store'
|
|
})
|
|
.then(response => {
|
|
if (!response.ok) {
|
|
console.warn("File selection send failed:", response.status);
|
|
}
|
|
// No need to read body - server just needs to receive the request
|
|
})
|
|
.catch(err => console.error("Send error:", err));
|
|
|
|
// inside confirm block, before history.back()
|
|
document.body.style.opacity = '0.6';
|
|
setTimeout(() => {
|
|
document.body.style.opacity = '1';
|
|
history.back();
|
|
}, 300);
|
|
}
|
|
|
|
function doCancel() {
|
|
const modal = document.querySelector('.modal');
|
|
if (modal) {
|
|
modal.style.opacity = '0';
|
|
setTimeout(() => {
|
|
history.back();
|
|
}, 200); // wait for fade-out
|
|
} else {
|
|
history.back();
|
|
}
|
|
}
|
|
|
|
// Attach Cancel handler safely
|
|
window.addEventListener('DOMContentLoaded', () => {
|
|
const cancelBtn = document.getElementById('cancelBtn');
|
|
if (cancelBtn) {
|
|
cancelBtn.addEventListener('click', doCancel);
|
|
}
|
|
|
|
load(curPath);
|
|
});
|
|
</script>
|
|
</body>
|
|
</html>)raw",
|
|
driveType == FLOPPY_DISK ? "Floppy"
|
|
: driveType == QUICK_DISK ? "QuickDisk"
|
|
: "Unknown",
|
|
driveType == FLOPPY_DISK ? "Floppy"
|
|
: driveType == QUICK_DISK ? "QuickDisk"
|
|
: "Unknown",
|
|
driveType == FLOPPY_DISK ? pThis->wifiCtrl.run.floppyDiskImage[pThis->wifiCtrl.run.activeDiskImageNo].c_str()
|
|
: driveType == QUICK_DISK ? pThis->wifiCtrl.run.quickDiskImage[0].c_str()
|
|
: driveType == RAMFILE ? pThis->wifiCtrl.run.ramFileImage[pThis->wifiCtrl.run.activeRamFileImageNo].c_str()
|
|
: "N/A",
|
|
currentDir.c_str(),
|
|
driveType == FLOPPY_DISK ? "floppy"
|
|
: driveType == QUICK_DISK ? "qd"
|
|
: "na",
|
|
driveType == FLOPPY_DISK ? "floppy"
|
|
: driveType == QUICK_DISK ? "qd"
|
|
: "na");
|
|
|
|
httpd_resp_set_type(req, "text/html");
|
|
httpd_resp_send(req, html, strlen(html));
|
|
}
|
|
else
|
|
{
|
|
httpd_resp_set_type(req, "text/html");
|
|
httpd_resp_send(req, "Insufficient Memory", 20);
|
|
}
|
|
return ESP_OK;
|
|
}
|
|
|
|
// /tasks POST handler. Handler processes requests where a user wants a specific task executing, ie. Change Floppy Dusk.
|
|
esp_err_t WiFi::defaultTasksHandler(httpd_req_t *req)
|
|
{
|
|
// Locals.
|
|
esp_err_t result = 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.
|
|
result = pThis->getPathFromURI(uriStr, "/tasks/", req->uri);
|
|
if (result == ESP_FAIL)
|
|
{
|
|
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Failed to extract URI");
|
|
}
|
|
else
|
|
{
|
|
// Change the Floppy Drive Image.
|
|
if (uriStr.substr(0, 12) == "changefloppy")
|
|
{
|
|
// Extract the name of the floppy image, verify it and if valid, send name to the RP2350 and it will
|
|
// implement the disk change.
|
|
result = changeDiskHandler(req, FLOPPY_DISK);
|
|
|
|
// Send the response and wait a while, then request reboot.
|
|
//result = httpd_resp_send(req, resp.c_str(), resp.size() + 1);
|
|
//if (result == ESP_OK)
|
|
// {
|
|
// vTaskDelay(100);
|
|
// }
|
|
}
|
|
else
|
|
|
|
// Change the QD Drive Image.
|
|
if (uriStr.substr(0, 8) == "changeqd")
|
|
{
|
|
// Extract the name of the QD image, verify it and if valid, send name to the RP2350 and it will
|
|
// implement the disk change.
|
|
result = changeDiskHandler(req, QUICK_DISK);
|
|
}
|
|
else
|
|
|
|
// Reload the RP2350 JSON Config.
|
|
if (uriStr.substr(0, 9) == "reloadcfg")
|
|
{
|
|
pThis->reloadRP2350Config();
|
|
|
|
// Indicate sending of reload config message successful.
|
|
httpd_resp_set_status(req, "200 OK");
|
|
httpd_resp_sendstr(req, "");
|
|
}
|
|
else
|
|
{
|
|
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Unknown tasks directive");
|
|
}
|
|
}
|
|
|
|
// Procesing complete, return result.
|
|
return result;
|
|
}
|
|
|
|
// /recover handler. A simple handler to restore the SD card to a known state using a zipped recovery archive. The archive is unzipped and untarred in the SD Card
|
|
// root directory after deleting the old directories if they exist.
|
|
esp_err_t WiFi::defaultRecoverHandler(httpd_req_t *req)
|
|
{
|
|
// Locals.
|
|
esp_err_t result = ESP_FAIL;
|
|
std::string confirm;
|
|
std::string resp;
|
|
std::string uri;
|
|
std::string url;
|
|
std::string urq;
|
|
std::vector<t_kvPair> pairs;
|
|
struct stat dirhdl;
|
|
|
|
// Retrieve pointer to object in order to access data.
|
|
WiFi *pThis = (WiFi *) req->user_ctx;
|
|
|
|
// Split the URI into components for ease of processing.
|
|
pThis->splitURI(req, uri, url, urq, pairs);
|
|
|
|
// Extract parameters.
|
|
confirm = "no";
|
|
for (auto pair : pairs)
|
|
{
|
|
if (pair.name == "confirm")
|
|
{
|
|
confirm = pair.value;
|
|
}
|
|
}
|
|
|
|
// Setup the http response header.
|
|
if (!pThis->wifiConfig.clientParams.useDHCP)
|
|
{
|
|
resp = "<head> <meta http-equiv=\"refresh\" content=\"3; URL=http://" + std::string(pThis->wifiConfig.clientParams.ip);
|
|
}
|
|
else
|
|
{
|
|
resp = "<head> <meta http-equiv=\"refresh\" content=\"3; URL=http://" + std::string(pThis->wifiCtrl.client.ip);
|
|
}
|
|
|
|
// We only process if a /recover?confirm=yes is received, just as precaution.
|
|
// Once the files are unpacked, issue an http redirect to /reboot.
|
|
if (confirm.compare("yes") == 0)
|
|
{
|
|
// Check to see if there is a backup file.
|
|
std::string backupDir = pThis->wifiCtrl.run.fsPath;
|
|
backupDir.append(WIFI_RECOVERY_FILE_DIR);
|
|
|
|
if (stat(backupDir.c_str(), &dirhdl) == -1)
|
|
{
|
|
ESP_LOGI(WIFITAG, "Backup directory (%s) doesnt exist, cannot recover.", backupDir.c_str());
|
|
resp += "/\" /> </head><body><font size=\"6\" face=\"verdana\" color=\"red\"/>Recovery not possible, backup directory does not exist... "
|
|
"</font><br><font size=\"4\" face=\"verdana\" color=\"black\"/><p>Please see documentation for suggestions.</p></font></body>";
|
|
}
|
|
else
|
|
{
|
|
std::string backupFile = backupDir;
|
|
backupFile.append("/").append(WIFI_RECOVERY_FILE);
|
|
|
|
if (stat(backupFile.c_str(), &dirhdl) == -1)
|
|
{
|
|
ESP_LOGI(WIFITAG, "Backup file (%s) doesnt exist, cannot recover.", backupFile.c_str());
|
|
resp += "/\" /> </head><body><font size=\"6\" face=\"verdana\" color=\"red\"/>Recovery not possible, backup file does not exist... "
|
|
"</font><br><font size=\"4\" face=\"verdana\" color=\"black\"/><p>Please see documentation for suggestions.</p></font></body>";
|
|
}
|
|
else
|
|
{
|
|
std::string oldEntry = pThis->wifiCtrl.run.fsPath;
|
|
oldEntry.append(pThis->wifiCtrl.run.webfs);
|
|
std::filesystem::path filePath(WIFI_RECOVERY_FILE);
|
|
|
|
// Remove old directories. Dont need to check error as the directory may not exist.
|
|
pThis->sdcard->deleteDir(oldEntry);
|
|
|
|
// Unzip the backup file.
|
|
result = pThis->unpackFile(backupDir, WIFI_RECOVERY_FILE, pThis->wifiCtrl.run.fsPath, filePath.replace_extension());
|
|
if (result != ESP_OK)
|
|
{
|
|
ESP_LOGI(WIFITAG, "Failed to unzip backup file (%s).", backupFile.c_str());
|
|
resp += "/\" /> </head><body><font size=\"6\" face=\"verdana\" color=\"red\"/>Recovery failure, could not unzip the backup file.... "
|
|
"</font><br><font size=\"4\" face=\"verdana\" color=\"black\"/><p>Please see documentation for suggestions.</p></font></body>";
|
|
}
|
|
else
|
|
{
|
|
// Untar the file.
|
|
result = pThis->unpackFile(pThis->wifiCtrl.run.fsPath, filePath.c_str(), pThis->wifiCtrl.run.fsPath, "");
|
|
if (result != ESP_OK)
|
|
{
|
|
ESP_LOGI(WIFITAG, "Failed to untar backup file (%s).", filePath.c_str());
|
|
resp += "/\" /> </head><body><font size=\"6\" face=\"verdana\" color=\"red\"/>Recovery failure, could not untar the backup file.... "
|
|
"</font><br><font size=\"4\" face=\"verdana\" color=\"black\"/><p>Please see documentation for suggestions.</p></font></body>";
|
|
}
|
|
else
|
|
{
|
|
oldEntry = pThis->wifiCtrl.run.fsPath;
|
|
oldEntry.append("/").append(filePath);
|
|
|
|
if (unlink(oldEntry.c_str()) != 0)
|
|
{
|
|
ESP_LOGI(WIFITAG, "Failed to remove temp tar file (%s).", oldEntry.c_str());
|
|
resp += "/\" /> </head><body><font size=\"6\" face=\"verdana\" color=\"red\"/>Recovery succeeded but couldnt remove temporary "
|
|
"files.... </font><br><font size=\"4\" face=\"verdana\" color=\"black\"/><p>Please see documentation for "
|
|
"suggestions.</p></font></body>";
|
|
}
|
|
else
|
|
{
|
|
result = ESP_OK;
|
|
resp += "/\" /> </head><body><font size=\"6\" face=\"verdana\" color=\"red\"/>Recovered SD Card contents from backup... "
|
|
"</font><br><font size=\"4\" face=\"verdana\" color=\"black\"/><p>If this screen doesnt auto-refresh, 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>";
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Build a response message to invalid command.
|
|
resp += "/\" /> </head><body><font size=\"6\" face=\"verdana\" color=\"red\"/>Invalid parameters, command ignored... </font><font size=\"5\" "
|
|
"face=\"verdana\" color=\"black\"/>Please wait.</font></body>";
|
|
}
|
|
httpd_resp_send(req, resp.c_str(), resp.size() + 1);
|
|
|
|
// Recovery was successful, reboot to activate.
|
|
if (result == ESP_OK)
|
|
{
|
|
vTaskDelay(100);
|
|
pThis->wifiCtrl.run.reboot = true;
|
|
}
|
|
|
|
// Get out, a reboot will occur very soon.
|
|
return (result);
|
|
}
|
|
|
|
// Method to start the basic HTTP webserver.
|
|
bool WiFi::startWebserver(void)
|
|
{
|
|
// Locals.
|
|
bool retVal = false;
|
|
httpd_config_t config = HTTPD_DEFAULT_CONFIG();
|
|
|
|
// Tweak default settings.
|
|
config.stack_size = 16384;
|
|
config.uri_match_fn = httpd_uri_match_wildcard;
|
|
config.lru_purge_enable = true;
|
|
config.max_uri_handlers = 18;
|
|
// max_open_sockets must accommodate the maximum number of parallel resource
|
|
// requests a page makes. The personality page requests ~12 files at once;
|
|
// with the default of 7, lru_purge_enable kills active file-transfer sessions
|
|
// to free slots — the evicted handler has already logged "Sending gzip file"
|
|
// but sent 0 body bytes, so the browser shows the resource as (pending).
|
|
// Set to 14 to handle 12 concurrent requests with 2 slots spare.
|
|
// CONFIG_LWIP_MAX_SOCKETS must be >= max_open_sockets + system sockets (~2);
|
|
// it is set to 16 in sdkconfig.
|
|
config.max_open_sockets = 20; // LWIP_MAX_SOCKETS(24) - 3 internal - 1 spare = 20
|
|
// Keep keep-alive enabled (HTTP/1.1 default) so the browser can multiplex
|
|
// multiple resource requests over one TCP connection. USB NCM at Full Speed
|
|
// (12 Mbps) is much slower than WiFi, so active transfers hold sockets longer.
|
|
// Short recv_wait_timeout aggressively recycles idle keep-alive connections.
|
|
// With 20 sockets available, the browser can always open fresh connections.
|
|
// Longer timeouts (5-15s) cause stale keep-alive connections from previous
|
|
// page loads to hold sockets, triggering LRU purge of active transfers.
|
|
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 otaesp32fw = {.uri = "/ota/esp32firmware", .method = HTTP_POST, .handler = otaESP32FirmwareUpdatePOSTHandler, .user_ctx = this};
|
|
const httpd_uri_t otarp2350fw = {.uri = "/ota/rp2350firmware", .method = HTTP_POST, .handler = otaRP2350FirmwareUpdatePOSTHandler, .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 rebootSubGET = {.uri = "/reboot/*", .method = HTTP_GET, .handler = defaultRebootHandler, .user_ctx = this};
|
|
const httpd_uri_t tasksPOST = {.uri = "/tasks", .method = HTTP_POST, .handler = defaultTasksHandler, .user_ctx = this};
|
|
const httpd_uri_t tasksGET = {.uri = "/tasks", .method = HTTP_GET, .handler = defaultTasksHandler, .user_ctx = this};
|
|
const httpd_uri_t tasksSubGET = {.uri = "/tasks/*", .method = HTTP_GET, .handler = defaultTasksHandler, .user_ctx = this};
|
|
const httpd_uri_t uriListGET = {.uri = "/list", .method = HTTP_GET, .handler = listDirectoryHandler, .user_ctx = this};
|
|
const httpd_uri_t recoverGET = {.uri = "/recover", .method = HTTP_GET, .handler = defaultRecoverHandler, .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.
|
|
strlcpy(this->wifiCtrl.run.basePath, this->wifiCtrl.run.fsPath, sizeof(this->wifiCtrl.run.basePath));
|
|
strlcat(this->wifiCtrl.run.basePath, this->wifiCtrl.run.webfs, 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/GET 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, &otaesp32fw);
|
|
httpd_register_uri_handler(wifiCtrl.run.server, &otarp2350fw);
|
|
httpd_register_uri_handler(wifiCtrl.run.server, &otafp);
|
|
httpd_register_uri_handler(wifiCtrl.run.server, &rebootPOST);
|
|
httpd_register_uri_handler(wifiCtrl.run.server, &rebootGET);
|
|
httpd_register_uri_handler(wifiCtrl.run.server, &rebootSubGET);
|
|
httpd_register_uri_handler(wifiCtrl.run.server, &tasksPOST);
|
|
httpd_register_uri_handler(wifiCtrl.run.server, &tasksGET);
|
|
httpd_register_uri_handler(wifiCtrl.run.server, &tasksSubGET);
|
|
httpd_register_uri_handler(wifiCtrl.run.server, &recoverGET);
|
|
httpd_register_uri_handler(wifiCtrl.run.server, &uriListGET);
|
|
|
|
// If no URL matches then default to serving files.
|
|
httpd_register_uri_handler(wifiCtrl.run.server, &files);
|
|
retVal = true;
|
|
}
|
|
|
|
// Return result of startup.
|
|
return (retVal);
|
|
}
|
|
|
|
// 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;
|
|
}
|
|
|
|
#if defined(CONFIG_IF_WIFI_ENABLED)
|
|
// Event handler for Client mode Wifi event callback.
|
|
void WiFi::wifiClientHandler(void *arg, esp_event_base_t event_base, int32_t event_id, void *event_data)
|
|
{
|
|
// Locals.
|
|
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)
|
|
{
|
|
pThis->wifiCtrl.client.connected = false;
|
|
|
|
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 (%d/%d)",
|
|
pThis->wifiCtrl.client.clientRetryCnt, CONFIG_IF_WIFI_MAX_RETRIES);
|
|
}
|
|
else
|
|
{
|
|
// Exhausted fast retries — reset counter and keep trying.
|
|
// Never give up permanently; handles transient AP outages (router
|
|
// reboots, range glitches) without requiring a power cycle.
|
|
// Each 20-retry cycle takes ~48s, providing natural backoff.
|
|
ESP_LOGW(WIFITAG, "max retries exhausted, restarting retry cycle");
|
|
pThis->wifiCtrl.client.clientRetryCnt = 0;
|
|
esp_wifi_connect();
|
|
}
|
|
}
|
|
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;
|
|
|
|
// Disable modem sleep so the radio stays active during TCP transfers.
|
|
// With MODEM_SLEEP (type 1) the radio sleeps between 100 ms beacon
|
|
// intervals; missed TCP ACKs during large file transfers (e.g. 97 kB
|
|
// font files) collapse the TCP congestion window and cause the send to
|
|
// stall indefinitely, leaving the browser with a pending 0-byte response.
|
|
esp_wifi_set_ps(WIFI_PS_NONE);
|
|
|
|
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(sWifiEventGroup, WIFI_CONNECTED_BIT);
|
|
|
|
// Start the webserver if it hasn't been configured.
|
|
if (pThis->wifiCtrl.run.server == NULL)
|
|
{
|
|
pThis->startWebserver();
|
|
}
|
|
}
|
|
}
|
|
|
|
// Event handler for Access Point mode Wifi event callback.
|
|
void WiFi::wifiAPHandler(void *arg, esp_event_base_t event_base, int32_t event_id, void *event_data)
|
|
{
|
|
// Locals.
|
|
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 hasn't 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);
|
|
}
|
|
}
|
|
|
|
// 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 = {};
|
|
wifiConfig.sta.threshold.authmode = WIFI_AUTH_WPA2_PSK;
|
|
wifiConfig.sta.pmf_cfg.capable = true;
|
|
wifiConfig.sta.pmf_cfg.required = false;
|
|
bool retVal = false;
|
|
|
|
// 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);
|
|
|
|
// 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.
|
|
sWifiEventGroup = xEventGroupCreate();
|
|
if (!sWifiEventGroup)
|
|
{
|
|
ESP_LOGI(WIFITAG, "Couldnt create event group, disabling WiFi.");
|
|
}
|
|
// Setup the network interface. esp_netif_init() is idempotent — safe if USB NCM called it first.
|
|
else if (esp_netif_init() != ESP_OK)
|
|
{
|
|
ESP_LOGI(WIFITAG, "Couldnt initialise netif, disabling WiFi.");
|
|
}
|
|
// Setup the event loop. Tolerate ESP_ERR_INVALID_STATE (already created by USB NCM init).
|
|
else
|
|
{
|
|
esp_err_t loopErr = esp_event_loop_create_default();
|
|
if (loopErr != ESP_OK && loopErr != ESP_ERR_INVALID_STATE)
|
|
{
|
|
ESP_LOGI(WIFITAG, "Couldnt initialise event loop, disabling WiFi.");
|
|
}
|
|
else
|
|
{
|
|
// 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);
|
|
}
|
|
else
|
|
{
|
|
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);
|
|
}
|
|
else
|
|
{
|
|
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);
|
|
}
|
|
else
|
|
{
|
|
IP4_ADDR(&ipInfo.gw, a, b, c, d);
|
|
esp_netif_set_ip_info(netConfig, &ipInfo);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Setup the config for wifi.
|
|
wifiInitConfig = WIFI_INIT_CONFIG_DEFAULT();
|
|
if (esp_wifi_init(&wifiInitConfig) != ESP_OK)
|
|
{
|
|
ESP_LOGI(WIFITAG, "Couldnt initialise wifi with default parameters, disabling WiFi.");
|
|
}
|
|
// Use RAM storage so WiFi never writes to NVS flash. Without this, any WiFi
|
|
// disconnect (including spontaneous ones caused by SPI ISR starvation during
|
|
// RP2350 reset) triggers an NVS flash write which temporarily disables the
|
|
// flash cache. If WiFi IRAM code accesses a DROM constant during that window
|
|
// it panics with "Cache disabled but cached memory region accessed" (EXCCAUSE=7,
|
|
// MMU fault at 0x3d300150). RAM storage eliminates all such NVS writes.
|
|
else if (esp_wifi_set_storage(WIFI_STORAGE_RAM) != ESP_OK)
|
|
{
|
|
ESP_LOGI(WIFITAG, "Couldnt set WiFi storage to RAM, disabling WiFi.");
|
|
}
|
|
// Register event handlers.
|
|
else if (esp_event_handler_instance_register(WIFI_EVENT, ESP_EVENT_ANY_ID, &wifiClientHandler, this, &instID) != ESP_OK)
|
|
{
|
|
ESP_LOGI(WIFITAG, "Couldnt register event handler for ID, disabling WiFi.");
|
|
}
|
|
else if (esp_event_handler_instance_register(IP_EVENT, IP_EVENT_STA_GOT_IP, &wifiClientHandler, this, &instIP) != ESP_OK)
|
|
{
|
|
ESP_LOGI(WIFITAG, "Couldnt register event handler for IP, disabling WiFi.");
|
|
}
|
|
else if (esp_wifi_set_mode(WIFI_MODE_STA) != ESP_OK)
|
|
{
|
|
ESP_LOGI(WIFITAG, "Couldnt set Wifi mode to Client, disabling WiFi.");
|
|
}
|
|
else if (esp_wifi_set_config(WIFI_IF_STA, &wifiConfig) != ESP_OK)
|
|
{
|
|
ESP_LOGI(WIFITAG, "Couldnt configure client mode, disabling WiFi.");
|
|
}
|
|
else if (esp_wifi_start() != ESP_OK)
|
|
{
|
|
ESP_LOGI(WIFITAG, "Couldnt start Client session, disabling WiFi.");
|
|
}
|
|
else
|
|
{
|
|
// Set TX power after esp_wifi_start() — must be called after the WiFi driver is running.
|
|
// Value is in 0.25dBm units: 8=2dBm, 40=10dBm, 78=19.5dBm, 80=20dBm.
|
|
// A value of 0 or out-of-range uses the ESP-IDF default (max for region).
|
|
int8_t txPwr = this->wifiConfig.params.txPower;
|
|
if (txPwr >= 8 && txPwr <= 84)
|
|
{
|
|
esp_wifi_set_max_tx_power(txPwr);
|
|
ESP_LOGI(WIFITAG, "TX power set to %d (%.1f dBm)", txPwr, txPwr * 0.25f);
|
|
}
|
|
else
|
|
{
|
|
// Default: let ESP-IDF use its default (typically max for configured country)
|
|
int8_t actual = 0;
|
|
esp_wifi_get_max_tx_power(&actual);
|
|
ESP_LOGI(WIFITAG, "TX power using default: %d (%.1f dBm)", actual, actual * 0.25f);
|
|
}
|
|
|
|
// When USB NCM is enabled, don't block the main task waiting for WiFi —
|
|
// the webserver is already running on the USB NCM interface and needs
|
|
// the main task responsive. WiFi will connect asynchronously via the
|
|
// event handler (wifiClientHandler), which starts the webserver if needed.
|
|
// Without USB NCM, keep the original blocking behaviour to maintain
|
|
// backwards compatibility.
|
|
#if defined(CONFIG_IF_USB_NCM_ENABLED)
|
|
// Non-blocking: wait briefly then return success regardless.
|
|
// WiFi connection continues in the background via event handlers.
|
|
bits = xEventGroupWaitBits(sWifiEventGroup, WIFI_CONNECTED_BIT | WIFI_FAIL_BIT, pdFALSE, pdFALSE, pdMS_TO_TICKS(5000));
|
|
if (bits & WIFI_CONNECTED_BIT)
|
|
{
|
|
ESP_LOGI(WIFITAG, "WiFi connected.");
|
|
}
|
|
else
|
|
{
|
|
ESP_LOGI(WIFITAG, "WiFi connecting in background (USB NCM active).");
|
|
}
|
|
retVal = true; // Don't reboot — USB NCM is functional.
|
|
#else
|
|
bits = xEventGroupWaitBits(sWifiEventGroup, WIFI_CONNECTED_BIT | WIFI_FAIL_BIT, pdFALSE, pdFALSE, portMAX_DELAY);
|
|
if (bits & WIFI_CONNECTED_BIT)
|
|
{
|
|
retVal = true;
|
|
}
|
|
else if (bits & WIFI_FAIL_BIT)
|
|
{
|
|
ESP_LOGI(WIFITAG, "Connection Fail: SSID:%s, password:%s.", this->wifiConfig.clientParams.ssid, this->wifiConfig.clientParams.pwd);
|
|
}
|
|
else
|
|
{
|
|
ESP_LOGE(WIFITAG, "Unknown event, bits:%ld", (unsigned long) bits);
|
|
}
|
|
#endif
|
|
}
|
|
}
|
|
}
|
|
|
|
// Return success/fail.
|
|
return (retVal);
|
|
}
|
|
|
|
// Method to initialise the interface as a Soft Access point with a given SSID
|
|
// and password.
|
|
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 = {};
|
|
memcpy(wifiConfig.ap.ssid, CONFIG_IF_WIFI_SSID, strlen(CONFIG_IF_WIFI_SSID));
|
|
memcpy(wifiConfig.ap.password, CONFIG_IF_WIFI_DEFAULT_SSID_PWD, strlen(CONFIG_IF_WIFI_DEFAULT_SSID_PWD));
|
|
wifiConfig.ap.ssid_len = strlen(CONFIG_IF_WIFI_SSID);
|
|
wifiConfig.ap.channel = CONFIG_IF_WIFI_AP_CHANNEL;
|
|
wifiConfig.ap.authmode = WIFI_AUTH_WPA_WPA2_PSK;
|
|
wifiConfig.ap.ssid_hidden = CONFIG_IF_WIFI_SSID_HIDDEN;
|
|
wifiConfig.ap.max_connection = CONFIG_IF_WIFI_MAX_CONNECTIONS;
|
|
wifiConfig.ap.beacon_interval = 100;
|
|
bool retVal = false;
|
|
|
|
// Initialize the network interface. esp_netif_init() is idempotent.
|
|
if (esp_netif_init() != ESP_OK)
|
|
{
|
|
ESP_LOGI(WIFITAG, "Couldnt initialise network interface, disabling WiFi.");
|
|
}
|
|
else
|
|
{
|
|
// Tolerate ESP_ERR_INVALID_STATE (already created by USB NCM init).
|
|
retCode = esp_event_loop_create_default();
|
|
if (retCode != ESP_OK && retCode != ESP_ERR_INVALID_STATE)
|
|
{
|
|
ESP_LOGI(WIFITAG, "Couldnt create default loop(%d), disabling WiFi.", retCode);
|
|
}
|
|
else
|
|
{
|
|
// 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);
|
|
}
|
|
else
|
|
{
|
|
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);
|
|
}
|
|
else
|
|
{
|
|
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);
|
|
}
|
|
else
|
|
{
|
|
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);
|
|
|
|
// Initialise AP with default parameters.
|
|
wifiInitConfig = WIFI_INIT_CONFIG_DEFAULT();
|
|
if (esp_wifi_init(&wifiInitConfig) != ESP_OK)
|
|
{
|
|
ESP_LOGI(WIFITAG, "Couldnt setup AP with default parameters, disabling WiFi.");
|
|
}
|
|
// RAM storage — same reason as in setupWifiClient(): prevents NVS
|
|
// flash writes that disable the cache during WiFi events.
|
|
else if (esp_wifi_set_storage(WIFI_STORAGE_RAM) != ESP_OK)
|
|
{
|
|
ESP_LOGI(WIFITAG, "Couldnt set WiFi AP storage to RAM, disabling WiFi.");
|
|
}
|
|
// Setup callback handlers for wifi events.
|
|
else if (esp_event_handler_instance_register(WIFI_EVENT, ESP_EVENT_ANY_ID, &wifiAPHandler, this, NULL) != ESP_OK)
|
|
{
|
|
ESP_LOGI(WIFITAG, "Couldnt setup event handlers, disabling WiFi.");
|
|
}
|
|
else
|
|
{
|
|
// 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_OK)
|
|
{
|
|
ESP_LOGI(WIFITAG, "Couldnt set mode to Access Point, disabling WiFi.");
|
|
}
|
|
// Configure the Access Point.
|
|
else if (esp_wifi_set_config(WIFI_IF_AP, &wifiConfig) != ESP_OK)
|
|
{
|
|
ESP_LOGI(WIFITAG, "Couldnt configure Access Point, disabling WiFi.");
|
|
}
|
|
// Start the Access Point.
|
|
else if (esp_wifi_start() != ESP_OK)
|
|
{
|
|
ESP_LOGI(WIFITAG, "Couldnt start Access Point session, disabling WiFi.");
|
|
}
|
|
else
|
|
{
|
|
// Set TX power after esp_wifi_start().
|
|
int8_t txPwr = this->wifiConfig.params.txPower;
|
|
if (txPwr >= 8 && txPwr <= 84)
|
|
{
|
|
esp_wifi_set_max_tx_power(txPwr);
|
|
ESP_LOGI(WIFITAG, "AP TX power set to %d (%.1f dBm)", txPwr, txPwr * 0.25f);
|
|
}
|
|
retVal = true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Return fail/success.
|
|
return (retVal);
|
|
}
|
|
|
|
// Method to disable the wifi turning the transceiver off.
|
|
bool WiFi::stopWifi(void)
|
|
{
|
|
// Locals.
|
|
bool retVal = true;
|
|
|
|
if (esp_wifi_stop() != ESP_OK)
|
|
{
|
|
ESP_LOGI(WIFITAG, "Couldnt stop the WiFi, reboot needed.");
|
|
retVal = false;
|
|
}
|
|
else if (esp_wifi_deinit() != ESP_OK)
|
|
{
|
|
ESP_LOGI(WIFITAG, "Couldnt deactivate WiFi, reboot needed.");
|
|
retVal = false;
|
|
}
|
|
|
|
// No errors.
|
|
return (retVal);
|
|
}
|
|
|
|
// WiFi interface runtime logic. This method provides a browser interface to the tzpuPico 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;
|
|
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;
|
|
ESP_LOGI(WIFITAG, "AP mode set but not configured, rebooting.");
|
|
}
|
|
else
|
|
{
|
|
wifiCtrl.run.wifiMode = wifiConfig.params.wifiMode;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Setup as a client for general browser connectivity.
|
|
if (!setupWifiClient())
|
|
{
|
|
wifiCtrl.run.reboot = true;
|
|
ESP_LOGI(WIFITAG, "Client mode set but not configured, rebooting.");
|
|
}
|
|
else
|
|
{
|
|
wifiCtrl.run.wifiMode = wifiConfig.params.wifiMode;
|
|
}
|
|
}
|
|
}
|
|
#endif // CONFIG_IF_WIFI_ENABLED
|
|
|
|
// readRP2350Info — superseded by binary IPC.
|
|
// The RP2350 now sends the flash partition header proactively via the INF IPC command
|
|
// (IPCF_CMD_INF). SDCard::storeRP2350Info() receives the full t_FlashPartitionHeader
|
|
// in a single T2 payload and writes it directly to wifiCtrl.run.rp2350FlashHeader via
|
|
// the sdcard->rp2350Header pointer set in WiFi::init().
|
|
// This function is no longer called and is retained only for reference.
|
|
void WiFi::readRP2350Info(const std::string &inParam, FSPI &fspi)
|
|
{
|
|
// No-op: rp2350FlashHeader is populated by SDCard::storeRP2350Info() on INF receipt.
|
|
(void) inParam;
|
|
(void) fspi;
|
|
}
|
|
|
|
// Method to instruct the RP2350 to reload the configuration file from the SD card. This method
|
|
// sends a simple prompt which the RP2350 then acts upon to read the JSON configuration file and
|
|
// process it.
|
|
void WiFi::reloadRP2350Config(void)
|
|
{
|
|
// Locals.
|
|
char msg[8];
|
|
|
|
// Send a CFG request with the current active App in the RP2350 which simply tells the RP2350 to reload configuration for that App.
|
|
// Use synchronous send (500 ms timeout) so that the command is confirmed delivered to the
|
|
// RP2350 via the next NOP poll before returning. If it fails, fall back to async queue.
|
|
sprintf(msg, "CFG%01d", wifiCtrl.run.rp2350FlashHeader.activeApp);
|
|
if (!CP_sendCmd(msg, 500))
|
|
{
|
|
ESP_LOGW("WIFI", "reloadRP2350Config: sync send failed, falling back to async queue");
|
|
CP_queueCmd(msg);
|
|
}
|
|
}
|
|
|
|
// Method to test if a reboot is required due to configuration changes.
|
|
bool WiFi::doReboot(void)
|
|
{
|
|
return (wifiCtrl.run.reboot);
|
|
}
|
|
|
|
/*
|
|
void WiFi::init(bool defaultMode, uint16_t device, NVS *nvs, SDCard *sdcard, cJSON *config, const char *fsPath, t_versionList *versionList)
|
|
{
|
|
// Locals.
|
|
std::string tmpDir = std::string(fsPath) + WIFI_TEMP_DIR;
|
|
struct stat s;
|
|
bool dirExists;
|
|
|
|
// Initialise variables.
|
|
if (!nvs || !sdcard || !fsPath || !versionList)
|
|
{
|
|
logError("Invalid initialization parameters");
|
|
}
|
|
else
|
|
{
|
|
wifiCtrl.client.clientRetryCnt = 0;
|
|
wifiCtrl.run.server = nullptr;
|
|
wifiCtrl.run.errorMsg = "";
|
|
wifiCtrl.run.rebootButton = false;
|
|
wifiCtrl.run.reboot = false;
|
|
wifiCtrl.run.wifiMode = defaultMode ? WIFI_CONFIG_AP : WIFI_ON;
|
|
this->nvs = nvs;
|
|
this->sdcard = sdcard;
|
|
strcpy(this->wifiCtrl.run.fsPath, fsPath);
|
|
this->wifiCtrl.run.versionList = versionList;
|
|
memset(&wifiCtrl.run.rp2350FlashHeader, 0x00, sizeof(t_FlashPartitionHeader));
|
|
if(sdcard) sdcard->rp2350Header = &wifiCtrl.run.rp2350FlashHeader;
|
|
|
|
// Check that the temp directory exists on SD card, create if necessary.
|
|
dirExists = stat(tmpDir.c_str(), &s) == 0;
|
|
ESP_LOGI(WIFITAG, "mkdir %s, test result:%d", tmpDir.c_str(), dirExists);
|
|
if (!dirExists)
|
|
{
|
|
if (mkdir(tmpDir.c_str(), 0775) != 0)
|
|
{
|
|
logError(("mkdir failed for directory:" + tmpDir).c_str());
|
|
}
|
|
}
|
|
|
|
// Retrieve configuration, if it doesnt exist, set defaults.
|
|
if (!nvs->retrieveData(this->wifiCtrl.run.thisClass.c_str(), &this->wifiConfig, sizeof(t_wifiConfig)))
|
|
{
|
|
ESP_LOGI(WIFITAG, "Wifi configuration set to default, no valid config in NVS found.");
|
|
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';
|
|
#if defined(CONFIG_IF_WIFI_ENABLED)
|
|
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);
|
|
wifiConfig.params.wifiMode = WIFI_CONFIG_AP;
|
|
#else
|
|
wifiConfig.apParams.ssid[0] = '\0';
|
|
wifiConfig.apParams.pwd[0] = '\0';
|
|
wifiConfig.params.wifiMode = WIFI_OFF;
|
|
#endif
|
|
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.txPower = 0;
|
|
|
|
if (!nvs->persistData(wifiCtrl.run.thisClass.c_str(), &this->wifiConfig, sizeof(t_wifiConfig)))
|
|
{
|
|
ESP_LOGI(WIFITAG, "Persisting Default Wifi configuration data failed, check NVS setup.");
|
|
}
|
|
else if (!nvs->commitData())
|
|
{
|
|
ESP_LOGI(WIFITAG, "NVS Commit writes operation failed, some previous writes may not persist in future power cycles.");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
*/
|
|
|
|
// Constructor. No overloading methods.
|
|
WiFi::WiFi(bool defaultMode, uint16_t device, NVS *nvs, SDCard *sdcard, cJSON *config, const char *fsPath, t_versionList *versionList)
|
|
{
|
|
// Locals.
|
|
std::string tmpDir = std::string(fsPath) + WIFI_TEMP_DIR;
|
|
struct stat s;
|
|
bool dirExists;
|
|
|
|
// Initialise variables.
|
|
if (!nvs || !sdcard || !fsPath || !versionList)
|
|
{
|
|
logError("Invalid initialization parameters");
|
|
}
|
|
else
|
|
{
|
|
wifiCtrl.client.clientRetryCnt = 0;
|
|
wifiCtrl.run.server = nullptr;
|
|
wifiCtrl.run.errorMsg = "";
|
|
wifiCtrl.run.rebootButton = false;
|
|
wifiCtrl.run.reboot = false;
|
|
wifiCtrl.run.wifiMode = defaultMode ? WIFI_CONFIG_AP : WIFI_ON;
|
|
wifiCtrl.run.activeDiskImageNo = 0;
|
|
wifiCtrl.run.activeRamFileImageNo = 0;
|
|
strcpy(wifiCtrl.run.webfs, WIFI_WEBFS_PATH);
|
|
wifiCtrl.run.ioBuf = IO_getIoBuf();
|
|
this->nvs = nvs;
|
|
this->sdcard = sdcard;
|
|
strncpy(wifiCtrl.run.fsPath, fsPath, FILE_PATH_MAX);
|
|
wifiCtrl.run.versionList = versionList;
|
|
memset(&wifiCtrl.run.rp2350FlashHeader, 0, sizeof(t_FlashPartitionHeader));
|
|
wifiCtrl.run.rp2350CpuFreq = 0;
|
|
wifiCtrl.run.rp2350PsramFreq = 0;
|
|
wifiCtrl.run.rp2350Voltage = 0;
|
|
wifiCtrl.run.rp2350FlashSize = 0;
|
|
wifiCtrl.run.rp2350PsramSize = 0;
|
|
wifiCtrl.run.rp2350HostClkHz = 0;
|
|
wifiCtrl.run.rp2350EmulSpeedHz = 0;
|
|
wifiCtrl.run.rp2350DriverSummary[0] = '\0';
|
|
// Wire SDCard's rp2350Header pointer directly to our rp2350FlashHeader so
|
|
// SDCard::storeRP2350Info() can populate it when the INF IPC command arrives.
|
|
if (sdcard)
|
|
sdcard->rp2350Header = &wifiCtrl.run.rp2350FlashHeader;
|
|
|
|
// Setup device, so we serve content based on the underlying host processor type.
|
|
switch (device)
|
|
{
|
|
case TARGET_DEVICE_Z80:
|
|
wifiCtrl.run.hostDevice = device;
|
|
wifiCtrl.run.hostDeviceName = TARGET_DEVICE_NAME_Z80;
|
|
wifiCtrl.run.hostDeviceDisplayName = TARGET_DEVICE_DISP_NAME_Z80;
|
|
break;
|
|
case TARGET_DEVICE_6502:
|
|
wifiCtrl.run.hostDevice = device;
|
|
wifiCtrl.run.hostDeviceName = TARGET_DEVICE_NAME_6502;
|
|
wifiCtrl.run.hostDeviceDisplayName = TARGET_DEVICE_DISP_NAME_6502;
|
|
break;
|
|
case TARGET_DEVICE_6512:
|
|
wifiCtrl.run.hostDevice = device;
|
|
wifiCtrl.run.hostDeviceName = TARGET_DEVICE_NAME_6512;
|
|
wifiCtrl.run.hostDeviceDisplayName = TARGET_DEVICE_DISP_NAME_6512;
|
|
break;
|
|
default:
|
|
wifiCtrl.run.hostDevice = TARGET_DEVICE_UNDEF;
|
|
wifiCtrl.run.hostDeviceName = TARGET_DEVICE_NAME_UNDEF;
|
|
wifiCtrl.run.hostDeviceDisplayName = TARGET_DEVICE_DISP_NAME_UNDEF;
|
|
break;
|
|
}
|
|
|
|
// Check that the temp directory exists on SD card, create if necessary.
|
|
dirExists = stat(tmpDir.c_str(), &s) == 0;
|
|
ESP_LOGI(WIFITAG, "mkdir %s, test result:%d", tmpDir.c_str(), dirExists);
|
|
if (!dirExists)
|
|
{
|
|
if (mkdir(tmpDir.c_str(), 0775) != 0)
|
|
{
|
|
logError(("mkdir failed for directory:" + tmpDir).c_str());
|
|
}
|
|
}
|
|
|
|
// Retrieve configuration, if it doesnt exist, set defaults.
|
|
if (!nvs->retrieveData(this->wifiCtrl.run.thisClass.c_str(), &this->wifiConfig, sizeof(t_wifiConfig)))
|
|
{
|
|
ESP_LOGI(WIFITAG, "Wifi configuration set to default, no valid config in NVS found.");
|
|
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';
|
|
#if defined(CONFIG_IF_WIFI_ENABLED)
|
|
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);
|
|
wifiConfig.params.wifiMode = WIFI_CONFIG_AP;
|
|
#else
|
|
wifiConfig.apParams.ssid[0] = '\0';
|
|
wifiConfig.apParams.pwd[0] = '\0';
|
|
wifiConfig.params.wifiMode = WIFI_OFF;
|
|
#endif
|
|
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.txPower = 0;
|
|
|
|
if (!nvs->persistData(wifiCtrl.run.thisClass.c_str(), &this->wifiConfig, sizeof(t_wifiConfig)))
|
|
{
|
|
ESP_LOGI(WIFITAG, "Persisting Default Wifi configuration data failed, check NVS setup.");
|
|
}
|
|
else if (!nvs->commitData())
|
|
{
|
|
ESP_LOGI(WIFITAG, "NVS Commit writes operation failed, some previous writes may not persist in future power cycles.");
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Sanitize txPower for backward compatibility with old NVS data
|
|
// that didn't have this field (would contain garbage bytes).
|
|
if (wifiConfig.params.txPower != 0 && (wifiConfig.params.txPower < 8 || wifiConfig.params.txPower > 84))
|
|
wifiConfig.params.txPower = 0;
|
|
}
|
|
|
|
// Sync the runtime wifiMode from the persisted config so that template
|
|
// expansion shows the correct WiFi mode (AP vs Client) even before
|
|
// wifi->run() is called. Without this, pages loaded via USB NCM before
|
|
// WiFi connects would show AP config instead of Client config.
|
|
if (wifiConfig.params.wifiMode == WIFI_CONFIG_CLIENT || wifiConfig.params.wifiMode == WIFI_CONFIG_AP)
|
|
{
|
|
wifiCtrl.run.wifiMode = wifiConfig.params.wifiMode;
|
|
}
|
|
|
|
// Process JSON and override any setting specified in config.
|
|
// Check we have a valid handle.
|
|
if (config == NULL)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Get the WiFi object.
|
|
cJSON *wifi_obj = cJSON_GetObjectItem(config, "wifi");
|
|
if (!cJSON_IsObject(wifi_obj))
|
|
{
|
|
ESP_LOGE(WIFITAG, "Error: 'wifi' is not an object\n");
|
|
return;
|
|
}
|
|
|
|
// Extract fields
|
|
cJSON *ssid = cJSON_GetObjectItem(wifi_obj, "ssid");
|
|
cJSON *pwd = cJSON_GetObjectItem(wifi_obj, "password");
|
|
cJSON *ip = cJSON_GetObjectItem(wifi_obj, "ip");
|
|
cJSON *netmask = cJSON_GetObjectItem(wifi_obj, "netmask");
|
|
cJSON *gateway = cJSON_GetObjectItem(wifi_obj, "gateway");
|
|
cJSON *dhcp = cJSON_GetObjectItem(wifi_obj, "dhcp");
|
|
cJSON *mode = cJSON_GetObjectItem(wifi_obj, "wifimode");
|
|
cJSON *active = cJSON_GetObjectItem(wifi_obj, "override");
|
|
cJSON *webfs = cJSON_GetObjectItem(wifi_obj, "webfs");
|
|
cJSON *persist = cJSON_GetObjectItem(wifi_obj, "persist");
|
|
cJSON *txpower = cJSON_GetObjectItem(wifi_obj, "txpower");
|
|
|
|
// Check override flag, exit if not valid.
|
|
if (!cJSON_IsNumber(active) || active->valueint < 0 || active->valueint > 1)
|
|
{
|
|
ESP_LOGE(WIFITAG, "Error: 'override' is not numeric, it should be 1 to override, 0 to ignore.\n");
|
|
return;
|
|
}
|
|
|
|
// Ignore config file if override is disabled.
|
|
if (active->valueint == 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Check wifi mode, can only be an access point or client mode.
|
|
if (!cJSON_IsString(mode) || (strcasecmp(mode->valuestring, "ap") != 0 && strcasecmp(mode->valuestring, "client") != 0))
|
|
{
|
|
ESP_LOGE(WIFITAG, "Error: 'wifimode' is not valid, it should be 'ap' for access point or 'client' for client mode.\n");
|
|
return;
|
|
}
|
|
|
|
// Access Point mode?
|
|
if (strcasecmp(mode->valuestring, "ap") == 0)
|
|
{
|
|
// If present, override the SSID.
|
|
if (cJSON_IsString(ssid) && strlen(ssid->valuestring) > 0)
|
|
{
|
|
strcpy(wifiConfig.apParams.ssid, ssid->valuestring);
|
|
}
|
|
// If present, override the PASSWORD.
|
|
if (cJSON_IsString(pwd) && strlen(pwd->valuestring) > 0)
|
|
{
|
|
strcpy(wifiConfig.apParams.pwd, pwd->valuestring);
|
|
}
|
|
// If present, override the IP.
|
|
if (cJSON_IsString(ip) && strlen(ip->valuestring) > 0)
|
|
{
|
|
strcpy(wifiConfig.apParams.ip, ip->valuestring);
|
|
}
|
|
// If present, override the NETMASK.
|
|
if (cJSON_IsString(netmask) && strlen(netmask->valuestring) > 0)
|
|
{
|
|
strcpy(wifiConfig.apParams.netmask, netmask->valuestring);
|
|
}
|
|
// If present, override the GATEWAY.
|
|
if (cJSON_IsString(gateway) && strlen(gateway->valuestring) > 0)
|
|
{
|
|
strcpy(wifiConfig.apParams.gateway, gateway->valuestring);
|
|
}
|
|
|
|
// Set WiFi mode to Access Point.
|
|
wifiCtrl.run.wifiMode = WIFI_CONFIG_AP;
|
|
}
|
|
else
|
|
{
|
|
// If present, override the SSID.
|
|
if (cJSON_IsString(ssid) && strlen(ssid->valuestring) > 0)
|
|
{
|
|
strcpy(wifiConfig.clientParams.ssid, ssid->valuestring);
|
|
}
|
|
// If present, override the PASSWORD.
|
|
if (cJSON_IsString(pwd) && strlen(pwd->valuestring) > 0)
|
|
{
|
|
strcpy(wifiConfig.clientParams.pwd, pwd->valuestring);
|
|
}
|
|
// If present, override the IP.
|
|
if (cJSON_IsString(ip) && strlen(ip->valuestring) > 0)
|
|
{
|
|
strcpy(wifiConfig.clientParams.ip, ip->valuestring);
|
|
}
|
|
// If present, override the NETMASK.
|
|
if (cJSON_IsString(netmask) && strlen(netmask->valuestring) > 0)
|
|
{
|
|
strcpy(wifiConfig.clientParams.netmask, netmask->valuestring);
|
|
}
|
|
// If present, override the GATEWAY.
|
|
if (cJSON_IsString(gateway) && strlen(gateway->valuestring) > 0)
|
|
{
|
|
strcpy(wifiConfig.clientParams.gateway, gateway->valuestring);
|
|
}
|
|
|
|
// Check dhcp flag, ignore if not valid.
|
|
if (!cJSON_IsNumber(dhcp) || dhcp->valueint < 0 || dhcp->valueint > 1)
|
|
{
|
|
ESP_LOGE(WIFITAG, "Error: 'dhcp' is not numeric, it should be 1 to use DHCP, 0 to use fixed address.\n");
|
|
}
|
|
else
|
|
{
|
|
this->wifiConfig.clientParams.useDHCP = dhcp->valueint;
|
|
}
|
|
|
|
// Set WiFi mode to Client Mode.
|
|
wifiCtrl.run.wifiMode = WIFI_CONFIG_CLIENT;
|
|
}
|
|
|
|
// If present, override the TX power setting (0=default, 8-84 in 0.25dBm units).
|
|
if (cJSON_IsNumber(txpower))
|
|
{
|
|
int val = txpower->valueint;
|
|
if (val == 0 || (val >= 8 && val <= 84))
|
|
{
|
|
wifiConfig.params.txPower = (int8_t) val;
|
|
ESP_LOGI(WIFITAG, "TX power set from config: %d (%.1f dBm)", val, val * 0.25f);
|
|
}
|
|
else
|
|
{
|
|
ESP_LOGE(WIFITAG, "Error: 'txpower' value %d out of range (0, or 8-84).\n", val);
|
|
}
|
|
}
|
|
|
|
// If present, override the web filesystem root directory name with the one provided in the config file.
|
|
if (cJSON_IsString(webfs) && strlen(webfs->valuestring) > 0)
|
|
{
|
|
ESP_LOGE(WIFITAG, "Info: webfs=%s\n", webfs->valuestring);
|
|
wifiCtrl.run.webfs[0] = '/';
|
|
strncpy(&wifiCtrl.run.webfs[1], webfs->valuestring, MAX_WEBFS_LEN - 1);
|
|
}
|
|
|
|
// If persist flag is set, ensure it has a valid value.
|
|
if (!cJSON_IsInvalid(persist) && cJSON_IsNumber(persist) && persist->valueint >= 0 && persist->valueint < 2)
|
|
{
|
|
// If set to 1, save the current values into the NVS to act as default when config file not present.
|
|
if (persist->valueint == 1)
|
|
{
|
|
if (!nvs->persistData(wifiCtrl.run.thisClass.c_str(), &this->wifiConfig, sizeof(t_wifiConfig)))
|
|
{
|
|
ESP_LOGI(WIFITAG, "Persisting Default Wifi configuration data failed, check NVS setup.");
|
|
}
|
|
else if (!nvs->commitData())
|
|
{
|
|
ESP_LOGI(WIFITAG, "NVS Commit writes operation failed, some previous writes may not persist in future power cycles.");
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Advise if the flag exists but isnt valid.
|
|
if (!cJSON_IsInvalid(persist))
|
|
{
|
|
ESP_LOGE(WIFITAG, "Error: 'persist' is not valid, it should be 1 to save new values in NVS, 0 to not save.");
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Constructor, used for version reporting so no hardware is initialised.
|
|
WiFi::WiFi(void)
|
|
{
|
|
}
|
|
|
|
// Destructor - only ever called when the class is used for version reporting.
|
|
WiFi::~WiFi(void)
|
|
{
|
|
}
|
|
|
|
#endif
|