Adds template libs
This commit is contained in:
+1
-1
@@ -2,4 +2,4 @@
|
||||
data/
|
||||
www/
|
||||
include/secrets.h
|
||||
lib/*
|
||||
;lib/*
|
||||
@@ -3,6 +3,6 @@ build_flags =
|
||||
-D BUILD_TARGET=\"$PIOENV\"
|
||||
-D ESP32SVELTEKIT_RUNNING_CORE=0
|
||||
-D EMBED_WWW
|
||||
-D ENABLE_CORS
|
||||
;-D ENABLE_CORS
|
||||
;-D SERIAL_INFO
|
||||
-D CORS_ORIGIN=\"*\"
|
||||
;-D CORS_ORIGIN=\"*\"
|
||||
+1
-1
@@ -3,7 +3,7 @@ build_flags =
|
||||
-D FT_BATTERY=0
|
||||
-D FT_NTP=1
|
||||
-D FT_SECURITY=1
|
||||
-D FT_MQTT=0
|
||||
-D FT_MQTT=1
|
||||
-D FT_SLEEP=0
|
||||
-D FT_UPLOAD_FIRMWARE=1
|
||||
-D FT_DOWNLOAD_FIRMWARE=1
|
||||
|
||||
@@ -0,0 +1,143 @@
|
||||
/**
|
||||
* ESP32 SvelteKit
|
||||
*
|
||||
* A simple, secure and extensible framework for IoT projects for ESP32 platforms
|
||||
* with responsive Sveltekit front-end built with TailwindCSS and DaisyUI.
|
||||
* https://github.com/theelims/ESP32-sveltekit
|
||||
*
|
||||
* Copyright (C) 2018 - 2023 rjwats
|
||||
* Copyright (C) 2023 theelims
|
||||
*
|
||||
* All Rights Reserved. This software may be modified and distributed under
|
||||
* the terms of the LGPL v3 license. See the LICENSE file for details.
|
||||
**/
|
||||
|
||||
#include <APSettingsService.h>
|
||||
|
||||
APSettingsService::APSettingsService(PsychicHttpServer *server,
|
||||
FS *fs,
|
||||
SecurityManager *securityManager) : _server(server),
|
||||
_securityManager(securityManager),
|
||||
_httpEndpoint(APSettings::read, APSettings::update, this, server, AP_SETTINGS_SERVICE_PATH, securityManager),
|
||||
_fsPersistence(APSettings::read, APSettings::update, this, fs, AP_SETTINGS_FILE),
|
||||
_dnsServer(nullptr),
|
||||
_lastManaged(0),
|
||||
_reconfigureAp(false)
|
||||
{
|
||||
addUpdateHandler([&](const String &originId)
|
||||
{ reconfigureAP(); },
|
||||
false);
|
||||
}
|
||||
|
||||
void APSettingsService::begin()
|
||||
{
|
||||
_httpEndpoint.begin();
|
||||
_fsPersistence.readFromFS();
|
||||
reconfigureAP();
|
||||
}
|
||||
|
||||
void APSettingsService::reconfigureAP()
|
||||
{
|
||||
_lastManaged = millis() - MANAGE_NETWORK_DELAY;
|
||||
_reconfigureAp = true;
|
||||
_recoveryMode = false;
|
||||
}
|
||||
|
||||
void APSettingsService::recoveryMode()
|
||||
{
|
||||
#ifdef SERIAL_INFO
|
||||
Serial.println("Recovery Mode needed");
|
||||
#endif
|
||||
_lastManaged = millis() - MANAGE_NETWORK_DELAY;
|
||||
_recoveryMode = true;
|
||||
_reconfigureAp = true;
|
||||
}
|
||||
|
||||
void APSettingsService::loop()
|
||||
{
|
||||
unsigned long currentMillis = millis();
|
||||
unsigned long manageElapsed = (unsigned long)(currentMillis - _lastManaged);
|
||||
if (manageElapsed >= MANAGE_NETWORK_DELAY)
|
||||
{
|
||||
_lastManaged = currentMillis;
|
||||
manageAP();
|
||||
}
|
||||
handleDNS();
|
||||
}
|
||||
|
||||
void APSettingsService::manageAP()
|
||||
{
|
||||
WiFiMode_t currentWiFiMode = WiFi.getMode();
|
||||
if (_state.provisionMode == AP_MODE_ALWAYS ||
|
||||
(_state.provisionMode == AP_MODE_DISCONNECTED && WiFi.status() != WL_CONNECTED) || _recoveryMode)
|
||||
{
|
||||
if (_reconfigureAp || currentWiFiMode == WIFI_OFF || currentWiFiMode == WIFI_STA)
|
||||
{
|
||||
startAP();
|
||||
}
|
||||
}
|
||||
else if ((currentWiFiMode == WIFI_AP || currentWiFiMode == WIFI_AP_STA) &&
|
||||
(_reconfigureAp || !WiFi.softAPgetStationNum()))
|
||||
{
|
||||
stopAP();
|
||||
}
|
||||
_reconfigureAp = false;
|
||||
}
|
||||
|
||||
void APSettingsService::startAP()
|
||||
{
|
||||
#ifdef SERIAL_INFO
|
||||
Serial.println("Starting software access point");
|
||||
#endif
|
||||
WiFi.softAPConfig(_state.localIP, _state.gatewayIP, _state.subnetMask);
|
||||
WiFi.softAP(_state.ssid.c_str(), _state.password.c_str(), _state.channel, _state.ssidHidden, _state.maxClients);
|
||||
#if CONFIG_IDF_TARGET_ESP32C3
|
||||
WiFi.setTxPower(WIFI_POWER_8_5dBm); // https://www.wemos.cc/en/latest/c3/c3_mini_1_0_0.html#about-wifi
|
||||
#endif
|
||||
if (!_dnsServer)
|
||||
{
|
||||
IPAddress apIp = WiFi.softAPIP();
|
||||
#ifdef SERIAL_INFO
|
||||
Serial.print("Starting captive portal on ");
|
||||
Serial.println(apIp);
|
||||
#endif
|
||||
_dnsServer = new DNSServer;
|
||||
_dnsServer->start(DNS_PORT, "*", apIp);
|
||||
}
|
||||
}
|
||||
|
||||
void APSettingsService::stopAP()
|
||||
{
|
||||
if (_dnsServer)
|
||||
{
|
||||
#ifdef SERIAL_INFO
|
||||
Serial.println("Stopping captive portal");
|
||||
#endif
|
||||
_dnsServer->stop();
|
||||
delete _dnsServer;
|
||||
_dnsServer = nullptr;
|
||||
}
|
||||
#ifdef SERIAL_INFO
|
||||
Serial.println("Stopping software access point");
|
||||
#endif
|
||||
WiFi.softAPdisconnect(true);
|
||||
}
|
||||
|
||||
void APSettingsService::handleDNS()
|
||||
{
|
||||
if (_dnsServer)
|
||||
{
|
||||
_dnsServer->processNextRequest();
|
||||
}
|
||||
}
|
||||
|
||||
APNetworkStatus APSettingsService::getAPNetworkStatus()
|
||||
{
|
||||
WiFiMode_t currentWiFiMode = WiFi.getMode();
|
||||
bool apActive = currentWiFiMode == WIFI_AP || currentWiFiMode == WIFI_AP_STA;
|
||||
if (apActive && _state.provisionMode != AP_MODE_ALWAYS && WiFi.status() == WL_CONNECTED)
|
||||
{
|
||||
return APNetworkStatus::LINGERING;
|
||||
}
|
||||
return apActive ? APNetworkStatus::ACTIVE : APNetworkStatus::INACTIVE;
|
||||
}
|
||||
@@ -0,0 +1,177 @@
|
||||
#ifndef APSettingsConfig_h
|
||||
#define APSettingsConfig_h
|
||||
|
||||
/**
|
||||
* ESP32 SvelteKit
|
||||
*
|
||||
* A simple, secure and extensible framework for IoT projects for ESP32 platforms
|
||||
* with responsive Sveltekit front-end built with TailwindCSS and DaisyUI.
|
||||
* https://github.com/theelims/ESP32-sveltekit
|
||||
*
|
||||
* Copyright (C) 2018 - 2023 rjwats
|
||||
* Copyright (C) 2023 theelims
|
||||
*
|
||||
* All Rights Reserved. This software may be modified and distributed under
|
||||
* the terms of the LGPL v3 license. See the LICENSE file for details.
|
||||
**/
|
||||
|
||||
#include <SettingValue.h>
|
||||
#include <HttpEndpoint.h>
|
||||
#include <FSPersistence.h>
|
||||
#include <JsonUtils.h>
|
||||
#include <WiFi.h>
|
||||
|
||||
#include <DNSServer.h>
|
||||
#include <IPAddress.h>
|
||||
|
||||
#ifndef FACTORY_AP_PROVISION_MODE
|
||||
#define FACTORY_AP_PROVISION_MODE AP_MODE_DISCONNECTED
|
||||
#endif
|
||||
|
||||
#ifndef FACTORY_AP_SSID
|
||||
#define FACTORY_AP_SSID "ESP32-SvelteKit-#{unique_id}"
|
||||
#endif
|
||||
|
||||
#ifndef FACTORY_AP_PASSWORD
|
||||
#define FACTORY_AP_PASSWORD "esp-sveltekit"
|
||||
#endif
|
||||
|
||||
#ifndef FACTORY_AP_LOCAL_IP
|
||||
#define FACTORY_AP_LOCAL_IP "192.168.4.1"
|
||||
#endif
|
||||
|
||||
#ifndef FACTORY_AP_GATEWAY_IP
|
||||
#define FACTORY_AP_GATEWAY_IP "192.168.4.1"
|
||||
#endif
|
||||
|
||||
#ifndef FACTORY_AP_SUBNET_MASK
|
||||
#define FACTORY_AP_SUBNET_MASK "255.255.255.0"
|
||||
#endif
|
||||
|
||||
#ifndef FACTORY_AP_CHANNEL
|
||||
#define FACTORY_AP_CHANNEL 1
|
||||
#endif
|
||||
|
||||
#ifndef FACTORY_AP_SSID_HIDDEN
|
||||
#define FACTORY_AP_SSID_HIDDEN false
|
||||
#endif
|
||||
|
||||
#ifndef FACTORY_AP_MAX_CLIENTS
|
||||
#define FACTORY_AP_MAX_CLIENTS 4
|
||||
#endif
|
||||
|
||||
#define AP_SETTINGS_FILE "/config/apSettings.json"
|
||||
#define AP_SETTINGS_SERVICE_PATH "/rest/apSettings"
|
||||
|
||||
#define AP_MODE_ALWAYS 0
|
||||
#define AP_MODE_DISCONNECTED 1
|
||||
#define AP_MODE_NEVER 2
|
||||
|
||||
#define MANAGE_NETWORK_DELAY 10000
|
||||
#define DNS_PORT 53
|
||||
|
||||
enum APNetworkStatus
|
||||
{
|
||||
ACTIVE = 0,
|
||||
INACTIVE,
|
||||
LINGERING
|
||||
};
|
||||
|
||||
class APSettings
|
||||
{
|
||||
public:
|
||||
uint8_t provisionMode;
|
||||
String ssid;
|
||||
String password;
|
||||
uint8_t channel;
|
||||
bool ssidHidden;
|
||||
uint8_t maxClients;
|
||||
|
||||
IPAddress localIP;
|
||||
IPAddress gatewayIP;
|
||||
IPAddress subnetMask;
|
||||
|
||||
bool operator==(const APSettings &settings) const
|
||||
{
|
||||
return provisionMode == settings.provisionMode && ssid == settings.ssid && password == settings.password &&
|
||||
channel == settings.channel && ssidHidden == settings.ssidHidden && maxClients == settings.maxClients &&
|
||||
localIP == settings.localIP && gatewayIP == settings.gatewayIP && subnetMask == settings.subnetMask;
|
||||
}
|
||||
|
||||
static void read(APSettings &settings, JsonObject &root)
|
||||
{
|
||||
root["provision_mode"] = settings.provisionMode;
|
||||
root["ssid"] = settings.ssid;
|
||||
root["password"] = settings.password;
|
||||
root["channel"] = settings.channel;
|
||||
root["ssid_hidden"] = settings.ssidHidden;
|
||||
root["max_clients"] = settings.maxClients;
|
||||
root["local_ip"] = settings.localIP.toString();
|
||||
root["gateway_ip"] = settings.gatewayIP.toString();
|
||||
root["subnet_mask"] = settings.subnetMask.toString();
|
||||
}
|
||||
|
||||
static StateUpdateResult update(JsonObject &root, APSettings &settings)
|
||||
{
|
||||
APSettings newSettings = {};
|
||||
newSettings.provisionMode = root["provision_mode"] | FACTORY_AP_PROVISION_MODE;
|
||||
switch (settings.provisionMode)
|
||||
{
|
||||
case AP_MODE_ALWAYS:
|
||||
case AP_MODE_DISCONNECTED:
|
||||
case AP_MODE_NEVER:
|
||||
break;
|
||||
default:
|
||||
newSettings.provisionMode = AP_MODE_DISCONNECTED;
|
||||
}
|
||||
newSettings.ssid = root["ssid"] | SettingValue::format(FACTORY_AP_SSID);
|
||||
newSettings.password = root["password"] | FACTORY_AP_PASSWORD;
|
||||
newSettings.channel = root["channel"] | FACTORY_AP_CHANNEL;
|
||||
newSettings.ssidHidden = root["ssid_hidden"] | FACTORY_AP_SSID_HIDDEN;
|
||||
newSettings.maxClients = root["max_clients"] | FACTORY_AP_MAX_CLIENTS;
|
||||
|
||||
JsonUtils::readIP(root, "local_ip", newSettings.localIP, FACTORY_AP_LOCAL_IP);
|
||||
JsonUtils::readIP(root, "gateway_ip", newSettings.gatewayIP, FACTORY_AP_GATEWAY_IP);
|
||||
JsonUtils::readIP(root, "subnet_mask", newSettings.subnetMask, FACTORY_AP_SUBNET_MASK);
|
||||
|
||||
if (newSettings == settings)
|
||||
{
|
||||
return StateUpdateResult::UNCHANGED;
|
||||
}
|
||||
settings = newSettings;
|
||||
return StateUpdateResult::CHANGED;
|
||||
}
|
||||
};
|
||||
|
||||
class APSettingsService : public StatefulService<APSettings>
|
||||
{
|
||||
public:
|
||||
APSettingsService(PsychicHttpServer *server, FS *fs, SecurityManager *securityManager);
|
||||
|
||||
void begin();
|
||||
void loop();
|
||||
APNetworkStatus getAPNetworkStatus();
|
||||
void recoveryMode();
|
||||
|
||||
private:
|
||||
PsychicHttpServer *_server;
|
||||
SecurityManager *_securityManager;
|
||||
HttpEndpoint<APSettings> _httpEndpoint;
|
||||
FSPersistence<APSettings> _fsPersistence;
|
||||
|
||||
// for the captive portal
|
||||
DNSServer *_dnsServer;
|
||||
|
||||
// for the mangement delay loop
|
||||
volatile unsigned long _lastManaged;
|
||||
volatile boolean _reconfigureAp;
|
||||
volatile boolean _recoveryMode = false;
|
||||
|
||||
void reconfigureAP();
|
||||
void manageAP();
|
||||
void startAP();
|
||||
void stopAP();
|
||||
void handleDNS();
|
||||
};
|
||||
|
||||
#endif // end APSettingsConfig_h
|
||||
@@ -0,0 +1,45 @@
|
||||
/**
|
||||
* ESP32 SvelteKit
|
||||
*
|
||||
* A simple, secure and extensible framework for IoT projects for ESP32 platforms
|
||||
* with responsive Sveltekit front-end built with TailwindCSS and DaisyUI.
|
||||
* https://github.com/theelims/ESP32-sveltekit
|
||||
*
|
||||
* Copyright (C) 2018 - 2023 rjwats
|
||||
* Copyright (C) 2023 theelims
|
||||
*
|
||||
* All Rights Reserved. This software may be modified and distributed under
|
||||
* the terms of the LGPL v3 license. See the LICENSE file for details.
|
||||
**/
|
||||
|
||||
#include <APStatus.h>
|
||||
|
||||
APStatus::APStatus(PsychicHttpServer *server,
|
||||
SecurityManager *securityManager,
|
||||
APSettingsService *apSettingsService) : _server(server),
|
||||
_securityManager(securityManager),
|
||||
_apSettingsService(apSettingsService)
|
||||
{
|
||||
}
|
||||
void APStatus::begin()
|
||||
{
|
||||
_server->on(AP_STATUS_SERVICE_PATH,
|
||||
HTTP_GET,
|
||||
_securityManager->wrapRequest(std::bind(&APStatus::apStatus, this, std::placeholders::_1),
|
||||
AuthenticationPredicates::IS_AUTHENTICATED));
|
||||
|
||||
ESP_LOGV("APStatus", "Registered GET endpoint: %s", AP_STATUS_SERVICE_PATH);
|
||||
}
|
||||
|
||||
esp_err_t APStatus::apStatus(PsychicRequest *request)
|
||||
{
|
||||
PsychicJsonResponse response = PsychicJsonResponse(request, false, MAX_AP_STATUS_SIZE);
|
||||
JsonObject root = response.getRoot();
|
||||
|
||||
root["status"] = _apSettingsService->getAPNetworkStatus();
|
||||
root["ip_address"] = WiFi.softAPIP().toString();
|
||||
root["mac_address"] = WiFi.softAPmacAddress();
|
||||
root["station_num"] = WiFi.softAPgetStationNum();
|
||||
|
||||
return response.send();
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
#ifndef APStatus_h
|
||||
#define APStatus_h
|
||||
|
||||
/**
|
||||
* ESP32 SvelteKit
|
||||
*
|
||||
* A simple, secure and extensible framework for IoT projects for ESP32 platforms
|
||||
* with responsive Sveltekit front-end built with TailwindCSS and DaisyUI.
|
||||
* https://github.com/theelims/ESP32-sveltekit
|
||||
*
|
||||
* Copyright (C) 2018 - 2023 rjwats
|
||||
* Copyright (C) 2023 theelims
|
||||
*
|
||||
* All Rights Reserved. This software may be modified and distributed under
|
||||
* the terms of the LGPL v3 license. See the LICENSE file for details.
|
||||
**/
|
||||
|
||||
#include <WiFi.h>
|
||||
|
||||
#include <ArduinoJson.h>
|
||||
#include <PsychicHttp.h>
|
||||
#include <IPAddress.h>
|
||||
#include <SecurityManager.h>
|
||||
#include <APSettingsService.h>
|
||||
|
||||
#define MAX_AP_STATUS_SIZE 1024
|
||||
#define AP_STATUS_SERVICE_PATH "/rest/apStatus"
|
||||
|
||||
class APStatus
|
||||
{
|
||||
public:
|
||||
APStatus(PsychicHttpServer *server, SecurityManager *securityManager, APSettingsService *apSettingsService);
|
||||
|
||||
void begin();
|
||||
|
||||
private:
|
||||
PsychicHttpServer *_server;
|
||||
SecurityManager *_securityManager;
|
||||
APSettingsService *_apSettingsService;
|
||||
esp_err_t apStatus(PsychicRequest *request);
|
||||
};
|
||||
|
||||
#endif // end APStatus_h
|
||||
@@ -0,0 +1,72 @@
|
||||
#pragma once
|
||||
|
||||
/**
|
||||
* ESP32 SvelteKit
|
||||
*
|
||||
* A simple, secure and extensible framework for IoT projects for ESP32 platforms
|
||||
* with responsive Sveltekit front-end built with TailwindCSS and DaisyUI.
|
||||
* https://github.com/theelims/ESP32-sveltekit
|
||||
*
|
||||
* Copyright (C) 2023 theelims
|
||||
*
|
||||
* All Rights Reserved. This software may be modified and distributed under
|
||||
* the terms of the LGPL v3 license. See the LICENSE file for details.
|
||||
**/
|
||||
|
||||
#include <WiFi.h>
|
||||
#include <ArduinoJson.h>
|
||||
#include <ESPFS.h>
|
||||
#include <EventSocket.h>
|
||||
|
||||
#define MAX_ESP_ANALYTICS_SIZE 1024
|
||||
#define EVENT_ANALYTICS "analytics"
|
||||
#define ANALYTICS_INTERVAL 2000
|
||||
|
||||
class AnalyticsService
|
||||
{
|
||||
public:
|
||||
AnalyticsService(EventSocket *socket) : _socket(socket){};
|
||||
|
||||
void begin()
|
||||
{
|
||||
_socket->registerEvent(EVENT_ANALYTICS);
|
||||
|
||||
xTaskCreatePinnedToCore(
|
||||
this->_loopImpl, // Function that should be called
|
||||
"Analytics Service", // Name of the task (for debugging)
|
||||
5120, // Stack size (bytes)
|
||||
this, // Pass reference to this class instance
|
||||
(tskIDLE_PRIORITY), // task priority
|
||||
NULL, // Task handle
|
||||
ESP32SVELTEKIT_RUNNING_CORE // Pin to application core
|
||||
);
|
||||
};
|
||||
|
||||
protected:
|
||||
EventSocket *_socket;
|
||||
|
||||
static void _loopImpl(void *_this) { static_cast<AnalyticsService *>(_this)->_loop(); }
|
||||
void _loop()
|
||||
{
|
||||
TickType_t xLastWakeTime = xTaskGetTickCount();
|
||||
StaticJsonDocument<MAX_ESP_ANALYTICS_SIZE> doc;
|
||||
char message[MAX_ESP_ANALYTICS_SIZE];
|
||||
while (1)
|
||||
{
|
||||
doc.clear();
|
||||
doc["uptime"] = millis() / 1000;
|
||||
doc["free_heap"] = ESP.getFreeHeap();
|
||||
doc["total_heap"] = ESP.getHeapSize();
|
||||
doc["min_free_heap"] = ESP.getMinFreeHeap();
|
||||
doc["max_alloc_heap"] = ESP.getMaxAllocHeap();
|
||||
doc["fs_used"] = ESPFS.usedBytes();
|
||||
doc["fs_total"] = ESPFS.totalBytes();
|
||||
doc["core_temp"] = temperatureRead();
|
||||
|
||||
serializeJson(doc, message);
|
||||
_socket->emit(EVENT_ANALYTICS, message);
|
||||
|
||||
vTaskDelayUntil(&xLastWakeTime, ANALYTICS_INTERVAL / portTICK_PERIOD_MS);
|
||||
}
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,158 @@
|
||||
/**
|
||||
* ESP32 SvelteKit
|
||||
*
|
||||
* A simple, secure and extensible framework for IoT projects for ESP32 platforms
|
||||
* with responsive Sveltekit front-end built with TailwindCSS and DaisyUI.
|
||||
* https://github.com/theelims/ESP32-sveltekit
|
||||
*
|
||||
* Copyright (C) 2018 - 2023 rjwats
|
||||
* Copyright (C) 2023 theelims
|
||||
*
|
||||
* All Rights Reserved. This software may be modified and distributed under
|
||||
* the terms of the LGPL v3 license. See the LICENSE file for details.
|
||||
**/
|
||||
|
||||
#include "ArduinoJsonJWT.h"
|
||||
|
||||
ArduinoJsonJWT::ArduinoJsonJWT(String secret) : _secret(secret)
|
||||
{
|
||||
}
|
||||
|
||||
void ArduinoJsonJWT::setSecret(String secret)
|
||||
{
|
||||
_secret = secret;
|
||||
}
|
||||
|
||||
String ArduinoJsonJWT::getSecret()
|
||||
{
|
||||
return _secret;
|
||||
}
|
||||
|
||||
/*
|
||||
* ESP32 uses mbedtls,
|
||||
*
|
||||
* Both come with decent HMAC implmentations supporting sha256, as well as others.
|
||||
*
|
||||
* No need to pull in additional crypto libraries - lets use what we already have.
|
||||
*/
|
||||
String ArduinoJsonJWT::sign(String &payload)
|
||||
{
|
||||
unsigned char hmacResult[32];
|
||||
{
|
||||
mbedtls_md_context_t ctx;
|
||||
mbedtls_md_type_t md_type = MBEDTLS_MD_SHA256;
|
||||
mbedtls_md_init(&ctx);
|
||||
mbedtls_md_setup(&ctx, mbedtls_md_info_from_type(md_type), 1);
|
||||
mbedtls_md_hmac_starts(&ctx, (unsigned char *)_secret.c_str(), _secret.length());
|
||||
mbedtls_md_hmac_update(&ctx, (unsigned char *)payload.c_str(), payload.length());
|
||||
mbedtls_md_hmac_finish(&ctx, hmacResult);
|
||||
mbedtls_md_free(&ctx);
|
||||
}
|
||||
return encode((char *)hmacResult, 32);
|
||||
}
|
||||
|
||||
String ArduinoJsonJWT::buildJWT(JsonObject &payload)
|
||||
{
|
||||
// serialize, then encode payload
|
||||
String jwt;
|
||||
serializeJson(payload, jwt);
|
||||
jwt = encode(jwt.c_str(), jwt.length());
|
||||
|
||||
// add the header to payload
|
||||
jwt = JWT_HEADER + '.' + jwt;
|
||||
|
||||
// add signature
|
||||
jwt += '.' + sign(jwt);
|
||||
|
||||
return jwt;
|
||||
}
|
||||
|
||||
void ArduinoJsonJWT::parseJWT(String jwt, JsonDocument &jsonDocument)
|
||||
{
|
||||
// clear json document before we begin, jsonDocument wil be null on failure
|
||||
jsonDocument.clear();
|
||||
|
||||
// must have the correct header and delimiter
|
||||
if (!jwt.startsWith(JWT_HEADER) || jwt.indexOf('.') != JWT_HEADER_SIZE)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// check there is a signature delimieter
|
||||
int signatureDelimiterIndex = jwt.lastIndexOf('.');
|
||||
if (signatureDelimiterIndex == JWT_HEADER_SIZE)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// check the signature is valid
|
||||
String signature = jwt.substring(signatureDelimiterIndex + 1);
|
||||
jwt = jwt.substring(0, signatureDelimiterIndex);
|
||||
if (sign(jwt) != signature)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// decode payload
|
||||
jwt = jwt.substring(JWT_HEADER_SIZE + 1);
|
||||
jwt = decode(jwt);
|
||||
|
||||
// parse payload, clearing json document after failure
|
||||
DeserializationError error = deserializeJson(jsonDocument, jwt);
|
||||
if (error != DeserializationError::Ok || !jsonDocument.is<JsonObject>())
|
||||
{
|
||||
jsonDocument.clear();
|
||||
}
|
||||
}
|
||||
|
||||
String ArduinoJsonJWT::encode(const char *cstr, int inputLen)
|
||||
{
|
||||
// prepare encoder
|
||||
base64_encodestate _state;
|
||||
base64_init_encodestate(&_state);
|
||||
size_t encodedLength = base64_encode_expected_len(inputLen) + 1;
|
||||
// prepare buffer of correct length, returning an empty string on failure
|
||||
char *buffer = (char *)malloc(encodedLength * sizeof(char));
|
||||
if (buffer == nullptr)
|
||||
{
|
||||
return "";
|
||||
}
|
||||
|
||||
// encode to buffer
|
||||
int len = base64_encode_block(cstr, inputLen, &buffer[0], &_state);
|
||||
len += base64_encode_blockend(&buffer[len], &_state);
|
||||
buffer[len] = 0;
|
||||
|
||||
// convert to arduino string, freeing buffer
|
||||
String value = String(buffer);
|
||||
free(buffer);
|
||||
buffer = nullptr;
|
||||
|
||||
// remove padding and convert to URL safe form
|
||||
while (value.length() > 0 && value.charAt(value.length() - 1) == '=')
|
||||
{
|
||||
value.remove(value.length() - 1);
|
||||
}
|
||||
value.replace('+', '-');
|
||||
value.replace('/', '_');
|
||||
|
||||
// return as string
|
||||
return value;
|
||||
}
|
||||
|
||||
String ArduinoJsonJWT::decode(String value)
|
||||
{
|
||||
// convert to standard base64
|
||||
value.replace('-', '+');
|
||||
value.replace('_', '/');
|
||||
|
||||
// prepare buffer of correct length
|
||||
char buffer[base64_decode_expected_len(value.length()) + 1];
|
||||
|
||||
// decode
|
||||
int len = base64_decode_chars(value.c_str(), value.length(), &buffer[0]);
|
||||
buffer[len] = 0;
|
||||
|
||||
// return as string
|
||||
return String(buffer);
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
#ifndef ArduinoJsonJWT_H
|
||||
#define ArduinoJsonJWT_H
|
||||
|
||||
/**
|
||||
* ESP32 SvelteKit
|
||||
*
|
||||
* A simple, secure and extensible framework for IoT projects for ESP32 platforms
|
||||
* with responsive Sveltekit front-end built with TailwindCSS and DaisyUI.
|
||||
* https://github.com/theelims/ESP32-sveltekit
|
||||
*
|
||||
* Copyright (C) 2018 - 2023 rjwats
|
||||
* Copyright (C) 2023 theelims
|
||||
*
|
||||
* All Rights Reserved. This software may be modified and distributed under
|
||||
* the terms of the LGPL v3 license. See the LICENSE file for details.
|
||||
**/
|
||||
|
||||
#include <Arduino.h>
|
||||
#include <ArduinoJson.h>
|
||||
#include <libb64/cdecode.h>
|
||||
#include <libb64/cencode.h>
|
||||
#include <mbedtls/md.h>
|
||||
|
||||
class ArduinoJsonJWT
|
||||
{
|
||||
private:
|
||||
String _secret;
|
||||
|
||||
const String JWT_HEADER = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9";
|
||||
const int JWT_HEADER_SIZE = JWT_HEADER.length();
|
||||
|
||||
String sign(String &value);
|
||||
|
||||
static String encode(const char *cstr, int len);
|
||||
static String decode(String value);
|
||||
|
||||
public:
|
||||
ArduinoJsonJWT(String secret);
|
||||
|
||||
void setSecret(String secret);
|
||||
String getSecret();
|
||||
|
||||
String buildJWT(JsonObject &payload);
|
||||
void parseJWT(String jwt, JsonDocument &jsonDocument);
|
||||
};
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,53 @@
|
||||
/**
|
||||
* ESP32 SvelteKit
|
||||
*
|
||||
* A simple, secure and extensible framework for IoT projects for ESP32 platforms
|
||||
* with responsive Sveltekit front-end built with TailwindCSS and DaisyUI.
|
||||
* https://github.com/theelims/ESP32-sveltekit
|
||||
*
|
||||
* Copyright (C) 2018 - 2023 rjwats
|
||||
* Copyright (C) 2023 theelims
|
||||
*
|
||||
* All Rights Reserved. This software may be modified and distributed under
|
||||
* the terms of the LGPL v3 license. See the LICENSE file for details.
|
||||
**/
|
||||
|
||||
#include <AuthenticationService.h>
|
||||
|
||||
#if FT_ENABLED(FT_SECURITY)
|
||||
|
||||
AuthenticationService::AuthenticationService(PsychicHttpServer *server, SecurityManager *securityManager) : _server(server),
|
||||
_securityManager(securityManager)
|
||||
{
|
||||
}
|
||||
|
||||
void AuthenticationService::begin()
|
||||
{
|
||||
// Signs in a user if the username and password match. Provides a JWT to be used in the Authorization header in subsequent requests
|
||||
_server->on(SIGN_IN_PATH, HTTP_POST, [this](PsychicRequest *request, JsonVariant &json)
|
||||
{
|
||||
if (json.is<JsonObject>()) {
|
||||
String username = json["username"];
|
||||
String password = json["password"];
|
||||
Authentication authentication = _securityManager->authenticate(username, password);
|
||||
if (authentication.authenticated) {
|
||||
PsychicJsonResponse response = PsychicJsonResponse(request, false, 256);
|
||||
JsonObject root = response.getRoot();
|
||||
root["access_token"] = _securityManager->generateJWT(authentication.user);
|
||||
return response.send();
|
||||
}
|
||||
}
|
||||
return request->reply(401); });
|
||||
|
||||
ESP_LOGV("AuthenticationService", "Registered POST endpoint: %s", SIGN_IN_PATH);
|
||||
|
||||
// Verifies that the request supplied a valid JWT
|
||||
_server->on(VERIFY_AUTHORIZATION_PATH, HTTP_GET, [this](PsychicRequest *request)
|
||||
{
|
||||
Authentication authentication = _securityManager->authenticateRequest(request);
|
||||
return request->reply(authentication.authenticated ? 200 : 401); });
|
||||
|
||||
ESP_LOGV("AuthenticationService", "Registered GET endpoint: %s", VERIFY_AUTHORIZATION_PATH);
|
||||
}
|
||||
|
||||
#endif // end FT_ENABLED(FT_SECURITY)
|
||||
@@ -0,0 +1,42 @@
|
||||
#ifndef AuthenticationService_H_
|
||||
#define AuthenticationService_H_
|
||||
|
||||
/**
|
||||
* ESP32 SvelteKit
|
||||
*
|
||||
* A simple, secure and extensible framework for IoT projects for ESP32 platforms
|
||||
* with responsive Sveltekit front-end built with TailwindCSS and DaisyUI.
|
||||
* https://github.com/theelims/ESP32-sveltekit
|
||||
*
|
||||
* Copyright (C) 2018 - 2023 rjwats
|
||||
* Copyright (C) 2023 theelims
|
||||
*
|
||||
* All Rights Reserved. This software may be modified and distributed under
|
||||
* the terms of the LGPL v3 license. See the LICENSE file for details.
|
||||
**/
|
||||
|
||||
#include <Features.h>
|
||||
#include <PsychicHttp.h>
|
||||
#include <SecurityManager.h>
|
||||
|
||||
#define VERIFY_AUTHORIZATION_PATH "/rest/verifyAuthorization"
|
||||
#define SIGN_IN_PATH "/rest/signIn"
|
||||
|
||||
#define MAX_AUTHENTICATION_SIZE 256
|
||||
|
||||
#if FT_ENABLED(FT_SECURITY)
|
||||
|
||||
class AuthenticationService
|
||||
{
|
||||
public:
|
||||
AuthenticationService(PsychicHttpServer *server, SecurityManager *securityManager);
|
||||
|
||||
void begin();
|
||||
|
||||
private:
|
||||
SecurityManager *_securityManager;
|
||||
PsychicHttpServer *_server;
|
||||
};
|
||||
|
||||
#endif // end FT_ENABLED(FT_SECURITY)
|
||||
#endif // end SecurityManager_h
|
||||
@@ -0,0 +1,33 @@
|
||||
/**
|
||||
* ESP32 SvelteKit
|
||||
*
|
||||
* A simple, secure and extensible framework for IoT projects for ESP32 platforms
|
||||
* with responsive Sveltekit front-end built with TailwindCSS and DaisyUI.
|
||||
* https://github.com/theelims/ESP32-sveltekit
|
||||
*
|
||||
* Copyright (C) 2023 theelims
|
||||
*
|
||||
* All Rights Reserved. This software may be modified and distributed under
|
||||
* the terms of the LGPL v3 license. See the LICENSE file for details.
|
||||
**/
|
||||
|
||||
#include <BatteryService.h>
|
||||
|
||||
BatteryService::BatteryService(EventSocket *socket) : _socket(socket)
|
||||
{
|
||||
}
|
||||
|
||||
void BatteryService::begin()
|
||||
{
|
||||
_socket->registerEvent(EVENT_BATTERY);
|
||||
}
|
||||
|
||||
void BatteryService::batteryEvent()
|
||||
{
|
||||
StaticJsonDocument<32> doc;
|
||||
char message[32];
|
||||
doc["soc"] = _lastSOC;
|
||||
doc["charging"] = _isCharging;
|
||||
serializeJson(doc, message);
|
||||
_socket->emit(EVENT_BATTERY, message);
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
#pragma once
|
||||
|
||||
/**
|
||||
* ESP32 SvelteKit
|
||||
*
|
||||
* A simple, secure and extensible framework for IoT projects for ESP32 platforms
|
||||
* with responsive Sveltekit front-end built with TailwindCSS and DaisyUI.
|
||||
* https://github.com/theelims/ESP32-sveltekit
|
||||
*
|
||||
* Copyright (C) 2023 theelims
|
||||
*
|
||||
* All Rights Reserved. This software may be modified and distributed under
|
||||
* the terms of the LGPL v3 license. See the LICENSE file for details.
|
||||
**/
|
||||
|
||||
#include <EventSocket.h>
|
||||
#include <JsonUtils.h>
|
||||
|
||||
#define EVENT_BATTERY "battery"
|
||||
|
||||
class BatteryService
|
||||
{
|
||||
public:
|
||||
BatteryService(EventSocket *socket);
|
||||
|
||||
void begin();
|
||||
|
||||
void updateSOC(float stateOfCharge)
|
||||
{
|
||||
_lastSOC = (int)round(stateOfCharge);
|
||||
batteryEvent();
|
||||
}
|
||||
|
||||
void setCharging(boolean isCharging)
|
||||
{
|
||||
_isCharging = isCharging;
|
||||
batteryEvent();
|
||||
}
|
||||
|
||||
private:
|
||||
void batteryEvent();
|
||||
EventSocket *_socket;
|
||||
int _lastSOC = 100;
|
||||
boolean _isCharging = false;
|
||||
};
|
||||
@@ -0,0 +1,297 @@
|
||||
#if defined(CAMERA_MODEL_WROVER_KIT)
|
||||
#define PWDN_GPIO_NUM -1
|
||||
#define RESET_GPIO_NUM -1
|
||||
#define XCLK_GPIO_NUM 21
|
||||
#define SIOD_GPIO_NUM 26
|
||||
#define SIOC_GPIO_NUM 27
|
||||
|
||||
#define Y9_GPIO_NUM 35
|
||||
#define Y8_GPIO_NUM 34
|
||||
#define Y7_GPIO_NUM 39
|
||||
#define Y6_GPIO_NUM 36
|
||||
#define Y5_GPIO_NUM 19
|
||||
#define Y4_GPIO_NUM 18
|
||||
#define Y3_GPIO_NUM 5
|
||||
#define Y2_GPIO_NUM 4
|
||||
#define VSYNC_GPIO_NUM 25
|
||||
#define HREF_GPIO_NUM 23
|
||||
#define PCLK_GPIO_NUM 22
|
||||
|
||||
#elif defined(CAMERA_MODEL_ESP_EYE)
|
||||
#define PWDN_GPIO_NUM -1
|
||||
#define RESET_GPIO_NUM -1
|
||||
#define XCLK_GPIO_NUM 4
|
||||
#define SIOD_GPIO_NUM 18
|
||||
#define SIOC_GPIO_NUM 23
|
||||
|
||||
#define Y9_GPIO_NUM 36
|
||||
#define Y8_GPIO_NUM 37
|
||||
#define Y7_GPIO_NUM 38
|
||||
#define Y6_GPIO_NUM 39
|
||||
#define Y5_GPIO_NUM 35
|
||||
#define Y4_GPIO_NUM 14
|
||||
#define Y3_GPIO_NUM 13
|
||||
#define Y2_GPIO_NUM 34
|
||||
#define VSYNC_GPIO_NUM 5
|
||||
#define HREF_GPIO_NUM 27
|
||||
#define PCLK_GPIO_NUM 25
|
||||
|
||||
#define LED_GPIO_NUM 22
|
||||
|
||||
#elif defined(CAMERA_MODEL_M5STACK_PSRAM)
|
||||
#define PWDN_GPIO_NUM -1
|
||||
#define RESET_GPIO_NUM 15
|
||||
#define XCLK_GPIO_NUM 27
|
||||
#define SIOD_GPIO_NUM 25
|
||||
#define SIOC_GPIO_NUM 23
|
||||
|
||||
#define Y9_GPIO_NUM 19
|
||||
#define Y8_GPIO_NUM 36
|
||||
#define Y7_GPIO_NUM 18
|
||||
#define Y6_GPIO_NUM 39
|
||||
#define Y5_GPIO_NUM 5
|
||||
#define Y4_GPIO_NUM 34
|
||||
#define Y3_GPIO_NUM 35
|
||||
#define Y2_GPIO_NUM 32
|
||||
#define VSYNC_GPIO_NUM 22
|
||||
#define HREF_GPIO_NUM 26
|
||||
#define PCLK_GPIO_NUM 21
|
||||
|
||||
#elif defined(CAMERA_MODEL_M5STACK_V2_PSRAM)
|
||||
#define PWDN_GPIO_NUM -1
|
||||
#define RESET_GPIO_NUM 15
|
||||
#define XCLK_GPIO_NUM 27
|
||||
#define SIOD_GPIO_NUM 22
|
||||
#define SIOC_GPIO_NUM 23
|
||||
|
||||
#define Y9_GPIO_NUM 19
|
||||
#define Y8_GPIO_NUM 36
|
||||
#define Y7_GPIO_NUM 18
|
||||
#define Y6_GPIO_NUM 39
|
||||
#define Y5_GPIO_NUM 5
|
||||
#define Y4_GPIO_NUM 34
|
||||
#define Y3_GPIO_NUM 35
|
||||
#define Y2_GPIO_NUM 32
|
||||
#define VSYNC_GPIO_NUM 25
|
||||
#define HREF_GPIO_NUM 26
|
||||
#define PCLK_GPIO_NUM 21
|
||||
|
||||
#elif defined(CAMERA_MODEL_M5STACK_WIDE)
|
||||
#define PWDN_GPIO_NUM -1
|
||||
#define RESET_GPIO_NUM 15
|
||||
#define XCLK_GPIO_NUM 27
|
||||
#define SIOD_GPIO_NUM 22
|
||||
#define SIOC_GPIO_NUM 23
|
||||
|
||||
#define Y9_GPIO_NUM 19
|
||||
#define Y8_GPIO_NUM 36
|
||||
#define Y7_GPIO_NUM 18
|
||||
#define Y6_GPIO_NUM 39
|
||||
#define Y5_GPIO_NUM 5
|
||||
#define Y4_GPIO_NUM 34
|
||||
#define Y3_GPIO_NUM 35
|
||||
#define Y2_GPIO_NUM 32
|
||||
#define VSYNC_GPIO_NUM 25
|
||||
#define HREF_GPIO_NUM 26
|
||||
#define PCLK_GPIO_NUM 21
|
||||
|
||||
#define LED_GPIO_NUM 2
|
||||
|
||||
#elif defined(CAMERA_MODEL_M5STACK_ESP32CAM)
|
||||
#define PWDN_GPIO_NUM -1
|
||||
#define RESET_GPIO_NUM 15
|
||||
#define XCLK_GPIO_NUM 27
|
||||
#define SIOD_GPIO_NUM 25
|
||||
#define SIOC_GPIO_NUM 23
|
||||
|
||||
#define Y9_GPIO_NUM 19
|
||||
#define Y8_GPIO_NUM 36
|
||||
#define Y7_GPIO_NUM 18
|
||||
#define Y6_GPIO_NUM 39
|
||||
#define Y5_GPIO_NUM 5
|
||||
#define Y4_GPIO_NUM 34
|
||||
#define Y3_GPIO_NUM 35
|
||||
#define Y2_GPIO_NUM 17
|
||||
#define VSYNC_GPIO_NUM 22
|
||||
#define HREF_GPIO_NUM 26
|
||||
#define PCLK_GPIO_NUM 21
|
||||
|
||||
#elif defined(CAMERA_MODEL_M5STACK_UNITCAM)
|
||||
#define PWDN_GPIO_NUM -1
|
||||
#define RESET_GPIO_NUM 15
|
||||
#define XCLK_GPIO_NUM 27
|
||||
#define SIOD_GPIO_NUM 25
|
||||
#define SIOC_GPIO_NUM 23
|
||||
|
||||
#define Y9_GPIO_NUM 19
|
||||
#define Y8_GPIO_NUM 36
|
||||
#define Y7_GPIO_NUM 18
|
||||
#define Y6_GPIO_NUM 39
|
||||
#define Y5_GPIO_NUM 5
|
||||
#define Y4_GPIO_NUM 34
|
||||
#define Y3_GPIO_NUM 35
|
||||
#define Y2_GPIO_NUM 32
|
||||
#define VSYNC_GPIO_NUM 22
|
||||
#define HREF_GPIO_NUM 26
|
||||
#define PCLK_GPIO_NUM 21
|
||||
|
||||
#elif defined(CAMERA_MODEL_AI_THINKER)
|
||||
#define PWDN_GPIO_NUM 32
|
||||
#define RESET_GPIO_NUM -1
|
||||
#define XCLK_GPIO_NUM 0
|
||||
#define SIOD_GPIO_NUM 26
|
||||
#define SIOC_GPIO_NUM 27
|
||||
|
||||
#define Y9_GPIO_NUM 35
|
||||
#define Y8_GPIO_NUM 34
|
||||
#define Y7_GPIO_NUM 39
|
||||
#define Y6_GPIO_NUM 36
|
||||
#define Y5_GPIO_NUM 21
|
||||
#define Y4_GPIO_NUM 19
|
||||
#define Y3_GPIO_NUM 18
|
||||
#define Y2_GPIO_NUM 5
|
||||
#define VSYNC_GPIO_NUM 25
|
||||
#define HREF_GPIO_NUM 23
|
||||
#define PCLK_GPIO_NUM 22
|
||||
|
||||
// 4 for flash led or 33 for normal led
|
||||
#define LED_GPIO_NUM 4
|
||||
|
||||
#elif defined(CAMERA_MODEL_TTGO_T_JOURNAL)
|
||||
#define PWDN_GPIO_NUM 0
|
||||
#define RESET_GPIO_NUM 15
|
||||
#define XCLK_GPIO_NUM 27
|
||||
#define SIOD_GPIO_NUM 25
|
||||
#define SIOC_GPIO_NUM 23
|
||||
|
||||
#define Y9_GPIO_NUM 19
|
||||
#define Y8_GPIO_NUM 36
|
||||
#define Y7_GPIO_NUM 18
|
||||
#define Y6_GPIO_NUM 39
|
||||
#define Y5_GPIO_NUM 5
|
||||
#define Y4_GPIO_NUM 34
|
||||
#define Y3_GPIO_NUM 35
|
||||
#define Y2_GPIO_NUM 17
|
||||
#define VSYNC_GPIO_NUM 22
|
||||
#define HREF_GPIO_NUM 26
|
||||
#define PCLK_GPIO_NUM 21
|
||||
|
||||
#elif defined(CAMERA_MODEL_XIAO_ESP32S3)
|
||||
#define PWDN_GPIO_NUM -1
|
||||
#define RESET_GPIO_NUM -1
|
||||
#define XCLK_GPIO_NUM 10
|
||||
#define SIOD_GPIO_NUM 40
|
||||
#define SIOC_GPIO_NUM 39
|
||||
|
||||
#define Y9_GPIO_NUM 48
|
||||
#define Y8_GPIO_NUM 11
|
||||
#define Y7_GPIO_NUM 12
|
||||
#define Y6_GPIO_NUM 14
|
||||
#define Y5_GPIO_NUM 16
|
||||
#define Y4_GPIO_NUM 18
|
||||
#define Y3_GPIO_NUM 17
|
||||
#define Y2_GPIO_NUM 15
|
||||
#define VSYNC_GPIO_NUM 38
|
||||
#define HREF_GPIO_NUM 47
|
||||
#define PCLK_GPIO_NUM 13
|
||||
|
||||
#elif defined(CAMERA_MODEL_ESP32_CAM_BOARD)
|
||||
// The 18 pin header on the board has Y5 and Y3 swapped
|
||||
#define USE_BOARD_HEADER 0
|
||||
#define PWDN_GPIO_NUM 32
|
||||
#define RESET_GPIO_NUM 33
|
||||
#define XCLK_GPIO_NUM 4
|
||||
#define SIOD_GPIO_NUM 18
|
||||
#define SIOC_GPIO_NUM 23
|
||||
|
||||
#define Y9_GPIO_NUM 36
|
||||
#define Y8_GPIO_NUM 19
|
||||
#define Y7_GPIO_NUM 21
|
||||
#define Y6_GPIO_NUM 39
|
||||
#if USE_BOARD_HEADER
|
||||
#define Y5_GPIO_NUM 13
|
||||
#else
|
||||
#define Y5_GPIO_NUM 35
|
||||
#endif
|
||||
#define Y4_GPIO_NUM 14
|
||||
#if USE_BOARD_HEADER
|
||||
#define Y3_GPIO_NUM 35
|
||||
#else
|
||||
#define Y3_GPIO_NUM 13
|
||||
#endif
|
||||
#define Y2_GPIO_NUM 34
|
||||
#define VSYNC_GPIO_NUM 5
|
||||
#define HREF_GPIO_NUM 27
|
||||
#define PCLK_GPIO_NUM 25
|
||||
|
||||
#elif defined(CAMERA_MODEL_ESP32S3_CAM_LCD)
|
||||
#define PWDN_GPIO_NUM -1
|
||||
#define RESET_GPIO_NUM -1
|
||||
#define XCLK_GPIO_NUM 40
|
||||
#define SIOD_GPIO_NUM 17
|
||||
#define SIOC_GPIO_NUM 18
|
||||
|
||||
#define Y9_GPIO_NUM 39
|
||||
#define Y8_GPIO_NUM 41
|
||||
#define Y7_GPIO_NUM 42
|
||||
#define Y6_GPIO_NUM 12
|
||||
#define Y5_GPIO_NUM 3
|
||||
#define Y4_GPIO_NUM 14
|
||||
#define Y3_GPIO_NUM 47
|
||||
#define Y2_GPIO_NUM 13
|
||||
#define VSYNC_GPIO_NUM 21
|
||||
#define HREF_GPIO_NUM 38
|
||||
#define PCLK_GPIO_NUM 11
|
||||
|
||||
#elif defined(CAMERA_MODEL_ESP32S2_CAM_BOARD)
|
||||
// The 18 pin header on the board has Y5 and Y3 swapped
|
||||
#define USE_BOARD_HEADER 0
|
||||
#define PWDN_GPIO_NUM 1
|
||||
#define RESET_GPIO_NUM 2
|
||||
#define XCLK_GPIO_NUM 42
|
||||
#define SIOD_GPIO_NUM 41
|
||||
#define SIOC_GPIO_NUM 18
|
||||
|
||||
#define Y9_GPIO_NUM 16
|
||||
#define Y8_GPIO_NUM 39
|
||||
#define Y7_GPIO_NUM 40
|
||||
#define Y6_GPIO_NUM 15
|
||||
#if USE_BOARD_HEADER
|
||||
#define Y5_GPIO_NUM 12
|
||||
#else
|
||||
#define Y5_GPIO_NUM 13
|
||||
#endif
|
||||
#define Y4_GPIO_NUM 5
|
||||
#if USE_BOARD_HEADER
|
||||
#define Y3_GPIO_NUM 13
|
||||
#else
|
||||
#define Y3_GPIO_NUM 12
|
||||
#endif
|
||||
#define Y2_GPIO_NUM 14
|
||||
#define VSYNC_GPIO_NUM 38
|
||||
#define HREF_GPIO_NUM 4
|
||||
#define PCLK_GPIO_NUM 3
|
||||
|
||||
#elif defined(CAMERA_MODEL_ESP32S3_EYE)
|
||||
#define PWDN_GPIO_NUM -1
|
||||
#define RESET_GPIO_NUM -1
|
||||
#define XCLK_GPIO_NUM 15
|
||||
#define SIOD_GPIO_NUM 4
|
||||
#define SIOC_GPIO_NUM 5
|
||||
|
||||
#define Y2_GPIO_NUM 11
|
||||
#define Y3_GPIO_NUM 9
|
||||
#define Y4_GPIO_NUM 8
|
||||
#define Y5_GPIO_NUM 10
|
||||
#define Y6_GPIO_NUM 12
|
||||
#define Y7_GPIO_NUM 18
|
||||
#define Y8_GPIO_NUM 17
|
||||
#define Y9_GPIO_NUM 16
|
||||
|
||||
#define VSYNC_GPIO_NUM 6
|
||||
#define HREF_GPIO_NUM 7
|
||||
#define PCLK_GPIO_NUM 13
|
||||
|
||||
#else
|
||||
#error "Camera model not selected"
|
||||
#endif
|
||||
@@ -0,0 +1,165 @@
|
||||
/**
|
||||
* ESP32 SvelteKit
|
||||
*
|
||||
* A simple, secure and extensible framework for IoT projects for ESP32 platforms
|
||||
* with responsive Sveltekit front-end built with TailwindCSS and DaisyUI.
|
||||
* https://github.com/theelims/ESP32-sveltekit
|
||||
*
|
||||
* Copyright (C) 2023 theelims
|
||||
*
|
||||
* All Rights Reserved. This software may be modified and distributed under
|
||||
* the terms of the LGPL v3 license. See the LICENSE file for details.
|
||||
**/
|
||||
|
||||
#include <DownloadFirmwareService.h>
|
||||
|
||||
extern const uint8_t rootca_crt_bundle_start[] asm("_binary_src_certs_x509_crt_bundle_bin_start");
|
||||
|
||||
static EventSocket *_socket = nullptr;
|
||||
static int previousProgress = 0;
|
||||
StaticJsonDocument<128> doc;
|
||||
|
||||
void update_started()
|
||||
{
|
||||
String output;
|
||||
doc["status"] = "preparing";
|
||||
serializeJson(doc, output);
|
||||
_socket->emit(EVENT_DOWNLOAD_OTA, output.c_str());
|
||||
}
|
||||
|
||||
void update_progress(int currentBytes, int totalBytes)
|
||||
{
|
||||
String output;
|
||||
doc["status"] = "progress";
|
||||
int progress = ((currentBytes * 100) / totalBytes);
|
||||
if (progress > previousProgress)
|
||||
{
|
||||
doc["progress"] = progress;
|
||||
_socket->emit(EVENT_DOWNLOAD_OTA, output.c_str());
|
||||
ESP_LOGV("Download OTA", "HTTP update process at %d of %d bytes... (%d %%)", currentBytes, totalBytes, progress);
|
||||
}
|
||||
previousProgress = progress;
|
||||
}
|
||||
|
||||
void update_finished()
|
||||
{
|
||||
String output;
|
||||
doc["status"] = "finished";
|
||||
serializeJson(doc, output);
|
||||
_socket->emit(EVENT_DOWNLOAD_OTA, output.c_str());
|
||||
|
||||
// delay to allow the event to be sent out
|
||||
vTaskDelay(100 / portTICK_PERIOD_MS);
|
||||
}
|
||||
|
||||
void updateTask(void *param)
|
||||
{
|
||||
WiFiClientSecure client;
|
||||
client.setCACertBundle(rootca_crt_bundle_start);
|
||||
client.setTimeout(10);
|
||||
|
||||
httpUpdate.setFollowRedirects(HTTPC_FORCE_FOLLOW_REDIRECTS);
|
||||
httpUpdate.rebootOnUpdate(true);
|
||||
|
||||
String url = *((String *)param);
|
||||
String output;
|
||||
// httpUpdate.onStart(update_started);
|
||||
// httpUpdate.onProgress(update_progress);
|
||||
// httpUpdate.onEnd(update_finished);
|
||||
|
||||
t_httpUpdate_return ret = httpUpdate.update(client, url.c_str());
|
||||
|
||||
switch (ret)
|
||||
{
|
||||
case HTTP_UPDATE_FAILED:
|
||||
|
||||
doc["status"] = "error";
|
||||
doc["error"] = httpUpdate.getLastErrorString().c_str();
|
||||
serializeJson(doc, output);
|
||||
_socket->emit(EVENT_DOWNLOAD_OTA, output.c_str());
|
||||
|
||||
ESP_LOGE("Download OTA", "HTTP Update failed with error (%d): %s", httpUpdate.getLastError(), httpUpdate.getLastErrorString().c_str());
|
||||
#ifdef SERIAL_INFO
|
||||
Serial.printf("HTTP Update failed with error (%d): %s\n", httpUpdate.getLastError(), httpUpdate.getLastErrorString().c_str());
|
||||
#endif
|
||||
break;
|
||||
case HTTP_UPDATE_NO_UPDATES:
|
||||
|
||||
doc["status"] = "error";
|
||||
doc["error"] = "Update failed, has same firmware version";
|
||||
serializeJson(doc, output);
|
||||
_socket->emit(EVENT_DOWNLOAD_OTA, output.c_str());
|
||||
|
||||
ESP_LOGE("Download OTA", "HTTP Update failed, has same firmware version");
|
||||
#ifdef SERIAL_INFO
|
||||
Serial.println("HTTP Update failed, has same firmware version");
|
||||
#endif
|
||||
break;
|
||||
case HTTP_UPDATE_OK:
|
||||
ESP_LOGI("Download OTA", "HTTP Update successful - Restarting");
|
||||
#ifdef SERIAL_INFO
|
||||
Serial.println("HTTP Update successful - Restarting");
|
||||
#endif
|
||||
break;
|
||||
}
|
||||
vTaskDelete(NULL);
|
||||
}
|
||||
|
||||
DownloadFirmwareService::DownloadFirmwareService(PsychicHttpServer *server,
|
||||
SecurityManager *securityManager,
|
||||
EventSocket *socket) : _server(server),
|
||||
_securityManager(securityManager),
|
||||
_socket(socket)
|
||||
{
|
||||
}
|
||||
|
||||
void DownloadFirmwareService::begin()
|
||||
{
|
||||
_socket->registerEvent(EVENT_DOWNLOAD_OTA);
|
||||
|
||||
_server->on(GITHUB_FIRMWARE_PATH,
|
||||
HTTP_POST,
|
||||
_securityManager->wrapCallback(
|
||||
std::bind(&DownloadFirmwareService::downloadUpdate, this, std::placeholders::_1, std::placeholders::_2),
|
||||
AuthenticationPredicates::IS_ADMIN));
|
||||
|
||||
ESP_LOGV("DownloadFirmwareService", "Registered POST endpoint: %s", GITHUB_FIRMWARE_PATH);
|
||||
}
|
||||
|
||||
esp_err_t DownloadFirmwareService::downloadUpdate(PsychicRequest *request, JsonVariant &json)
|
||||
{
|
||||
if (!json.is<JsonObject>())
|
||||
{
|
||||
return request->reply(400);
|
||||
}
|
||||
|
||||
String downloadURL = json["download_url"];
|
||||
ESP_LOGI("Download OTA", "Starting OTA from: %s", downloadURL.c_str());
|
||||
#ifdef SERIAL_INFO
|
||||
Serial.println("Starting OTA from: " + downloadURL);
|
||||
#endif
|
||||
|
||||
doc["status"] = "preparing";
|
||||
doc["progress"] = 0;
|
||||
doc["error"] = "";
|
||||
|
||||
String output;
|
||||
serializeJson(doc, output);
|
||||
|
||||
_socket->emit(EVENT_DOWNLOAD_OTA, output.c_str());
|
||||
|
||||
if (xTaskCreatePinnedToCore(
|
||||
&updateTask, // Function that should be called
|
||||
"Update", // Name of the task (for debugging)
|
||||
OTA_TASK_STACK_SIZE, // Stack size (bytes)
|
||||
&downloadURL, // Pass reference to this class instance
|
||||
(configMAX_PRIORITIES - 1), // Pretty high task priority
|
||||
NULL, // Task handle
|
||||
1 // Have it on application core
|
||||
) != pdPASS)
|
||||
{
|
||||
ESP_LOGE("Download OTA", "Couldn't create download OTA task");
|
||||
return request->reply(500);
|
||||
}
|
||||
return request->reply(200);
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
#pragma once
|
||||
|
||||
/**
|
||||
* ESP32 SvelteKit
|
||||
*
|
||||
* A simple, secure and extensible framework for IoT projects for ESP32 platforms
|
||||
* with responsive Sveltekit front-end built with TailwindCSS and DaisyUI.
|
||||
* https://github.com/theelims/ESP32-sveltekit
|
||||
*
|
||||
* Copyright (C) 2018 - 2023 rjwats
|
||||
* Copyright (C) 2023 theelims
|
||||
*
|
||||
* All Rights Reserved. This software may be modified and distributed under
|
||||
* the terms of the LGPL v3 license. See the LICENSE file for details.
|
||||
**/
|
||||
|
||||
#include <Arduino.h>
|
||||
|
||||
#include <WiFi.h>
|
||||
#include <ArduinoJson.h>
|
||||
#include <EventSocket.h>
|
||||
#include <PsychicHttp.h>
|
||||
#include <SecurityManager.h>
|
||||
|
||||
#include <HTTPClient.h>
|
||||
#include <HTTPUpdate.h>
|
||||
// #include <SSLCertBundle.h>
|
||||
|
||||
#define GITHUB_FIRMWARE_PATH "/rest/downloadUpdate"
|
||||
#define EVENT_DOWNLOAD_OTA "otastatus"
|
||||
#define OTA_TASK_STACK_SIZE 9216
|
||||
|
||||
class DownloadFirmwareService
|
||||
{
|
||||
public:
|
||||
DownloadFirmwareService(PsychicHttpServer *server, SecurityManager *securityManager, EventSocket *socket);
|
||||
|
||||
void begin();
|
||||
|
||||
private:
|
||||
SecurityManager *_securityManager;
|
||||
PsychicHttpServer *_server;
|
||||
EventSocket *_socket;
|
||||
esp_err_t downloadUpdate(PsychicRequest *request, JsonVariant &json);
|
||||
};
|
||||
@@ -0,0 +1,204 @@
|
||||
/**
|
||||
* ESP32 SvelteKit
|
||||
*
|
||||
* A simple, secure and extensible framework for IoT projects for ESP32 platforms
|
||||
* with responsive Sveltekit front-end built with TailwindCSS and DaisyUI.
|
||||
* https://github.com/theelims/ESP32-sveltekit
|
||||
*
|
||||
* Copyright (C) 2018 - 2023 rjwats
|
||||
* Copyright (C) 2024 theelims
|
||||
*
|
||||
* All Rights Reserved. This software may be modified and distributed under
|
||||
* the terms of the LGPL v3 license. See the LICENSE file for details.
|
||||
**/
|
||||
|
||||
#include <ESP32SvelteKit.h>
|
||||
|
||||
ESP32SvelteKit::ESP32SvelteKit(PsychicHttpServer *server, unsigned int numberEndpoints) : _server(server),
|
||||
_numberEndpoints(numberEndpoints),
|
||||
_featureService(server),
|
||||
_securitySettingsService(server, &ESPFS),
|
||||
_wifiSettingsService(server, &ESPFS, &_securitySettingsService, &_socket),
|
||||
_wifiScanner(server, &_securitySettingsService),
|
||||
_wifiStatus(server, &_securitySettingsService),
|
||||
_apSettingsService(server, &ESPFS, &_securitySettingsService),
|
||||
_apStatus(server, &_securitySettingsService, &_apSettingsService),
|
||||
_socket(server, &_securitySettingsService, AuthenticationPredicates::IS_AUTHENTICATED),
|
||||
#if FT_ENABLED(FT_NTP)
|
||||
_ntpSettingsService(server, &ESPFS, &_securitySettingsService),
|
||||
_ntpStatus(server, &_securitySettingsService),
|
||||
#endif
|
||||
#if FT_ENABLED(FT_UPLOAD_FIRMWARE)
|
||||
_uploadFirmwareService(server, &_securitySettingsService),
|
||||
#endif
|
||||
#if FT_ENABLED(FT_DOWNLOAD_FIRMWARE)
|
||||
_downloadFirmwareService(server, &_securitySettingsService, &_socket),
|
||||
#endif
|
||||
#if FT_ENABLED(FT_MQTT)
|
||||
_mqttSettingsService(server, &ESPFS, &_securitySettingsService),
|
||||
_mqttStatus(server, &_mqttSettingsService, &_securitySettingsService),
|
||||
#endif
|
||||
#if FT_ENABLED(FT_SECURITY)
|
||||
_authenticationService(server, &_securitySettingsService),
|
||||
#endif
|
||||
#if FT_ENABLED(FT_SLEEP)
|
||||
_sleepService(server, &_securitySettingsService),
|
||||
#endif
|
||||
#if FT_ENABLED(FT_BATTERY)
|
||||
_batteryService(&_socket),
|
||||
#endif
|
||||
#if FT_ENABLED(FT_ANALYTICS)
|
||||
_analyticsService(&_socket),
|
||||
#endif
|
||||
_restartService(server, &_securitySettingsService),
|
||||
_factoryResetService(server, &ESPFS, &_securitySettingsService),
|
||||
_systemStatus(server, &_securitySettingsService)
|
||||
{
|
||||
}
|
||||
|
||||
void ESP32SvelteKit::begin()
|
||||
{
|
||||
ESP_LOGV("ESP32SvelteKit", "Loading settings from files system");
|
||||
ESPFS.begin(true);
|
||||
|
||||
_wifiSettingsService.initWiFi();
|
||||
|
||||
// SvelteKit uses a lot of handlers, so we need to increase the max_uri_handlers
|
||||
// WWWData has 77 Endpoints, Framework has 27, and Lighstate Demo has 4
|
||||
_server->config.max_uri_handlers = _numberEndpoints;
|
||||
_server->listen(80);
|
||||
|
||||
#ifdef EMBED_WWW
|
||||
// Serve static resources from PROGMEM
|
||||
ESP_LOGV("ESP32SvelteKit", "Registering routes from PROGMEM static resources");
|
||||
WWWData::registerRoutes(
|
||||
[&](const String &uri, const String &contentType, const uint8_t *content, size_t len)
|
||||
{
|
||||
PsychicHttpRequestCallback requestHandler = [contentType, content, len](PsychicRequest *request)
|
||||
{
|
||||
PsychicResponse response(request);
|
||||
response.setCode(200);
|
||||
response.setContentType(contentType.c_str());
|
||||
response.addHeader("Content-Encoding", "gzip");
|
||||
response.addHeader("Cache-Control", "public, immutable, max-age=31536000");
|
||||
response.setContent(content, len);
|
||||
return response.send();
|
||||
};
|
||||
PsychicWebHandler *handler = new PsychicWebHandler();
|
||||
handler->onRequest(requestHandler);
|
||||
_server->on(uri.c_str(), HTTP_GET, handler);
|
||||
|
||||
// Set default end-point for all non matching requests
|
||||
// this is easier than using webServer.onNotFound()
|
||||
if (uri.equals("/index.html"))
|
||||
{
|
||||
_server->defaultEndpoint->setHandler(handler);
|
||||
}
|
||||
});
|
||||
#else
|
||||
// Serve static resources from /www/
|
||||
ESP_LOGV("ESP32SvelteKit", "Registering routes from FS /www/ static resources");
|
||||
_server->serveStatic("/_app/", ESPFS, "/www/_app/");
|
||||
_server->serveStatic("/favicon.png", ESPFS, "/www/favicon.png");
|
||||
// Serving all other get requests with "/www/index.htm"
|
||||
_server->onNotFound([](PsychicRequest *request)
|
||||
{
|
||||
if (request->method() == HTTP_GET) {
|
||||
PsychicFileResponse response(request, ESPFS, "/www/index.html", "text/html");
|
||||
return response.send();
|
||||
// String url = "http://" + request->host() + "/index.html";
|
||||
// request->redirect(url.c_str());
|
||||
} });
|
||||
#endif
|
||||
#ifdef SERVE_CONFIG_FILES
|
||||
_server->serveStatic("/config/", ESPFS, "/config/");
|
||||
#endif
|
||||
|
||||
// Serve static resources from /config/ if set by platformio.ini
|
||||
#if SERVE_CONFIG_FILES
|
||||
_server->serveStatic("/config/", ESPFS, "/config/");
|
||||
#endif
|
||||
|
||||
#if defined(ENABLE_CORS)
|
||||
ESP_LOGV("ESP32SvelteKit", "Enabling CORS headers");
|
||||
DefaultHeaders::Instance().addHeader("Access-Control-Allow-Origin", CORS_ORIGIN);
|
||||
DefaultHeaders::Instance().addHeader("Access-Control-Allow-Headers", "Accept, Content-Type, Authorization");
|
||||
DefaultHeaders::Instance().addHeader("Access-Control-Allow-Credentials", "true");
|
||||
#endif
|
||||
|
||||
ESP_LOGV("ESP32SvelteKit", "Starting MDNS");
|
||||
MDNS.begin(_wifiSettingsService.getHostname().c_str());
|
||||
MDNS.setInstanceName(_appName);
|
||||
MDNS.addService("http", "tcp", 80);
|
||||
MDNS.addService("ws", "tcp", 80);
|
||||
MDNS.addServiceTxt("http", "tcp", "Firmware Version", APP_VERSION);
|
||||
|
||||
#ifdef SERIAL_INFO
|
||||
Serial.printf("Running Firmware Version: %s\n", APP_VERSION);
|
||||
#endif
|
||||
|
||||
// Start the services
|
||||
_apStatus.begin();
|
||||
_socket.begin();
|
||||
_apSettingsService.begin();
|
||||
_factoryResetService.begin();
|
||||
_featureService.begin();
|
||||
_restartService.begin();
|
||||
_systemStatus.begin();
|
||||
_wifiSettingsService.begin();
|
||||
_wifiScanner.begin();
|
||||
_wifiStatus.begin();
|
||||
|
||||
#if FT_ENABLED(FT_UPLOAD_FIRMWARE)
|
||||
_uploadFirmwareService.begin();
|
||||
#endif
|
||||
#if FT_ENABLED(FT_DOWNLOAD_FIRMWARE)
|
||||
_downloadFirmwareService.begin();
|
||||
#endif
|
||||
#if FT_ENABLED(FT_NTP)
|
||||
_ntpSettingsService.begin();
|
||||
_ntpStatus.begin();
|
||||
#endif
|
||||
#if FT_ENABLED(FT_MQTT)
|
||||
_mqttSettingsService.begin();
|
||||
_mqttStatus.begin();
|
||||
#endif
|
||||
#if FT_ENABLED(FT_SECURITY)
|
||||
_authenticationService.begin();
|
||||
_securitySettingsService.begin();
|
||||
#endif
|
||||
#if FT_ENABLED(FT_ANALYTICS)
|
||||
_analyticsService.begin();
|
||||
#endif
|
||||
#if FT_ENABLED(FT_SLEEP)
|
||||
_sleepService.begin();
|
||||
#endif
|
||||
#if FT_ENABLED(FT_BATTERY)
|
||||
_batteryService.begin();
|
||||
#endif
|
||||
|
||||
// Start the loop task
|
||||
ESP_LOGV("ESP32SvelteKit", "Starting loop task");
|
||||
xTaskCreatePinnedToCore(
|
||||
this->_loopImpl, // Function that should be called
|
||||
"ESP32 SvelteKit Loop", // Name of the task (for debugging)
|
||||
4096, // Stack size (bytes)
|
||||
this, // Pass reference to this class instance
|
||||
(tskIDLE_PRIORITY + 1), // task priority
|
||||
NULL, // Task handle
|
||||
ESP32SVELTEKIT_RUNNING_CORE // Pin to application core
|
||||
);
|
||||
}
|
||||
|
||||
void ESP32SvelteKit::_loop()
|
||||
{
|
||||
while (1)
|
||||
{
|
||||
_wifiSettingsService.loop(); // 30 seconds
|
||||
_apSettingsService.loop(); // 10 seconds
|
||||
#if FT_ENABLED(FT_MQTT)
|
||||
_mqttSettingsService.loop(); // 5 seconds
|
||||
#endif
|
||||
vTaskDelay(20 / portTICK_PERIOD_MS);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,211 @@
|
||||
#ifndef ESP32SvelteKit_h
|
||||
#define ESP32SvelteKit_h
|
||||
|
||||
/**
|
||||
* ESP32 SvelteKit
|
||||
*
|
||||
* A simple, secure and extensible framework for IoT projects for ESP32 platforms
|
||||
* with responsive Sveltekit front-end built with TailwindCSS and DaisyUI.
|
||||
* https://github.com/theelims/ESP32-sveltekit
|
||||
*
|
||||
* Copyright (C) 2018 - 2023 rjwats
|
||||
* Copyright (C) 2023 theelims
|
||||
*
|
||||
* All Rights Reserved. This software may be modified and distributed under
|
||||
* the terms of the LGPL v3 license. See the LICENSE file for details.
|
||||
**/
|
||||
|
||||
#include <Arduino.h>
|
||||
|
||||
#include <WiFi.h>
|
||||
#include <ESPmDNS.h>
|
||||
#include <AnalyticsService.h>
|
||||
#include <FeaturesService.h>
|
||||
#include <APSettingsService.h>
|
||||
#include <APStatus.h>
|
||||
#include <AuthenticationService.h>
|
||||
#include <BatteryService.h>
|
||||
#include <FactoryResetService.h>
|
||||
#include <DownloadFirmwareService.h>
|
||||
#include <EventSocket.h>
|
||||
#include <MqttSettingsService.h>
|
||||
#include <MqttStatus.h>
|
||||
#include <NTPSettingsService.h>
|
||||
#include <NTPStatus.h>
|
||||
#include <UploadFirmwareService.h>
|
||||
#include <RestartService.h>
|
||||
#include <SecuritySettingsService.h>
|
||||
#include <SleepService.h>
|
||||
#include <SystemStatus.h>
|
||||
#include <WiFiScanner.h>
|
||||
#include <WiFiSettingsService.h>
|
||||
#include <WiFiStatus.h>
|
||||
#include <ESPFS.h>
|
||||
#include <PsychicHttp.h>
|
||||
|
||||
#ifdef EMBED_WWW
|
||||
#include <WWWData.h>
|
||||
#endif
|
||||
|
||||
#ifndef CORS_ORIGIN
|
||||
#define CORS_ORIGIN "*"
|
||||
#endif
|
||||
|
||||
#ifndef APP_VERSION
|
||||
#define APP_VERSION "demo"
|
||||
#endif
|
||||
|
||||
#ifndef APP_NAME
|
||||
#define APP_NAME "ESP32 SvelteKit Demo"
|
||||
#endif
|
||||
|
||||
#ifndef ESP32SVELTEKIT_RUNNING_CORE
|
||||
#define ESP32SVELTEKIT_RUNNING_CORE -1
|
||||
#endif
|
||||
|
||||
class ESP32SvelteKit
|
||||
{
|
||||
public:
|
||||
ESP32SvelteKit(PsychicHttpServer *server, unsigned int numberEndpoints = 115);
|
||||
|
||||
void begin();
|
||||
|
||||
FS *getFS()
|
||||
{
|
||||
return &ESPFS;
|
||||
}
|
||||
|
||||
PsychicHttpServer *getServer()
|
||||
{
|
||||
return _server;
|
||||
}
|
||||
|
||||
SecurityManager *getSecurityManager()
|
||||
{
|
||||
return &_securitySettingsService;
|
||||
}
|
||||
|
||||
EventSocket *getSocket()
|
||||
{
|
||||
return &_socket;
|
||||
}
|
||||
|
||||
#if FT_ENABLED(FT_SECURITY)
|
||||
StatefulService<SecuritySettings> *getSecuritySettingsService()
|
||||
{
|
||||
return &_securitySettingsService;
|
||||
}
|
||||
#endif
|
||||
|
||||
StatefulService<WiFiSettings> *getWiFiSettingsService()
|
||||
{
|
||||
return &_wifiSettingsService;
|
||||
}
|
||||
|
||||
StatefulService<APSettings> *getAPSettingsService()
|
||||
{
|
||||
return &_apSettingsService;
|
||||
}
|
||||
|
||||
#if FT_ENABLED(FT_NTP)
|
||||
StatefulService<NTPSettings> *getNTPSettingsService()
|
||||
{
|
||||
return &_ntpSettingsService;
|
||||
}
|
||||
#endif
|
||||
|
||||
#if FT_ENABLED(FT_MQTT)
|
||||
StatefulService<MqttSettings> *getMqttSettingsService()
|
||||
{
|
||||
return &_mqttSettingsService;
|
||||
}
|
||||
|
||||
PsychicMqttClient *getMqttClient()
|
||||
{
|
||||
return _mqttSettingsService.getMqttClient();
|
||||
}
|
||||
#endif
|
||||
|
||||
#if FT_ENABLED(FT_SLEEP)
|
||||
SleepService *getSleepService()
|
||||
{
|
||||
return &_sleepService;
|
||||
}
|
||||
#endif
|
||||
|
||||
#if FT_ENABLED(FT_BATTERY)
|
||||
BatteryService *getBatteryService()
|
||||
{
|
||||
return &_batteryService;
|
||||
}
|
||||
#endif
|
||||
|
||||
FeaturesService *getFeatureService()
|
||||
{
|
||||
return &_featureService;
|
||||
}
|
||||
|
||||
void factoryReset()
|
||||
{
|
||||
_factoryResetService.factoryReset();
|
||||
}
|
||||
|
||||
void setMDNSAppName(String name)
|
||||
{
|
||||
_appName = name;
|
||||
}
|
||||
|
||||
void recoveryMode()
|
||||
{
|
||||
_apSettingsService.recoveryMode();
|
||||
}
|
||||
|
||||
private:
|
||||
PsychicHttpServer *_server;
|
||||
unsigned int _numberEndpoints;
|
||||
FeaturesService _featureService;
|
||||
SecuritySettingsService _securitySettingsService;
|
||||
WiFiSettingsService _wifiSettingsService;
|
||||
WiFiScanner _wifiScanner;
|
||||
WiFiStatus _wifiStatus;
|
||||
APSettingsService _apSettingsService;
|
||||
APStatus _apStatus;
|
||||
EventSocket _socket;
|
||||
#if FT_ENABLED(FT_NTP)
|
||||
NTPSettingsService _ntpSettingsService;
|
||||
NTPStatus _ntpStatus;
|
||||
#endif
|
||||
#if FT_ENABLED(FT_UPLOAD_FIRMWARE)
|
||||
UploadFirmwareService _uploadFirmwareService;
|
||||
#endif
|
||||
#if FT_ENABLED(FT_DOWNLOAD_FIRMWARE)
|
||||
DownloadFirmwareService _downloadFirmwareService;
|
||||
#endif
|
||||
#if FT_ENABLED(FT_MQTT)
|
||||
MqttSettingsService _mqttSettingsService;
|
||||
MqttStatus _mqttStatus;
|
||||
#endif
|
||||
#if FT_ENABLED(FT_SECURITY)
|
||||
AuthenticationService _authenticationService;
|
||||
#endif
|
||||
#if FT_ENABLED(FT_SLEEP)
|
||||
SleepService _sleepService;
|
||||
#endif
|
||||
#if FT_ENABLED(FT_BATTERY)
|
||||
BatteryService _batteryService;
|
||||
#endif
|
||||
#if FT_ENABLED(FT_ANALYTICS)
|
||||
AnalyticsService _analyticsService;
|
||||
#endif
|
||||
RestartService _restartService;
|
||||
FactoryResetService _factoryResetService;
|
||||
SystemStatus _systemStatus;
|
||||
|
||||
String _appName = APP_NAME;
|
||||
|
||||
protected:
|
||||
static void _loopImpl(void *_this) { static_cast<ESP32SvelteKit *>(_this)->_loop(); }
|
||||
void _loop();
|
||||
};
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,21 @@
|
||||
#ifndef ESPFS_H_
|
||||
#define ESPFS_H_
|
||||
|
||||
/**
|
||||
* ESP32 SvelteKit
|
||||
*
|
||||
* A simple, secure and extensible framework for IoT projects for ESP32 platforms
|
||||
* with responsive Sveltekit front-end built with TailwindCSS and DaisyUI.
|
||||
* https://github.com/theelims/ESP32-sveltekit
|
||||
*
|
||||
* Copyright (C) 2018 - 2023 rjwats
|
||||
* Copyright (C) 2023 theelims
|
||||
*
|
||||
* All Rights Reserved. This software may be modified and distributed under
|
||||
* the terms of the LGPL v3 license. See the LICENSE file for details.
|
||||
**/
|
||||
|
||||
#include <LittleFS.h>
|
||||
#define ESPFS LittleFS
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,75 @@
|
||||
#ifndef EventEndpoint_h
|
||||
#define EventEndpoint_h
|
||||
|
||||
/**
|
||||
* ESP32 SvelteKit
|
||||
*
|
||||
* A simple, secure and extensible framework for IoT projects for ESP32 platforms
|
||||
* with responsive Sveltekit front-end built with TailwindCSS and DaisyUI.
|
||||
* https://github.com/theelims/ESP32-sveltekit
|
||||
*
|
||||
* Copyright (C) 2018 - 2023 rjwats
|
||||
* Copyright (C) 2023 theelims
|
||||
*
|
||||
* All Rights Reserved. This software may be modified and distributed under
|
||||
* the terms of the LGPL v3 license. See the LICENSE file for details.
|
||||
**/
|
||||
|
||||
#include <EventSocket.h>
|
||||
#include <PsychicHttp.h>
|
||||
#include <SecurityManager.h>
|
||||
#include <StatefulService.h>
|
||||
|
||||
template <class T>
|
||||
class EventEndpoint
|
||||
{
|
||||
public:
|
||||
EventEndpoint(JsonStateReader<T> stateReader,
|
||||
JsonStateUpdater<T> stateUpdater,
|
||||
StatefulService<T> *statefulService,
|
||||
EventSocket *socket, const char *event,
|
||||
size_t bufferSize = DEFAULT_BUFFER_SIZE) : _stateReader(stateReader),
|
||||
_stateUpdater(stateUpdater),
|
||||
_statefulService(statefulService),
|
||||
_socket(socket),
|
||||
_bufferSize(bufferSize),
|
||||
_event(event)
|
||||
{
|
||||
_statefulService->addUpdateHandler([&](const String &originId)
|
||||
{ syncState(originId); },
|
||||
false);
|
||||
}
|
||||
|
||||
void begin()
|
||||
{
|
||||
_socket->registerEvent(_event);
|
||||
_socket->onEvent(_event, std::bind(&EventEndpoint::updateState, this, std::placeholders::_1, std::placeholders::_2));
|
||||
_socket->onSubscribe(_event, std::bind(&EventEndpoint::syncState, this, std::placeholders::_1, std::placeholders::_2));
|
||||
}
|
||||
|
||||
private:
|
||||
JsonStateReader<T> _stateReader;
|
||||
JsonStateUpdater<T> _stateUpdater;
|
||||
StatefulService<T> *_statefulService;
|
||||
EventSocket *_socket;
|
||||
const char *_event;
|
||||
size_t _bufferSize;
|
||||
|
||||
void updateState(JsonObject &root, int originId)
|
||||
{
|
||||
_statefulService->update(root, _stateUpdater, String(originId));
|
||||
}
|
||||
|
||||
void syncState(const String &originId, bool sync = false)
|
||||
{
|
||||
DynamicJsonDocument jsonDocument{_bufferSize};
|
||||
JsonObject root = jsonDocument.to<JsonObject>();
|
||||
String output;
|
||||
_statefulService->read(root, _stateReader);
|
||||
serializeJson(root, output);
|
||||
ESP_LOGV("EventEndpoint", "Syncing state: %s", output.c_str());
|
||||
_socket->emit(_event, output.c_str(), originId.c_str(), sync);
|
||||
}
|
||||
};
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,226 @@
|
||||
#include <EventSocket.h>
|
||||
|
||||
SemaphoreHandle_t clientSubscriptionsMutex = xSemaphoreCreateMutex();
|
||||
|
||||
EventSocket::EventSocket(PsychicHttpServer *server,
|
||||
SecurityManager *securityManager,
|
||||
AuthenticationPredicate authenticationPredicate) : _server(server),
|
||||
_securityManager(securityManager),
|
||||
_authenticationPredicate(authenticationPredicate),
|
||||
_bufferSize(1024)
|
||||
{
|
||||
}
|
||||
|
||||
void EventSocket::begin()
|
||||
{
|
||||
_socket.setFilter(_securityManager->filterRequest(_authenticationPredicate));
|
||||
_socket.onOpen((std::bind(&EventSocket::onWSOpen, this, std::placeholders::_1)));
|
||||
_socket.onClose(std::bind(&EventSocket::onWSClose, this, std::placeholders::_1));
|
||||
_socket.onFrame(std::bind(&EventSocket::onFrame, this, std::placeholders::_1, std::placeholders::_2));
|
||||
_server->on(EVENT_SERVICE_PATH, &_socket);
|
||||
|
||||
registerEvent("errorToast");
|
||||
registerEvent("warningToast");
|
||||
registerEvent("infoToast");
|
||||
registerEvent("successToast");
|
||||
|
||||
ESP_LOGV("EventSocket", "Registered event socket endpoint: %s", EVENT_SERVICE_PATH);
|
||||
}
|
||||
|
||||
void EventSocket::registerEvent(String event)
|
||||
{
|
||||
if (!isEventValid(event))
|
||||
{
|
||||
ESP_LOGV("EventSocket", "Registering event: %s", event.c_str());
|
||||
events.push_back(event);
|
||||
}
|
||||
else
|
||||
{
|
||||
ESP_LOGW("EventSocket", "Event already registered: %s", event.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
void EventSocket::onWSOpen(PsychicWebSocketClient *client)
|
||||
{
|
||||
ESP_LOGI("EventSocket", "ws[%s][%u] connect", client->remoteIP().toString().c_str(), client->socket());
|
||||
}
|
||||
|
||||
void EventSocket::onWSClose(PsychicWebSocketClient *client)
|
||||
{
|
||||
for (auto &event_subscriptions : client_subscriptions)
|
||||
{
|
||||
event_subscriptions.second.remove(client->socket());
|
||||
}
|
||||
ESP_LOGI("EventSocket", "ws[%s][%u] disconnect", client->remoteIP().toString().c_str(), client->socket());
|
||||
}
|
||||
|
||||
esp_err_t EventSocket::onFrame(PsychicWebSocketRequest *request, httpd_ws_frame *frame)
|
||||
{
|
||||
ESP_LOGV("EventSocket", "ws[%s][%u] opcode[%d]", request->client()->remoteIP().toString().c_str(),
|
||||
request->client()->socket(), frame->type);
|
||||
|
||||
if (frame->type == HTTPD_WS_TYPE_TEXT)
|
||||
{
|
||||
ESP_LOGV("EventSocket", "ws[%s][%u] request: %s", request->client()->remoteIP().toString().c_str(),
|
||||
request->client()->socket(), (char *)frame->payload);
|
||||
|
||||
DynamicJsonDocument doc = DynamicJsonDocument(_bufferSize);
|
||||
DeserializationError error = deserializeJson(doc, (char *)frame->payload, frame->len);
|
||||
|
||||
if (!error && doc.is<JsonObject>())
|
||||
{
|
||||
String event = doc["event"];
|
||||
if (event == "subscribe")
|
||||
{
|
||||
// only subscribe to events that are registered
|
||||
if (isEventValid(doc["data"].as<String>()))
|
||||
{
|
||||
client_subscriptions[doc["data"]].push_back(request->client()->socket());
|
||||
handleSubscribeCallbacks(doc["data"], String(request->client()->socket()));
|
||||
}
|
||||
else
|
||||
{
|
||||
ESP_LOGW("EventSocket", "Client tried to subscribe to unregistered event: %s", doc["data"].as<String>().c_str());
|
||||
}
|
||||
}
|
||||
else if (event == "unsubscribe")
|
||||
{
|
||||
client_subscriptions[doc["data"]].remove(request->client()->socket());
|
||||
}
|
||||
else
|
||||
{
|
||||
JsonObject jsonObject = doc["data"].as<JsonObject>();
|
||||
handleEventCallbacks(event, jsonObject, request->client()->socket());
|
||||
}
|
||||
return ESP_OK;
|
||||
}
|
||||
}
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
void EventSocket::emit(String event, String payload)
|
||||
{
|
||||
emit(event.c_str(), payload.c_str(), "");
|
||||
}
|
||||
|
||||
void EventSocket::emit(const char *event, const char *payload)
|
||||
{
|
||||
emit(event, payload, "");
|
||||
}
|
||||
|
||||
void EventSocket::emit(const char *event, const char *payload, const char *originId, bool onlyToSameOrigin)
|
||||
{
|
||||
// Only process valid events
|
||||
if (!isEventValid(String(event)))
|
||||
{
|
||||
ESP_LOGW("EventSocket", "Method tried to emit unregistered event: %s", event);
|
||||
return;
|
||||
}
|
||||
|
||||
int originSubscriptionId = originId[0] ? atoi(originId) : -1;
|
||||
xSemaphoreTake(clientSubscriptionsMutex, portMAX_DELAY);
|
||||
auto &subscriptions = client_subscriptions[event];
|
||||
if (subscriptions.empty())
|
||||
{
|
||||
xSemaphoreGive(clientSubscriptionsMutex);
|
||||
return;
|
||||
}
|
||||
String msg = "[\"" + String(event) + "\"," + String(payload) + "]";
|
||||
|
||||
// if onlyToSameOrigin == true, send the message back to the origin
|
||||
if (onlyToSameOrigin && originSubscriptionId > 0)
|
||||
{
|
||||
auto *client = _socket.getClient(originSubscriptionId);
|
||||
if (client)
|
||||
{
|
||||
ESP_LOGV("EventSocket", "Emitting event: %s to %s, Message: %s", event, client->remoteIP().toString().c_str(),
|
||||
msg.c_str());
|
||||
client->sendMessage(msg.c_str());
|
||||
}
|
||||
}
|
||||
else
|
||||
{ // else send the message to all other clients
|
||||
|
||||
for (int subscription : client_subscriptions[event])
|
||||
{
|
||||
if (subscription == originSubscriptionId)
|
||||
continue;
|
||||
auto *client = _socket.getClient(subscription);
|
||||
if (!client)
|
||||
{
|
||||
subscriptions.remove(subscription);
|
||||
continue;
|
||||
}
|
||||
ESP_LOGV("EventSocket", "Emitting event: %s to %s, Message: %s", event, client->remoteIP().toString().c_str(),
|
||||
msg.c_str());
|
||||
client->sendMessage(msg.c_str());
|
||||
}
|
||||
}
|
||||
xSemaphoreGive(clientSubscriptionsMutex);
|
||||
}
|
||||
|
||||
void EventSocket::pushNotification(String message, pushEvent event)
|
||||
{
|
||||
String eventType;
|
||||
switch (event)
|
||||
{
|
||||
case (PUSHERROR):
|
||||
eventType = "errorToast";
|
||||
break;
|
||||
case (PUSHWARNING):
|
||||
eventType = "warningToast";
|
||||
break;
|
||||
case (PUSHINFO):
|
||||
eventType = "infoToast";
|
||||
break;
|
||||
case (PUSHSUCCESS):
|
||||
eventType = "successToast";
|
||||
break;
|
||||
default:
|
||||
ESP_LOGW("EventSocket", "Client tried invalid push notification: %s", event);
|
||||
return;
|
||||
}
|
||||
emit(eventType.c_str(), message.c_str());
|
||||
}
|
||||
|
||||
void EventSocket::handleEventCallbacks(String event, JsonObject &jsonObject, int originId)
|
||||
{
|
||||
for (auto &callback : event_callbacks[event])
|
||||
{
|
||||
callback(jsonObject, originId);
|
||||
}
|
||||
}
|
||||
|
||||
void EventSocket::handleSubscribeCallbacks(String event, const String &originId)
|
||||
{
|
||||
for (auto &callback : subscribe_callbacks[event])
|
||||
{
|
||||
callback(originId, true);
|
||||
}
|
||||
}
|
||||
|
||||
void EventSocket::onEvent(String event, EventCallback callback)
|
||||
{
|
||||
if (!isEventValid(event))
|
||||
{
|
||||
ESP_LOGW("EventSocket", "Method tried to register unregistered event: %s", event.c_str());
|
||||
return;
|
||||
}
|
||||
event_callbacks[event].push_back(callback);
|
||||
}
|
||||
|
||||
void EventSocket::onSubscribe(String event, SubscribeCallback callback)
|
||||
{
|
||||
if (!isEventValid(event))
|
||||
{
|
||||
ESP_LOGW("EventSocket", "Method tried to subscribe to unregistered event: %s", event.c_str());
|
||||
return;
|
||||
}
|
||||
subscribe_callbacks[event].push_back(callback);
|
||||
ESP_LOGI("EventSocket", "onSubscribe for event: %s", event.c_str());
|
||||
}
|
||||
|
||||
bool EventSocket::isEventValid(String event)
|
||||
{
|
||||
return std::find(events.begin(), events.end(), event) != events.end();
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
#ifndef Socket_h
|
||||
#define Socket_h
|
||||
|
||||
#include <PsychicHttp.h>
|
||||
#include <SecurityManager.h>
|
||||
#include <StatefulService.h>
|
||||
#include <list>
|
||||
#include <map>
|
||||
#include <vector>
|
||||
|
||||
#define EVENT_SERVICE_PATH "/ws/events"
|
||||
|
||||
typedef std::function<void(JsonObject &root, int originId)> EventCallback;
|
||||
typedef std::function<void(const String &originId, bool sync)> SubscribeCallback;
|
||||
|
||||
enum pushEvent
|
||||
{
|
||||
PUSHERROR,
|
||||
PUSHWARNING,
|
||||
PUSHINFO,
|
||||
PUSHSUCCESS
|
||||
};
|
||||
|
||||
class EventSocket
|
||||
{
|
||||
public:
|
||||
EventSocket(PsychicHttpServer *server, SecurityManager *_securityManager, AuthenticationPredicate authenticationPredicate = AuthenticationPredicates::IS_AUTHENTICATED);
|
||||
|
||||
void begin();
|
||||
|
||||
void registerEvent(String event);
|
||||
|
||||
void onEvent(String event, EventCallback callback);
|
||||
|
||||
void onSubscribe(String event, SubscribeCallback callback);
|
||||
|
||||
void emit(String event, String payload);
|
||||
|
||||
void emit(const char *event, const char *payload);
|
||||
|
||||
void emit(const char *event, const char *payload, const char *originId, bool onlyToSameOrigin = false);
|
||||
// if onlyToSameOrigin == true, the message will be sent to the originId only, otherwise it will be broadcasted to all clients except the originId
|
||||
|
||||
void pushNotification(String message, pushEvent event);
|
||||
|
||||
private:
|
||||
PsychicHttpServer *_server;
|
||||
PsychicWebSocketHandler _socket;
|
||||
SecurityManager *_securityManager;
|
||||
AuthenticationPredicate _authenticationPredicate;
|
||||
|
||||
std::vector<String> events;
|
||||
std::map<String, std::list<int>> client_subscriptions;
|
||||
std::map<String, std::list<EventCallback>> event_callbacks;
|
||||
std::map<String, std::list<SubscribeCallback>> subscribe_callbacks;
|
||||
void handleEventCallbacks(String event, JsonObject &jsonObject, int originId);
|
||||
void handleSubscribeCallbacks(String event, const String &originId);
|
||||
|
||||
bool isEventValid(String event);
|
||||
|
||||
size_t _bufferSize;
|
||||
void onWSOpen(PsychicWebSocketClient *client);
|
||||
void onWSClose(PsychicWebSocketClient *client);
|
||||
esp_err_t onFrame(PsychicWebSocketRequest *request, httpd_ws_frame *frame);
|
||||
};
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,145 @@
|
||||
#ifndef FSPersistence_h
|
||||
#define FSPersistence_h
|
||||
|
||||
/**
|
||||
* ESP32 SvelteKit
|
||||
*
|
||||
* A simple, secure and extensible framework for IoT projects for ESP32 platforms
|
||||
* with responsive Sveltekit front-end built with TailwindCSS and DaisyUI.
|
||||
* https://github.com/theelims/ESP32-sveltekit
|
||||
*
|
||||
* Copyright (C) 2018 - 2023 rjwats
|
||||
* Copyright (C) 2023 theelims
|
||||
*
|
||||
* All Rights Reserved. This software may be modified and distributed under
|
||||
* the terms of the LGPL v3 license. See the LICENSE file for details.
|
||||
**/
|
||||
|
||||
#include <StatefulService.h>
|
||||
#include <FS.h>
|
||||
|
||||
template <class T>
|
||||
class FSPersistence
|
||||
{
|
||||
public:
|
||||
FSPersistence(JsonStateReader<T> stateReader,
|
||||
JsonStateUpdater<T> stateUpdater,
|
||||
StatefulService<T> *statefulService,
|
||||
FS *fs,
|
||||
const char *filePath,
|
||||
size_t bufferSize = DEFAULT_BUFFER_SIZE) : _stateReader(stateReader),
|
||||
_stateUpdater(stateUpdater),
|
||||
_statefulService(statefulService),
|
||||
_fs(fs),
|
||||
_filePath(filePath),
|
||||
_bufferSize(bufferSize),
|
||||
_updateHandlerId(0)
|
||||
{
|
||||
enableUpdateHandler();
|
||||
}
|
||||
|
||||
void readFromFS()
|
||||
{
|
||||
File settingsFile = _fs->open(_filePath, "r");
|
||||
|
||||
if (settingsFile)
|
||||
{
|
||||
DynamicJsonDocument jsonDocument = DynamicJsonDocument(_bufferSize);
|
||||
DeserializationError error = deserializeJson(jsonDocument, settingsFile);
|
||||
if (error == DeserializationError::Ok && jsonDocument.is<JsonObject>())
|
||||
{
|
||||
JsonObject jsonObject = jsonDocument.as<JsonObject>();
|
||||
_statefulService->updateWithoutPropagation(jsonObject, _stateUpdater);
|
||||
settingsFile.close();
|
||||
return;
|
||||
}
|
||||
settingsFile.close();
|
||||
}
|
||||
|
||||
// If we reach here we have not been successful in loading the config and hard-coded defaults are now applied.
|
||||
// The settings are then written back to the file system so the defaults persist between resets. This last step is
|
||||
// required as in some cases defaults contain randomly generated values which would otherwise be modified on reset.
|
||||
applyDefaults();
|
||||
writeToFS();
|
||||
}
|
||||
|
||||
bool writeToFS()
|
||||
{
|
||||
// create and populate a new json object
|
||||
DynamicJsonDocument jsonDocument = DynamicJsonDocument(_bufferSize);
|
||||
JsonObject jsonObject = jsonDocument.to<JsonObject>();
|
||||
_statefulService->read(jsonObject, _stateReader);
|
||||
|
||||
// make directories if required
|
||||
mkdirs();
|
||||
|
||||
// serialize it to filesystem
|
||||
File settingsFile = _fs->open(_filePath, "w");
|
||||
|
||||
// failed to open file, return false
|
||||
if (!settingsFile)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// serialize the data to the file
|
||||
serializeJson(jsonDocument, settingsFile);
|
||||
settingsFile.close();
|
||||
return true;
|
||||
}
|
||||
|
||||
void disableUpdateHandler()
|
||||
{
|
||||
if (_updateHandlerId)
|
||||
{
|
||||
_statefulService->removeUpdateHandler(_updateHandlerId);
|
||||
_updateHandlerId = 0;
|
||||
}
|
||||
}
|
||||
|
||||
void enableUpdateHandler()
|
||||
{
|
||||
if (!_updateHandlerId)
|
||||
{
|
||||
_updateHandlerId = _statefulService->addUpdateHandler([&](const String &originId)
|
||||
{ writeToFS(); });
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
JsonStateReader<T> _stateReader;
|
||||
JsonStateUpdater<T> _stateUpdater;
|
||||
StatefulService<T> *_statefulService;
|
||||
FS *_fs;
|
||||
const char *_filePath;
|
||||
size_t _bufferSize;
|
||||
update_handler_id_t _updateHandlerId;
|
||||
|
||||
// We assume we have a _filePath with format "/directory1/directory2/filename"
|
||||
// We create a directory for each missing parent
|
||||
void mkdirs()
|
||||
{
|
||||
String path(_filePath);
|
||||
int index = 0;
|
||||
while ((index = path.indexOf('/', index + 1)) != -1)
|
||||
{
|
||||
String segment = path.substring(0, index);
|
||||
if (!_fs->exists(segment))
|
||||
{
|
||||
_fs->mkdir(segment);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected:
|
||||
// We assume the updater supplies sensible defaults if an empty object
|
||||
// is supplied, this virtual function allows that to be changed.
|
||||
virtual void applyDefaults()
|
||||
{
|
||||
DynamicJsonDocument jsonDocument = DynamicJsonDocument(_bufferSize);
|
||||
JsonObject jsonObject = jsonDocument.as<JsonObject>();
|
||||
_statefulService->updateWithoutPropagation(jsonObject, _stateUpdater);
|
||||
}
|
||||
};
|
||||
|
||||
#endif // end FSPersistence
|
||||
@@ -0,0 +1,58 @@
|
||||
/**
|
||||
* ESP32 SvelteKit
|
||||
*
|
||||
* A simple, secure and extensible framework for IoT projects for ESP32 platforms
|
||||
* with responsive Sveltekit front-end built with TailwindCSS and DaisyUI.
|
||||
* https://github.com/theelims/ESP32-sveltekit
|
||||
*
|
||||
* Copyright (C) 2018 - 2023 rjwats
|
||||
* Copyright (C) 2023 theelims
|
||||
*
|
||||
* All Rights Reserved. This software may be modified and distributed under
|
||||
* the terms of the LGPL v3 license. See the LICENSE file for details.
|
||||
**/
|
||||
|
||||
#include <FactoryResetService.h>
|
||||
|
||||
using namespace std::placeholders;
|
||||
|
||||
FactoryResetService::FactoryResetService(PsychicHttpServer *server,
|
||||
FS *fs,
|
||||
SecurityManager *securityManager) : _server(server),
|
||||
fs(fs),
|
||||
_securityManager(securityManager)
|
||||
{
|
||||
}
|
||||
|
||||
void FactoryResetService::begin()
|
||||
{
|
||||
_server->on(FACTORY_RESET_SERVICE_PATH,
|
||||
HTTP_POST,
|
||||
_securityManager->wrapRequest(std::bind(&FactoryResetService::handleRequest, this, _1), AuthenticationPredicates::IS_ADMIN));
|
||||
|
||||
ESP_LOGV("FactoryResetService", "Registered POST endpoint: %s", FACTORY_RESET_SERVICE_PATH);
|
||||
}
|
||||
|
||||
esp_err_t FactoryResetService::handleRequest(PsychicRequest *request)
|
||||
{
|
||||
request->reply(200);
|
||||
factoryReset();
|
||||
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete function assumes that all files are stored flat, within the config directory.
|
||||
*/
|
||||
void FactoryResetService::factoryReset()
|
||||
{
|
||||
File root = fs->open(FS_CONFIG_DIRECTORY);
|
||||
File file;
|
||||
while (file = root.openNextFile())
|
||||
{
|
||||
String path = file.path();
|
||||
file.close();
|
||||
fs->remove(path);
|
||||
}
|
||||
RestartService::restartNow();
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
#ifndef FactoryResetService_h
|
||||
#define FactoryResetService_h
|
||||
|
||||
/**
|
||||
* ESP32 SvelteKit
|
||||
*
|
||||
* A simple, secure and extensible framework for IoT projects for ESP32 platforms
|
||||
* with responsive Sveltekit front-end built with TailwindCSS and DaisyUI.
|
||||
* https://github.com/theelims/ESP32-sveltekit
|
||||
*
|
||||
* Copyright (C) 2018 - 2023 rjwats
|
||||
* Copyright (C) 2023 theelims
|
||||
*
|
||||
* All Rights Reserved. This software may be modified and distributed under
|
||||
* the terms of the LGPL v3 license. See the LICENSE file for details.
|
||||
**/
|
||||
|
||||
#include <WiFi.h>
|
||||
|
||||
#include <PsychicHttp.h>
|
||||
#include <SecurityManager.h>
|
||||
#include <RestartService.h>
|
||||
#include <FS.h>
|
||||
|
||||
#define FS_CONFIG_DIRECTORY "/config"
|
||||
#define FACTORY_RESET_SERVICE_PATH "/rest/factoryReset"
|
||||
|
||||
class FactoryResetService
|
||||
{
|
||||
FS *fs;
|
||||
|
||||
public:
|
||||
FactoryResetService(PsychicHttpServer *server, FS *fs, SecurityManager *securityManager);
|
||||
|
||||
void begin();
|
||||
void factoryReset();
|
||||
|
||||
private:
|
||||
PsychicHttpServer *_server;
|
||||
SecurityManager *_securityManager;
|
||||
esp_err_t handleRequest(PsychicRequest *request);
|
||||
};
|
||||
|
||||
#endif // end FactoryResetService_h
|
||||
@@ -0,0 +1,60 @@
|
||||
#ifndef Features_h
|
||||
#define Features_h
|
||||
|
||||
/**
|
||||
* ESP32 SvelteKit
|
||||
*
|
||||
* A simple, secure and extensible framework for IoT projects for ESP32 platforms
|
||||
* with responsive Sveltekit front-end built with TailwindCSS and DaisyUI.
|
||||
* https://github.com/theelims/ESP32-sveltekit
|
||||
*
|
||||
* Copyright (C) 2018 - 2023 rjwats
|
||||
* Copyright (C) 2023 theelims
|
||||
*
|
||||
* All Rights Reserved. This software may be modified and distributed under
|
||||
* the terms of the LGPL v3 license. See the LICENSE file for details.
|
||||
**/
|
||||
|
||||
#define FT_ENABLED(feature) feature
|
||||
|
||||
// security feature on by default
|
||||
#ifndef FT_SECURITY
|
||||
#define FT_SECURITY 1
|
||||
#endif
|
||||
|
||||
// mqtt feature on by default
|
||||
#ifndef FT_MQTT
|
||||
#define FT_MQTT 1
|
||||
#endif
|
||||
|
||||
// ntp feature on by default
|
||||
#ifndef FT_NTP
|
||||
#define FT_NTP 1
|
||||
#endif
|
||||
|
||||
// upload firmware feature off by default
|
||||
#ifndef FT_UPLOAD_FIRMWARE
|
||||
#define FT_UPLOAD_FIRMWARE 0
|
||||
#endif
|
||||
|
||||
// download firmware feature off by default
|
||||
#ifndef FT_DOWNLOAD_FIRMWARE
|
||||
#define FT_DOWNLOAD_FIRMWARE 0
|
||||
#endif
|
||||
|
||||
// ESP32 sleep states off by default
|
||||
#ifndef FT_SLEEP
|
||||
#define FT_SLEEP 0
|
||||
#endif
|
||||
|
||||
// ESP32 battery state off by default
|
||||
#ifndef FT_BATTERY
|
||||
#define FT_BATTERY 0
|
||||
#endif
|
||||
|
||||
// ESP32 analytics on by default
|
||||
#ifndef FT_ANALYTICS
|
||||
#define FT_ANALYTICS 1
|
||||
#endif
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,90 @@
|
||||
/**
|
||||
* ESP32 SvelteKit
|
||||
*
|
||||
* A simple, secure and extensible framework for IoT projects for ESP32 platforms
|
||||
* with responsive Sveltekit front-end built with TailwindCSS and DaisyUI.
|
||||
* https://github.com/theelims/ESP32-sveltekit
|
||||
*
|
||||
* Copyright (C) 2018 - 2023 rjwats
|
||||
* Copyright (C) 2023 theelims
|
||||
*
|
||||
* All Rights Reserved. This software may be modified and distributed under
|
||||
* the terms of the LGPL v3 license. See the LICENSE file for details.
|
||||
**/
|
||||
|
||||
#include <FeaturesService.h>
|
||||
|
||||
FeaturesService::FeaturesService(PsychicHttpServer *server) : _server(server)
|
||||
{
|
||||
}
|
||||
|
||||
void FeaturesService::begin()
|
||||
{
|
||||
_server->on(FEATURES_SERVICE_PATH, HTTP_GET, [&](PsychicRequest *request)
|
||||
{
|
||||
PsychicJsonResponse response = PsychicJsonResponse(request, false, MAX_FEATURES_SIZE);
|
||||
JsonObject root = response.getRoot();
|
||||
|
||||
#if FT_ENABLED(FT_SECURITY)
|
||||
root["security"] = true;
|
||||
#else
|
||||
root["security"] = false;
|
||||
#endif
|
||||
#if FT_ENABLED(FT_MQTT)
|
||||
root["mqtt"] = true;
|
||||
#else
|
||||
root["mqtt"] = false;
|
||||
#endif
|
||||
#if FT_ENABLED(FT_NTP)
|
||||
root["ntp"] = true;
|
||||
#else
|
||||
root["ntp"] = false;
|
||||
#endif
|
||||
#if FT_ENABLED(FT_UPLOAD_FIRMWARE)
|
||||
root["upload_firmware"] = true;
|
||||
#else
|
||||
root["upload_firmware"] = false;
|
||||
#endif
|
||||
#if FT_ENABLED(FT_DOWNLOAD_FIRMWARE)
|
||||
root["download_firmware"] = true;
|
||||
#else
|
||||
root["download_firmware"] = false;
|
||||
#endif
|
||||
#if FT_ENABLED(FT_SLEEP)
|
||||
root["sleep"] = true;
|
||||
#else
|
||||
root["sleep"] = false;
|
||||
#endif
|
||||
#if FT_ENABLED(FT_BATTERY)
|
||||
root["battery"] = true;
|
||||
#else
|
||||
root["battery"] = false;
|
||||
#endif
|
||||
#if FT_ENABLED(FT_ANALYTICS)
|
||||
root["analytics"] = true;
|
||||
#else
|
||||
root["analytics"] = false;
|
||||
#endif
|
||||
root["firmware_version"] = APP_VERSION;
|
||||
root["firmware_name"] = APP_NAME;
|
||||
root["firmware_built_target"] = BUILD_TARGET;
|
||||
|
||||
// Iterate over user features
|
||||
for (auto &element : userFeatures)
|
||||
{
|
||||
root[element.feature.c_str()] = element.enabled;
|
||||
}
|
||||
|
||||
return response.send(); });
|
||||
|
||||
ESP_LOGV("FeaturesService", "Registered GET endpoint: %s", FEATURES_SERVICE_PATH);
|
||||
}
|
||||
|
||||
void FeaturesService::addFeature(String feature, bool enabled)
|
||||
{
|
||||
UserFeature newFeature;
|
||||
newFeature.feature = feature;
|
||||
newFeature.enabled = enabled;
|
||||
|
||||
userFeatures.push_back(newFeature);
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
#ifndef FeaturesService_h
|
||||
#define FeaturesService_h
|
||||
|
||||
/**
|
||||
* ESP32 SvelteKit
|
||||
*
|
||||
* A simple, secure and extensible framework for IoT projects for ESP32 platforms
|
||||
* with responsive Sveltekit front-end built with TailwindCSS and DaisyUI.
|
||||
* https://github.com/theelims/ESP32-sveltekit
|
||||
*
|
||||
* Copyright (C) 2018 - 2023 rjwats
|
||||
* Copyright (C) 2023 theelims
|
||||
*
|
||||
* All Rights Reserved. This software may be modified and distributed under
|
||||
* the terms of the LGPL v3 license. See the LICENSE file for details.
|
||||
**/
|
||||
|
||||
#include <Features.h>
|
||||
|
||||
#include <WiFi.h>
|
||||
#include <ArduinoJson.h>
|
||||
#include <PsychicHttp.h>
|
||||
#include <vector>
|
||||
|
||||
#define MAX_FEATURES_SIZE 256
|
||||
#define FEATURES_SERVICE_PATH "/rest/features"
|
||||
|
||||
typedef struct
|
||||
{
|
||||
String feature;
|
||||
bool enabled;
|
||||
} UserFeature;
|
||||
|
||||
class FeaturesService
|
||||
{
|
||||
public:
|
||||
FeaturesService(PsychicHttpServer *server);
|
||||
|
||||
void begin();
|
||||
|
||||
void addFeature(String feature, bool enabled);
|
||||
|
||||
private:
|
||||
PsychicHttpServer *_server;
|
||||
std::vector<UserFeature> userFeatures;
|
||||
};
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,109 @@
|
||||
#ifndef HttpEndpoint_h
|
||||
#define HttpEndpoint_h
|
||||
|
||||
#include <functional>
|
||||
|
||||
#include <PsychicHttp.h>
|
||||
|
||||
#include <SecurityManager.h>
|
||||
#include <StatefulService.h>
|
||||
|
||||
#define HTTP_ENDPOINT_ORIGIN_ID "http"
|
||||
#define HTTPS_ENDPOINT_ORIGIN_ID "https"
|
||||
|
||||
using namespace std::placeholders; // for `_1` etc
|
||||
|
||||
template <class T>
|
||||
class HttpEndpoint
|
||||
{
|
||||
protected:
|
||||
JsonStateReader<T> _stateReader;
|
||||
JsonStateUpdater<T> _stateUpdater;
|
||||
StatefulService<T> *_statefulService;
|
||||
size_t _bufferSize;
|
||||
SecurityManager *_securityManager;
|
||||
AuthenticationPredicate _authenticationPredicate;
|
||||
PsychicHttpServer *_server;
|
||||
const char *_servicePath;
|
||||
|
||||
public:
|
||||
HttpEndpoint(JsonStateReader<T> stateReader,
|
||||
JsonStateUpdater<T> stateUpdater,
|
||||
StatefulService<T> *statefulService,
|
||||
PsychicHttpServer *server,
|
||||
const char *servicePath,
|
||||
SecurityManager *securityManager,
|
||||
AuthenticationPredicate authenticationPredicate = AuthenticationPredicates::IS_ADMIN,
|
||||
size_t bufferSize = DEFAULT_BUFFER_SIZE)
|
||||
: _stateReader(stateReader), _stateUpdater(stateUpdater), _statefulService(statefulService), _server(server), _servicePath(servicePath), _securityManager(securityManager), _authenticationPredicate(authenticationPredicate), _bufferSize(bufferSize)
|
||||
{
|
||||
}
|
||||
|
||||
// register the web server on() endpoints
|
||||
void begin()
|
||||
{
|
||||
|
||||
// OPTIONS (for CORS preflight)
|
||||
#ifdef ENABLE_CORS
|
||||
_server->on(_servicePath,
|
||||
HTTP_OPTIONS,
|
||||
_securityManager->wrapRequest(
|
||||
[this](PsychicRequest *request)
|
||||
{
|
||||
return request->reply(200);
|
||||
},
|
||||
AuthenticationPredicates::IS_AUTHENTICATED));
|
||||
#endif
|
||||
|
||||
// GET
|
||||
_server->on(_servicePath,
|
||||
HTTP_GET,
|
||||
_securityManager->wrapRequest(
|
||||
[this](PsychicRequest *request)
|
||||
{
|
||||
PsychicJsonResponse response = PsychicJsonResponse(request, false, _bufferSize);
|
||||
JsonObject jsonObject = response.getRoot();
|
||||
_statefulService->read(jsonObject, _stateReader);
|
||||
return response.send();
|
||||
},
|
||||
_authenticationPredicate));
|
||||
ESP_LOGV("HttpEndpoint", "Registered GET endpoint: %s", _servicePath);
|
||||
|
||||
// POST
|
||||
_server->on(_servicePath,
|
||||
HTTP_POST,
|
||||
_securityManager->wrapCallback(
|
||||
[this](PsychicRequest *request, JsonVariant &json)
|
||||
{
|
||||
if (!json.is<JsonObject>())
|
||||
{
|
||||
return request->reply(400);
|
||||
}
|
||||
|
||||
JsonObject jsonObject = json.as<JsonObject>();
|
||||
StateUpdateResult outcome = _statefulService->updateWithoutPropagation(jsonObject, _stateUpdater);
|
||||
|
||||
if (outcome == StateUpdateResult::ERROR)
|
||||
{
|
||||
return request->reply(400);
|
||||
}
|
||||
else if ((outcome == StateUpdateResult::CHANGED))
|
||||
{
|
||||
// persist the changes to the FS
|
||||
_statefulService->callUpdateHandlers(HTTP_ENDPOINT_ORIGIN_ID);
|
||||
}
|
||||
|
||||
PsychicJsonResponse response = PsychicJsonResponse(request, false, _bufferSize);
|
||||
jsonObject = response.getRoot();
|
||||
|
||||
_statefulService->read(jsonObject, _stateReader);
|
||||
|
||||
return response.send();
|
||||
},
|
||||
_authenticationPredicate));
|
||||
|
||||
ESP_LOGV("HttpEndpoint", "Registered POST endpoint: %s", _servicePath);
|
||||
}
|
||||
};
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,35 @@
|
||||
#ifndef IPUtils_h
|
||||
#define IPUtils_h
|
||||
|
||||
/**
|
||||
* ESP32 SvelteKit
|
||||
*
|
||||
* A simple, secure and extensible framework for IoT projects for ESP32 platforms
|
||||
* with responsive Sveltekit front-end built with TailwindCSS and DaisyUI.
|
||||
* https://github.com/theelims/ESP32-sveltekit
|
||||
*
|
||||
* Copyright (C) 2018 - 2023 rjwats
|
||||
* Copyright (C) 2023 theelims
|
||||
*
|
||||
* All Rights Reserved. This software may be modified and distributed under
|
||||
* the terms of the LGPL v3 license. See the LICENSE file for details.
|
||||
**/
|
||||
|
||||
#include <IPAddress.h>
|
||||
|
||||
const IPAddress IP_NOT_SET = IPAddress(INADDR_NONE);
|
||||
|
||||
class IPUtils
|
||||
{
|
||||
public:
|
||||
static bool isSet(const IPAddress &ip)
|
||||
{
|
||||
return ip != IP_NOT_SET;
|
||||
}
|
||||
static bool isNotSet(const IPAddress &ip)
|
||||
{
|
||||
return ip == IP_NOT_SET;
|
||||
}
|
||||
};
|
||||
|
||||
#endif // end IPUtils_h
|
||||
@@ -0,0 +1,50 @@
|
||||
#ifndef JsonUtils_h
|
||||
#define JsonUtils_h
|
||||
|
||||
/**
|
||||
* ESP32 SvelteKit
|
||||
*
|
||||
* A simple, secure and extensible framework for IoT projects for ESP32 platforms
|
||||
* with responsive Sveltekit front-end built with TailwindCSS and DaisyUI.
|
||||
* https://github.com/theelims/ESP32-sveltekit
|
||||
*
|
||||
* Copyright (C) 2018 - 2023 rjwats
|
||||
* Copyright (C) 2023 theelims
|
||||
*
|
||||
* All Rights Reserved. This software may be modified and distributed under
|
||||
* the terms of the LGPL v3 license. See the LICENSE file for details.
|
||||
**/
|
||||
|
||||
#include <Arduino.h>
|
||||
#include <IPUtils.h>
|
||||
#include <ArduinoJson.h>
|
||||
|
||||
class JsonUtils
|
||||
{
|
||||
public:
|
||||
static void readIP(JsonObject &root, const String &key, IPAddress &ip, const String &def)
|
||||
{
|
||||
IPAddress defaultIp = {};
|
||||
if (!defaultIp.fromString(def))
|
||||
{
|
||||
defaultIp = INADDR_NONE;
|
||||
}
|
||||
readIP(root, key, ip, defaultIp);
|
||||
}
|
||||
static void readIP(JsonObject &root, const String &key, IPAddress &ip, const IPAddress &defaultIp = INADDR_NONE)
|
||||
{
|
||||
if (!root[key].is<String>() || !ip.fromString(root[key].as<String>()))
|
||||
{
|
||||
ip = defaultIp;
|
||||
}
|
||||
}
|
||||
static void writeIP(JsonObject &root, const String &key, const IPAddress &ip)
|
||||
{
|
||||
if (IPUtils::isSet(ip))
|
||||
{
|
||||
root[key] = ip.toString();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
#endif // end JsonUtils
|
||||
@@ -0,0 +1,169 @@
|
||||
ESP32-SvelteKit is distributed with two licenses for different sections of the
|
||||
code. The back end code inherits the GNU LESSER GENERAL PUBLIC LICENSE Version 3
|
||||
and is therefore distributed said license. The front end code is distributed
|
||||
under the MIT License.
|
||||
|
||||
GNU LESSER GENERAL PUBLIC LICENSE
|
||||
Version 3, 29 June 2007
|
||||
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
This version of the GNU Lesser General Public License incorporates
|
||||
the terms and conditions of version 3 of the GNU General Public
|
||||
License, supplemented by the additional permissions listed below.
|
||||
|
||||
0. Additional Definitions.
|
||||
|
||||
As used herein, "this License" refers to version 3 of the GNU Lesser
|
||||
General Public License, and the "GNU GPL" refers to version 3 of the GNU
|
||||
General Public License.
|
||||
|
||||
"The Library" refers to a covered work governed by this License,
|
||||
other than an Application or a Combined Work as defined below.
|
||||
|
||||
An "Application" is any work that makes use of an interface provided
|
||||
by the Library, but which is not otherwise based on the Library.
|
||||
Defining a subclass of a class defined by the Library is deemed a mode
|
||||
of using an interface provided by the Library.
|
||||
|
||||
A "Combined Work" is a work produced by combining or linking an
|
||||
Application with the Library. The particular version of the Library
|
||||
with which the Combined Work was made is also called the "Linked
|
||||
Version".
|
||||
|
||||
The "Minimal Corresponding Source" for a Combined Work means the
|
||||
Corresponding Source for the Combined Work, excluding any source code
|
||||
for portions of the Combined Work that, considered in isolation, are
|
||||
based on the Application, and not on the Linked Version.
|
||||
|
||||
The "Corresponding Application Code" for a Combined Work means the
|
||||
object code and/or source code for the Application, including any data
|
||||
and utility programs needed for reproducing the Combined Work from the
|
||||
Application, but excluding the System Libraries of the Combined Work.
|
||||
|
||||
1. Exception to Section 3 of the GNU GPL.
|
||||
|
||||
You may convey a covered work under sections 3 and 4 of this License
|
||||
without being bound by section 3 of the GNU GPL.
|
||||
|
||||
2. Conveying Modified Versions.
|
||||
|
||||
If you modify a copy of the Library, and, in your modifications, a
|
||||
facility refers to a function or data to be supplied by an Application
|
||||
that uses the facility (other than as an argument passed when the
|
||||
facility is invoked), then you may convey a copy of the modified
|
||||
version:
|
||||
|
||||
a) under this License, provided that you make a good faith effort to
|
||||
ensure that, in the event an Application does not supply the
|
||||
function or data, the facility still operates, and performs
|
||||
whatever part of its purpose remains meaningful, or
|
||||
|
||||
b) under the GNU GPL, with none of the additional permissions of
|
||||
this License applicable to that copy.
|
||||
|
||||
3. Object Code Incorporating Material from Library Header Files.
|
||||
|
||||
The object code form of an Application may incorporate material from
|
||||
a header file that is part of the Library. You may convey such object
|
||||
code under terms of your choice, provided that, if the incorporated
|
||||
material is not limited to numerical parameters, data structure
|
||||
layouts and accessors, or small macros, inline functions and templates
|
||||
(ten or fewer lines in length), you do both of the following:
|
||||
|
||||
a) Give prominent notice with each copy of the object code that the
|
||||
Library is used in it and that the Library and its use are
|
||||
covered by this License.
|
||||
|
||||
b) Accompany the object code with a copy of the GNU GPL and this license
|
||||
document.
|
||||
|
||||
4. Combined Works.
|
||||
|
||||
You may convey a Combined Work under terms of your choice that,
|
||||
taken together, effectively do not restrict modification of the
|
||||
portions of the Library contained in the Combined Work and reverse
|
||||
engineering for debugging such modifications, if you also do each of
|
||||
the following:
|
||||
|
||||
a) Give prominent notice with each copy of the Combined Work that
|
||||
the Library is used in it and that the Library and its use are
|
||||
covered by this License.
|
||||
|
||||
b) Accompany the Combined Work with a copy of the GNU GPL and this license
|
||||
document.
|
||||
|
||||
c) For a Combined Work that displays copyright notices during
|
||||
execution, include the copyright notice for the Library among
|
||||
these notices, as well as a reference directing the user to the
|
||||
copies of the GNU GPL and this license document.
|
||||
|
||||
d) Do one of the following:
|
||||
|
||||
0) Convey the Minimal Corresponding Source under the terms of this
|
||||
License, and the Corresponding Application Code in a form
|
||||
suitable for, and under terms that permit, the user to
|
||||
recombine or relink the Application with a modified version of
|
||||
the Linked Version to produce a modified Combined Work, in the
|
||||
manner specified by section 6 of the GNU GPL for conveying
|
||||
Corresponding Source.
|
||||
|
||||
1) Use a suitable shared library mechanism for linking with the
|
||||
Library. A suitable mechanism is one that (a) uses at run time
|
||||
a copy of the Library already present on the user's computer
|
||||
system, and (b) will operate properly with a modified version
|
||||
of the Library that is interface-compatible with the Linked
|
||||
Version.
|
||||
|
||||
e) Provide Installation Information, but only if you would otherwise
|
||||
be required to provide such information under section 6 of the
|
||||
GNU GPL, and only to the extent that such information is
|
||||
necessary to install and execute a modified version of the
|
||||
Combined Work produced by recombining or relinking the
|
||||
Application with a modified version of the Linked Version. (If
|
||||
you use option 4d0, the Installation Information must accompany
|
||||
the Minimal Corresponding Source and Corresponding Application
|
||||
Code. If you use option 4d1, you must provide the Installation
|
||||
Information in the manner specified by section 6 of the GNU GPL
|
||||
for conveying Corresponding Source.)
|
||||
|
||||
5. Combined Libraries.
|
||||
|
||||
You may place library facilities that are a work based on the
|
||||
Library side by side in a single library together with other library
|
||||
facilities that are not Applications and are not covered by this
|
||||
License, and convey such a combined library under terms of your
|
||||
choice, if you do both of the following:
|
||||
|
||||
a) Accompany the combined library with a copy of the same work based
|
||||
on the Library, uncombined with any other library facilities,
|
||||
conveyed under the terms of this License.
|
||||
|
||||
b) Give prominent notice with the combined library that part of it
|
||||
is a work based on the Library, and explaining where to find the
|
||||
accompanying uncombined form of the same work.
|
||||
|
||||
6. Revised Versions of the GNU Lesser General Public License.
|
||||
|
||||
The Free Software Foundation may publish revised and/or new versions
|
||||
of the GNU Lesser General Public License from time to time. Such new
|
||||
versions will be similar in spirit to the present version, but may
|
||||
differ in detail to address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the
|
||||
Library as you received it specifies that a certain numbered version
|
||||
of the GNU Lesser General Public License "or any later version"
|
||||
applies to it, you have the option of following the terms and
|
||||
conditions either of that published version or of any later version
|
||||
published by the Free Software Foundation. If the Library as you
|
||||
received it does not specify a version number of the GNU Lesser
|
||||
General Public License, you may choose any version of the GNU Lesser
|
||||
General Public License ever published by the Free Software Foundation.
|
||||
|
||||
If the Library as you received it specifies that a proxy can decide
|
||||
whether future versions of the GNU Lesser General Public License shall
|
||||
apply, that proxy's public statement of acceptance of any version is
|
||||
permanent authorization for you to choose that version for the
|
||||
Library.
|
||||
@@ -0,0 +1,163 @@
|
||||
#ifndef MqttEndpoint_h
|
||||
#define MqttEndpoint_h
|
||||
|
||||
/**
|
||||
* ESP32 SvelteKit
|
||||
*
|
||||
* A simple, secure and extensible framework for IoT projects for ESP32 platforms
|
||||
* with responsive Sveltekit front-end built with TailwindCSS and DaisyUI.
|
||||
* https://github.com/theelims/ESP32-sveltekit
|
||||
*
|
||||
* Copyright (C) 2018 - 2023 rjwats
|
||||
* Copyright (C) 2023 - 2024 theelims
|
||||
*
|
||||
* All Rights Reserved. This software may be modified and distributed under
|
||||
* the terms of the LGPL v3 license. See the LICENSE file for details.
|
||||
**/
|
||||
|
||||
#include <StatefulService.h>
|
||||
#include <PsychicMqttClient.h>
|
||||
|
||||
#define MQTT_ORIGIN_ID "mqtt"
|
||||
|
||||
template <class T>
|
||||
class MqttEndpoint
|
||||
{
|
||||
public:
|
||||
MqttEndpoint(JsonStateReader<T> stateReader,
|
||||
JsonStateUpdater<T> stateUpdater,
|
||||
StatefulService<T> *statefulService,
|
||||
PsychicMqttClient *mqttClient,
|
||||
const String &pubTopic = "",
|
||||
const String &subTopic = "",
|
||||
bool retain = false,
|
||||
size_t bufferSize = DEFAULT_BUFFER_SIZE) : _stateReader(stateReader),
|
||||
_stateUpdater(stateUpdater),
|
||||
_statefulService(statefulService),
|
||||
_mqttClient(mqttClient),
|
||||
_pubTopic(pubTopic),
|
||||
_subTopic(subTopic),
|
||||
_retain(retain),
|
||||
_bufferSize(bufferSize)
|
||||
|
||||
{
|
||||
_statefulService->addUpdateHandler([&](const String &originId)
|
||||
{ publish(); },
|
||||
false);
|
||||
|
||||
_mqttClient->onConnect(std::bind(&MqttEndpoint::onConnect, this));
|
||||
|
||||
_mqttClient->onMessage(std::bind(&MqttEndpoint::onMqttMessage,
|
||||
this,
|
||||
std::placeholders::_1,
|
||||
std::placeholders::_2,
|
||||
std::placeholders::_3,
|
||||
std::placeholders::_4,
|
||||
std::placeholders::_5));
|
||||
}
|
||||
|
||||
public:
|
||||
void configureTopics(const String &pubTopic, const String &subTopic)
|
||||
{
|
||||
setSubTopic(subTopic);
|
||||
setPubTopic(pubTopic);
|
||||
}
|
||||
|
||||
void setSubTopic(const String &subTopic)
|
||||
{
|
||||
if (!_subTopic.equals(subTopic))
|
||||
{
|
||||
// unsubscribe from the existing topic if one was set
|
||||
if (_subTopic.length() > 0)
|
||||
{
|
||||
_mqttClient->unsubscribe(_subTopic.c_str());
|
||||
}
|
||||
// set the new topic and re-configure the subscription
|
||||
_subTopic = subTopic;
|
||||
subscribe();
|
||||
}
|
||||
}
|
||||
|
||||
void setPubTopic(const String &pubTopic)
|
||||
{
|
||||
_pubTopic = pubTopic;
|
||||
publish();
|
||||
}
|
||||
|
||||
void setRetain(const bool retain)
|
||||
{
|
||||
_retain = retain;
|
||||
publish();
|
||||
}
|
||||
|
||||
void publish()
|
||||
{
|
||||
if (_pubTopic.length() > 0 && _mqttClient->connected())
|
||||
{
|
||||
// serialize to json doc
|
||||
DynamicJsonDocument json(_bufferSize);
|
||||
JsonObject jsonObject = json.to<JsonObject>();
|
||||
_statefulService->read(jsonObject, _stateReader);
|
||||
|
||||
// serialize to string
|
||||
String payload;
|
||||
serializeJson(json, payload);
|
||||
|
||||
// publish the payload
|
||||
_mqttClient->publish(_pubTopic.c_str(), 0, _retain, payload.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
PsychicMqttClient *getMqttClient()
|
||||
{
|
||||
return _mqttClient;
|
||||
}
|
||||
|
||||
protected:
|
||||
StatefulService<T> *_statefulService;
|
||||
PsychicMqttClient *_mqttClient;
|
||||
int _bufferSize;
|
||||
JsonStateUpdater<T> _stateUpdater;
|
||||
JsonStateReader<T> _stateReader;
|
||||
String _subTopic;
|
||||
String _pubTopic;
|
||||
bool _retain;
|
||||
|
||||
void onMqttMessage(char *topic,
|
||||
char *payload,
|
||||
int retain,
|
||||
int qos,
|
||||
bool dup)
|
||||
{
|
||||
// we only care about the topic we are watching in this class
|
||||
if (strcmp(_subTopic.c_str(), topic))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// deserialize from string
|
||||
DynamicJsonDocument json(_bufferSize);
|
||||
DeserializationError error = deserializeJson(json, payload);
|
||||
if (!error && json.is<JsonObject>())
|
||||
{
|
||||
JsonObject jsonObject = json.as<JsonObject>();
|
||||
_statefulService->update(jsonObject, _stateUpdater, MQTT_ORIGIN_ID);
|
||||
}
|
||||
}
|
||||
|
||||
void onConnect()
|
||||
{
|
||||
subscribe();
|
||||
publish();
|
||||
}
|
||||
|
||||
void subscribe()
|
||||
{
|
||||
if (_subTopic.length() > 0)
|
||||
{
|
||||
_mqttClient->subscribe(_subTopic.c_str(), 2);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
#endif // end MqttEndpoint
|
||||
@@ -0,0 +1,163 @@
|
||||
#ifndef MqttPubSub_h
|
||||
#define MqttPubSub_h
|
||||
|
||||
/**
|
||||
* ESP32 SvelteKit
|
||||
*
|
||||
* A simple, secure and extensible framework for IoT projects for ESP32 platforms
|
||||
* with responsive Sveltekit front-end built with TailwindCSS and DaisyUI.
|
||||
* https://github.com/theelims/ESP32-sveltekit
|
||||
*
|
||||
* Copyright (C) 2018 - 2023 rjwats
|
||||
* Copyright (C) 2023 - 2024 theelims
|
||||
*
|
||||
* All Rights Reserved. This software may be modified and distributed under
|
||||
* the terms of the LGPL v3 license. See the LICENSE file for details.
|
||||
**/
|
||||
|
||||
#include <StatefulService.h>
|
||||
#include <PsychicMqttClient.h>
|
||||
|
||||
#define MQTT_ORIGIN_ID "mqtt"
|
||||
|
||||
template <class T>
|
||||
class MqttPubSub
|
||||
{
|
||||
public:
|
||||
MqttPubSub(JsonStateReader<T> stateReader,
|
||||
JsonStateUpdater<T> stateUpdater,
|
||||
StatefulService<T> *statefulService,
|
||||
PsychicMqttClient *mqttClient,
|
||||
const String &pubTopic = "",
|
||||
const String &subTopic = "",
|
||||
bool retain = false,
|
||||
size_t bufferSize = DEFAULT_BUFFER_SIZE) : _stateReader(stateReader),
|
||||
_stateUpdater(stateUpdater),
|
||||
_statefulService(statefulService),
|
||||
_mqttClient(mqttClient),
|
||||
_pubTopic(pubTopic),
|
||||
_subTopic(subTopic),
|
||||
_retain(retain),
|
||||
_bufferSize(bufferSize)
|
||||
|
||||
{
|
||||
_statefulService->addUpdateHandler([&](const String &originId)
|
||||
{ publish(); },
|
||||
false);
|
||||
|
||||
_mqttClient->onConnect(std::bind(&MqttPubSub::onConnect, this));
|
||||
|
||||
_mqttClient->onMessage(std::bind(&MqttPubSub::onMqttMessage,
|
||||
this,
|
||||
std::placeholders::_1,
|
||||
std::placeholders::_2,
|
||||
std::placeholders::_3,
|
||||
std::placeholders::_4,
|
||||
std::placeholders::_5));
|
||||
}
|
||||
|
||||
public:
|
||||
void configureTopics(const String &pubTopic, const String &subTopic)
|
||||
{
|
||||
setSubTopic(subTopic);
|
||||
setPubTopic(pubTopic);
|
||||
}
|
||||
|
||||
void setSubTopic(const String &subTopic)
|
||||
{
|
||||
if (!_subTopic.equals(subTopic))
|
||||
{
|
||||
// unsubscribe from the existing topic if one was set
|
||||
if (_subTopic.length() > 0)
|
||||
{
|
||||
_mqttClient->unsubscribe(_subTopic.c_str());
|
||||
}
|
||||
// set the new topic and re-configure the subscription
|
||||
_subTopic = subTopic;
|
||||
subscribe();
|
||||
}
|
||||
}
|
||||
|
||||
void setPubTopic(const String &pubTopic)
|
||||
{
|
||||
_pubTopic = pubTopic;
|
||||
publish();
|
||||
}
|
||||
|
||||
void setRetain(const bool retain)
|
||||
{
|
||||
_retain = retain;
|
||||
publish();
|
||||
}
|
||||
|
||||
void publish()
|
||||
{
|
||||
if (_pubTopic.length() > 0 && _mqttClient->connected())
|
||||
{
|
||||
// serialize to json doc
|
||||
DynamicJsonDocument json(_bufferSize);
|
||||
JsonObject jsonObject = json.to<JsonObject>();
|
||||
_statefulService->read(jsonObject, _stateReader);
|
||||
|
||||
// serialize to string
|
||||
String payload;
|
||||
serializeJson(json, payload);
|
||||
|
||||
// publish the payload
|
||||
_mqttClient->publish(_pubTopic.c_str(), 0, _retain, payload.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
PsychicMqttClient *getMqttClient()
|
||||
{
|
||||
return _mqttClient;
|
||||
}
|
||||
|
||||
protected:
|
||||
StatefulService<T> *_statefulService;
|
||||
PsychicMqttClient *_mqttClient;
|
||||
int _bufferSize;
|
||||
JsonStateUpdater<T> _stateUpdater;
|
||||
JsonStateReader<T> _stateReader;
|
||||
String _subTopic;
|
||||
String _pubTopic;
|
||||
bool _retain;
|
||||
|
||||
void onMqttMessage(char *topic,
|
||||
char *payload,
|
||||
int retain,
|
||||
int qos,
|
||||
bool dup)
|
||||
{
|
||||
// we only care about the topic we are watching in this class
|
||||
if (strcmp(_subTopic.c_str(), topic))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// deserialize from string
|
||||
DynamicJsonDocument json(_bufferSize);
|
||||
DeserializationError error = deserializeJson(json, payload);
|
||||
if (!error && json.is<JsonObject>())
|
||||
{
|
||||
JsonObject jsonObject = json.as<JsonObject>();
|
||||
_statefulService->update(jsonObject, _stateUpdater, MQTT_ORIGIN_ID);
|
||||
}
|
||||
}
|
||||
|
||||
void onConnect()
|
||||
{
|
||||
subscribe();
|
||||
publish();
|
||||
}
|
||||
|
||||
void subscribe()
|
||||
{
|
||||
if (_subTopic.length() > 0)
|
||||
{
|
||||
_mqttClient->subscribe(_subTopic.c_str(), 2);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
#endif // end MqttPubSub
|
||||
@@ -0,0 +1,195 @@
|
||||
/**
|
||||
* ESP32 SvelteKit
|
||||
*
|
||||
* A simple, secure and extensible framework for IoT projects for ESP32 platforms
|
||||
* with responsive Sveltekit front-end built with TailwindCSS and DaisyUI.
|
||||
* https://github.com/theelims/ESP32-sveltekit
|
||||
*
|
||||
* Copyright (C) 2018 - 2023 rjwats
|
||||
* Copyright (C) 2023 theelims
|
||||
*
|
||||
* All Rights Reserved. This software may be modified and distributed under
|
||||
* the terms of the LGPL v3 license. See the LICENSE file for details.
|
||||
**/
|
||||
|
||||
#include <MqttSettingsService.h>
|
||||
|
||||
extern const uint8_t rootca_crt_bundle_start[] asm("_binary_src_certs_x509_crt_bundle_bin_start");
|
||||
|
||||
/**
|
||||
* Retains a copy of the cstr provided in the pointer provided using dynamic allocation.
|
||||
*
|
||||
* Frees the pointer before allocation and leaves it as nullptr if cstr == nullptr.
|
||||
*/
|
||||
static char *retainCstr(const char *cstr, char **ptr)
|
||||
{
|
||||
// free up previously retained value if exists
|
||||
free(*ptr);
|
||||
*ptr = nullptr;
|
||||
|
||||
// dynamically allocate and copy cstr (if non null)
|
||||
if (cstr != nullptr)
|
||||
{
|
||||
*ptr = (char *)malloc(strlen(cstr) + 1);
|
||||
strcpy(*ptr, cstr);
|
||||
}
|
||||
|
||||
// return reference to pointer for convenience
|
||||
return *ptr;
|
||||
}
|
||||
|
||||
MqttSettingsService::MqttSettingsService(PsychicHttpServer *server,
|
||||
FS *fs,
|
||||
SecurityManager *securityManager) : _server(server),
|
||||
_securityManager(securityManager),
|
||||
_httpEndpoint(MqttSettings::read, MqttSettings::update, this, server, MQTT_SETTINGS_SERVICE_PATH, securityManager),
|
||||
_fsPersistence(MqttSettings::read, MqttSettings::update, this, fs, MQTT_SETTINGS_FILE),
|
||||
_retainedHost(nullptr),
|
||||
_retainedClientId(nullptr),
|
||||
_retainedUsername(nullptr),
|
||||
_retainedPassword(nullptr),
|
||||
_reconfigureMqtt(false),
|
||||
_mqttClient(),
|
||||
_lastError("None")
|
||||
{
|
||||
addUpdateHandler([&](const String &originId)
|
||||
{ onConfigUpdated(); },
|
||||
false);
|
||||
|
||||
_mqttClient.setCACertBundle(rootca_crt_bundle_start);
|
||||
}
|
||||
|
||||
MqttSettingsService::~MqttSettingsService()
|
||||
{
|
||||
}
|
||||
|
||||
void MqttSettingsService::begin()
|
||||
{
|
||||
WiFi.onEvent(
|
||||
std::bind(&MqttSettingsService::onStationModeDisconnected, this, std::placeholders::_1, std::placeholders::_2),
|
||||
WiFiEvent_t::ARDUINO_EVENT_WIFI_STA_DISCONNECTED);
|
||||
WiFi.onEvent(std::bind(&MqttSettingsService::onStationModeGotIP, this, std::placeholders::_1, std::placeholders::_2),
|
||||
WiFiEvent_t::ARDUINO_EVENT_WIFI_STA_GOT_IP);
|
||||
_mqttClient.onConnect(std::bind(&MqttSettingsService::onMqttConnect, this, std::placeholders::_1));
|
||||
_mqttClient.onDisconnect(std::bind(&MqttSettingsService::onMqttDisconnect, this, std::placeholders::_1));
|
||||
_mqttClient.onError(std::bind(&MqttSettingsService::onMqttError, this, std::placeholders::_1));
|
||||
|
||||
_httpEndpoint.begin();
|
||||
_fsPersistence.readFromFS();
|
||||
}
|
||||
|
||||
void MqttSettingsService::loop()
|
||||
{
|
||||
if (_reconfigureMqtt)
|
||||
{
|
||||
// reconfigure MQTT client
|
||||
configureMqtt();
|
||||
|
||||
// clear the reconnection flags
|
||||
_reconfigureMqtt = false;
|
||||
}
|
||||
}
|
||||
|
||||
bool MqttSettingsService::isEnabled()
|
||||
{
|
||||
return _state.enabled;
|
||||
}
|
||||
|
||||
bool MqttSettingsService::isConnected()
|
||||
{
|
||||
return _mqttClient.connected();
|
||||
}
|
||||
|
||||
const char *MqttSettingsService::getClientId()
|
||||
{
|
||||
// return _mqttClient.getClientId();
|
||||
return _state.clientId.c_str();
|
||||
}
|
||||
|
||||
PsychicMqttClient *MqttSettingsService::getMqttClient()
|
||||
{
|
||||
return &_mqttClient;
|
||||
}
|
||||
|
||||
String MqttSettingsService::getLastError()
|
||||
{
|
||||
return _lastError;
|
||||
}
|
||||
|
||||
void MqttSettingsService::onMqttConnect(bool sessionPresent)
|
||||
{
|
||||
ESP_LOGI("MQTT", "Connected to MQTT: %s", _mqttClient.getMqttConfig()->uri);
|
||||
#ifdef SERIAL_INFO
|
||||
Serial.printf("Connected to MQTT: %s\n", _mqttClient.getMqttConfig()->uri);
|
||||
#endif
|
||||
_lastError = "None";
|
||||
}
|
||||
|
||||
void MqttSettingsService::onMqttDisconnect(bool sessionPresent)
|
||||
{
|
||||
ESP_LOGI("MQTT", "Disconnected from MQTT.");
|
||||
#ifdef SERIAL_INFO
|
||||
Serial.println("Disconnected from MQTT.");
|
||||
#endif
|
||||
}
|
||||
|
||||
void MqttSettingsService::onMqttError(esp_mqtt_error_codes_t error)
|
||||
{
|
||||
if (error.error_type == MQTT_ERROR_TYPE_TCP_TRANSPORT)
|
||||
{
|
||||
_lastError = strerror(error.esp_transport_sock_errno);
|
||||
ESP_LOGE("MQTT", "MQTT TCP error: %s", _lastError.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
void MqttSettingsService::onConfigUpdated()
|
||||
{
|
||||
_reconfigureMqtt = true;
|
||||
}
|
||||
|
||||
void MqttSettingsService::onStationModeGotIP(WiFiEvent_t event, WiFiEventInfo_t info)
|
||||
{
|
||||
if (_state.enabled)
|
||||
{
|
||||
ESP_LOGI("MQTT", "WiFi connection established, starting MQTT client.");
|
||||
onConfigUpdated();
|
||||
}
|
||||
}
|
||||
|
||||
void MqttSettingsService::onStationModeDisconnected(WiFiEvent_t event, WiFiEventInfo_t info)
|
||||
{
|
||||
if (_state.enabled)
|
||||
{
|
||||
ESP_LOGI("MQTT", "WiFi connection dropped, stopping MQTT client.");
|
||||
onConfigUpdated();
|
||||
}
|
||||
}
|
||||
|
||||
void MqttSettingsService::configureMqtt()
|
||||
{
|
||||
// disconnect if currently connected
|
||||
_mqttClient.disconnect();
|
||||
|
||||
// only connect if WiFi is connected and MQTT is enabled
|
||||
if (_state.enabled && WiFi.isConnected())
|
||||
{
|
||||
#ifdef SERIAL_INFO
|
||||
Serial.println("Connecting to MQTT...");
|
||||
#endif
|
||||
_mqttClient.setServer(retainCstr(_state.uri.c_str(), &_retainedHost));
|
||||
if (_state.username.length() > 0)
|
||||
{
|
||||
_mqttClient.setCredentials(
|
||||
retainCstr(_state.username.c_str(), &_retainedUsername),
|
||||
retainCstr(_state.password.length() > 0 ? _state.password.c_str() : nullptr, &_retainedPassword));
|
||||
}
|
||||
else
|
||||
{
|
||||
_mqttClient.setCredentials(retainCstr(nullptr, &_retainedUsername), retainCstr(nullptr, &_retainedPassword));
|
||||
}
|
||||
_mqttClient.setClientId(retainCstr(_state.clientId.c_str(), &_retainedClientId));
|
||||
_mqttClient.setKeepAlive(_state.keepAlive);
|
||||
_mqttClient.setCleanSession(_state.cleanSession);
|
||||
_mqttClient.connect();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
#ifndef MqttSettingsService_h
|
||||
#define MqttSettingsService_h
|
||||
|
||||
/**
|
||||
* ESP32 SvelteKit
|
||||
*
|
||||
* A simple, secure and extensible framework for IoT projects for ESP32 platforms
|
||||
* with responsive Sveltekit front-end built with TailwindCSS and DaisyUI.
|
||||
* https://github.com/theelims/ESP32-sveltekit
|
||||
*
|
||||
* Copyright (C) 2018 - 2023 rjwats
|
||||
* Copyright (C) 2023 theelims
|
||||
*
|
||||
* All Rights Reserved. This software may be modified and distributed under
|
||||
* the terms of the LGPL v3 license. See the LICENSE file for details.
|
||||
**/
|
||||
|
||||
#include <StatefulService.h>
|
||||
#include <HttpEndpoint.h>
|
||||
#include <FSPersistence.h>
|
||||
#include <PsychicMqttClient.h>
|
||||
#include <SettingValue.h>
|
||||
#include <WiFi.h>
|
||||
|
||||
#ifndef FACTORY_MQTT_ENABLED
|
||||
#define FACTORY_MQTT_ENABLED false
|
||||
#endif
|
||||
|
||||
#ifndef FACTORY_MQTT_HOST
|
||||
#define FACTORY_MQTT_HOST "test.mosquitto.org"
|
||||
#endif
|
||||
|
||||
#ifndef FACTORY_MQTT_PORT
|
||||
#define FACTORY_MQTT_PORT 1883
|
||||
#endif
|
||||
|
||||
#ifndef FACTORY_MQTT_USERNAME
|
||||
#define FACTORY_MQTT_USERNAME ""
|
||||
#endif
|
||||
|
||||
#ifndef FACTORY_MQTT_PASSWORD
|
||||
#define FACTORY_MQTT_PASSWORD ""
|
||||
#endif
|
||||
|
||||
#ifndef FACTORY_MQTT_CLIENT_ID
|
||||
#define FACTORY_MQTT_CLIENT_ID "#{platform}-#{unique_id}"
|
||||
#endif
|
||||
|
||||
#ifndef FACTORY_MQTT_KEEP_ALIVE
|
||||
#define FACTORY_MQTT_KEEP_ALIVE 16
|
||||
#endif
|
||||
|
||||
#ifndef FACTORY_MQTT_CLEAN_SESSION
|
||||
#define FACTORY_MQTT_CLEAN_SESSION true
|
||||
#endif
|
||||
|
||||
#ifndef FACTORY_MQTT_MAX_TOPIC_LENGTH
|
||||
#define FACTORY_MQTT_MAX_TOPIC_LENGTH 128
|
||||
#endif
|
||||
|
||||
#define MQTT_SETTINGS_FILE "/config/mqttSettings.json"
|
||||
#define MQTT_SETTINGS_SERVICE_PATH "/rest/mqttSettings"
|
||||
|
||||
#define MQTT_RECONNECTION_DELAY 5000
|
||||
|
||||
class MqttSettings
|
||||
{
|
||||
public:
|
||||
// host and port - if enabled
|
||||
bool enabled;
|
||||
String uri;
|
||||
|
||||
// username and password
|
||||
String username;
|
||||
String password;
|
||||
|
||||
// client id settings
|
||||
String clientId;
|
||||
|
||||
// connection settings
|
||||
uint16_t keepAlive;
|
||||
bool cleanSession;
|
||||
|
||||
static void read(MqttSettings &settings, JsonObject &root)
|
||||
{
|
||||
root["enabled"] = settings.enabled;
|
||||
root["uri"] = settings.uri;
|
||||
root["username"] = settings.username;
|
||||
root["password"] = settings.password;
|
||||
root["client_id"] = settings.clientId;
|
||||
root["keep_alive"] = settings.keepAlive;
|
||||
root["clean_session"] = settings.cleanSession;
|
||||
}
|
||||
|
||||
static StateUpdateResult update(JsonObject &root, MqttSettings &settings)
|
||||
{
|
||||
settings.enabled = root["enabled"] | FACTORY_MQTT_ENABLED;
|
||||
settings.uri = root["uri"] | FACTORY_MQTT_URI;
|
||||
settings.username = root["username"] | SettingValue::format(FACTORY_MQTT_USERNAME);
|
||||
settings.password = root["password"] | FACTORY_MQTT_PASSWORD;
|
||||
settings.clientId = root["client_id"] | SettingValue::format(FACTORY_MQTT_CLIENT_ID);
|
||||
settings.keepAlive = root["keep_alive"] | FACTORY_MQTT_KEEP_ALIVE;
|
||||
settings.cleanSession = root["clean_session"] | FACTORY_MQTT_CLEAN_SESSION;
|
||||
return StateUpdateResult::CHANGED;
|
||||
}
|
||||
};
|
||||
|
||||
class MqttSettingsService : public StatefulService<MqttSettings>
|
||||
{
|
||||
public:
|
||||
MqttSettingsService(PsychicHttpServer *server, FS *fs, SecurityManager *securityManager);
|
||||
~MqttSettingsService();
|
||||
|
||||
void begin();
|
||||
void loop();
|
||||
bool isEnabled();
|
||||
bool isConnected();
|
||||
const char *getClientId();
|
||||
String getLastError();
|
||||
PsychicMqttClient *getMqttClient();
|
||||
|
||||
protected:
|
||||
void onConfigUpdated();
|
||||
|
||||
private:
|
||||
PsychicHttpServer *_server;
|
||||
SecurityManager *_securityManager;
|
||||
HttpEndpoint<MqttSettings> _httpEndpoint;
|
||||
FSPersistence<MqttSettings> _fsPersistence;
|
||||
|
||||
// Pointers to hold retained copies of the mqtt client connection strings.
|
||||
// This is required as AsyncMqttClient holds references to the supplied connection strings.
|
||||
char *_retainedHost;
|
||||
char *_retainedClientId;
|
||||
char *_retainedUsername;
|
||||
char *_retainedPassword;
|
||||
|
||||
// variable to help manage connection
|
||||
bool _reconfigureMqtt;
|
||||
String _lastError;
|
||||
|
||||
// the MQTT client instance
|
||||
PsychicMqttClient _mqttClient;
|
||||
|
||||
void onStationModeGotIP(WiFiEvent_t event, WiFiEventInfo_t info);
|
||||
void onStationModeDisconnected(WiFiEvent_t event, WiFiEventInfo_t info);
|
||||
|
||||
void onMqttConnect(bool sessionPresent);
|
||||
void onMqttDisconnect(bool sessionPresent);
|
||||
void onMqttError(esp_mqtt_error_codes_t error);
|
||||
void configureMqtt();
|
||||
};
|
||||
|
||||
#endif // end MqttSettingsService_h
|
||||
@@ -0,0 +1,46 @@
|
||||
/**
|
||||
* ESP32 SvelteKit
|
||||
*
|
||||
* A simple, secure and extensible framework for IoT projects for ESP32 platforms
|
||||
* with responsive Sveltekit front-end built with TailwindCSS and DaisyUI.
|
||||
* https://github.com/theelims/ESP32-sveltekit
|
||||
*
|
||||
* Copyright (C) 2018 - 2023 rjwats
|
||||
* Copyright (C) 2023 theelims
|
||||
*
|
||||
* All Rights Reserved. This software may be modified and distributed under
|
||||
* the terms of the LGPL v3 license. See the LICENSE file for details.
|
||||
**/
|
||||
|
||||
#include <MqttStatus.h>
|
||||
|
||||
MqttStatus::MqttStatus(PsychicHttpServer *server,
|
||||
MqttSettingsService *mqttSettingsService,
|
||||
SecurityManager *securityManager) : _server(server),
|
||||
_securityManager(securityManager),
|
||||
_mqttSettingsService(mqttSettingsService)
|
||||
{
|
||||
}
|
||||
|
||||
void MqttStatus::begin()
|
||||
{
|
||||
_server->on(MQTT_STATUS_SERVICE_PATH,
|
||||
HTTP_GET,
|
||||
_securityManager->wrapRequest(std::bind(&MqttStatus::mqttStatus, this, std::placeholders::_1),
|
||||
AuthenticationPredicates::IS_AUTHENTICATED));
|
||||
|
||||
ESP_LOGV("MqttStatus", "Registered GET endpoint: %s", MQTT_STATUS_SERVICE_PATH);
|
||||
}
|
||||
|
||||
esp_err_t MqttStatus::mqttStatus(PsychicRequest *request)
|
||||
{
|
||||
PsychicJsonResponse response = PsychicJsonResponse(request, false, MAX_MQTT_STATUS_SIZE);
|
||||
JsonObject root = response.getRoot();
|
||||
|
||||
root["enabled"] = _mqttSettingsService->isEnabled();
|
||||
root["connected"] = _mqttSettingsService->isConnected();
|
||||
root["client_id"] = _mqttSettingsService->getClientId();
|
||||
root["last_error"] = _mqttSettingsService->getLastError();
|
||||
|
||||
return response.send();
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
#ifndef MqttStatus_h
|
||||
#define MqttStatus_h
|
||||
|
||||
/**
|
||||
* ESP32 SvelteKit
|
||||
*
|
||||
* A simple, secure and extensible framework for IoT projects for ESP32 platforms
|
||||
* with responsive Sveltekit front-end built with TailwindCSS and DaisyUI.
|
||||
* https://github.com/theelims/ESP32-sveltekit
|
||||
*
|
||||
* Copyright (C) 2018 - 2023 rjwats
|
||||
* Copyright (C) 2023 theelims
|
||||
*
|
||||
* All Rights Reserved. This software may be modified and distributed under
|
||||
* the terms of the LGPL v3 license. See the LICENSE file for details.
|
||||
**/
|
||||
|
||||
#include <WiFi.h>
|
||||
|
||||
#include <MqttSettingsService.h>
|
||||
#include <ArduinoJson.h>
|
||||
#include <PsychicHttp.h>
|
||||
#include <SecurityManager.h>
|
||||
|
||||
#define MAX_MQTT_STATUS_SIZE 1024
|
||||
#define MQTT_STATUS_SERVICE_PATH "/rest/mqttStatus"
|
||||
|
||||
class MqttStatus
|
||||
{
|
||||
public:
|
||||
MqttStatus(PsychicHttpServer *server, MqttSettingsService *mqttSettingsService, SecurityManager *securityManager);
|
||||
|
||||
void begin();
|
||||
|
||||
private:
|
||||
PsychicHttpServer *_server;
|
||||
SecurityManager *_securityManager;
|
||||
MqttSettingsService *_mqttSettingsService;
|
||||
|
||||
esp_err_t mqttStatus(PsychicRequest *request);
|
||||
};
|
||||
|
||||
#endif // end MqttStatus_h
|
||||
@@ -0,0 +1,99 @@
|
||||
/**
|
||||
* ESP32 SvelteKit
|
||||
*
|
||||
* A simple, secure and extensible framework for IoT projects for ESP32 platforms
|
||||
* with responsive Sveltekit front-end built with TailwindCSS and DaisyUI.
|
||||
* https://github.com/theelims/ESP32-sveltekit
|
||||
*
|
||||
* Copyright (C) 2018 - 2023 rjwats
|
||||
* Copyright (C) 2023 theelims
|
||||
*
|
||||
* All Rights Reserved. This software may be modified and distributed under
|
||||
* the terms of the LGPL v3 license. See the LICENSE file for details.
|
||||
**/
|
||||
|
||||
#include <NTPSettingsService.h>
|
||||
|
||||
NTPSettingsService::NTPSettingsService(PsychicHttpServer *server,
|
||||
FS *fs,
|
||||
SecurityManager *securityManager) : _server(server),
|
||||
_securityManager(securityManager),
|
||||
_httpEndpoint(NTPSettings::read, NTPSettings::update, this, server, NTP_SETTINGS_SERVICE_PATH, securityManager),
|
||||
_fsPersistence(NTPSettings::read, NTPSettings::update, this, fs, NTP_SETTINGS_FILE)
|
||||
{
|
||||
addUpdateHandler([&](const String &originId)
|
||||
{ configureNTP(); },
|
||||
false);
|
||||
}
|
||||
|
||||
void NTPSettingsService::begin()
|
||||
{
|
||||
WiFi.onEvent(
|
||||
std::bind(&NTPSettingsService::onStationModeDisconnected, this, std::placeholders::_1, std::placeholders::_2),
|
||||
WiFiEvent_t::ARDUINO_EVENT_WIFI_STA_DISCONNECTED);
|
||||
WiFi.onEvent(std::bind(&NTPSettingsService::onStationModeGotIP, this, std::placeholders::_1, std::placeholders::_2),
|
||||
WiFiEvent_t::ARDUINO_EVENT_WIFI_STA_GOT_IP);
|
||||
|
||||
_httpEndpoint.begin();
|
||||
_server->on(TIME_PATH,
|
||||
HTTP_POST,
|
||||
_securityManager->wrapCallback(
|
||||
std::bind(&NTPSettingsService::configureTime, this, std::placeholders::_1, std::placeholders::_2),
|
||||
AuthenticationPredicates::IS_ADMIN));
|
||||
|
||||
ESP_LOGV("NTPSettingsService", "Registered POST endpoint: %s", TIME_PATH);
|
||||
|
||||
_fsPersistence.readFromFS();
|
||||
configureNTP();
|
||||
}
|
||||
|
||||
void NTPSettingsService::onStationModeGotIP(WiFiEvent_t event, WiFiEventInfo_t info)
|
||||
{
|
||||
#ifdef SERIAL_INFO
|
||||
Serial.println(F("Got IP address, starting NTP Synchronization"));
|
||||
#endif
|
||||
configureNTP();
|
||||
}
|
||||
|
||||
void NTPSettingsService::onStationModeDisconnected(WiFiEvent_t event, WiFiEventInfo_t info)
|
||||
{
|
||||
#ifdef SERIAL_INFO
|
||||
Serial.println(F("WiFi connection dropped, stopping NTP."));
|
||||
#endif
|
||||
configureNTP();
|
||||
}
|
||||
|
||||
void NTPSettingsService::configureNTP()
|
||||
{
|
||||
if (WiFi.isConnected() && _state.enabled)
|
||||
{
|
||||
#ifdef SERIAL_INFO
|
||||
Serial.println(F("Starting NTP..."));
|
||||
#endif
|
||||
configTzTime(_state.tzFormat.c_str(), _state.server.c_str());
|
||||
}
|
||||
else
|
||||
{
|
||||
setenv("TZ", _state.tzFormat.c_str(), 1);
|
||||
tzset();
|
||||
sntp_stop();
|
||||
}
|
||||
}
|
||||
|
||||
esp_err_t NTPSettingsService::configureTime(PsychicRequest *request, JsonVariant &json)
|
||||
{
|
||||
if (!sntp_enabled() && json.is<JsonObject>())
|
||||
{
|
||||
struct tm tm = {0};
|
||||
String timeLocal = json["local_time"];
|
||||
char *s = strptime(timeLocal.c_str(), "%Y-%m-%dT%H:%M:%S", &tm);
|
||||
if (s != nullptr)
|
||||
{
|
||||
time_t time = mktime(&tm);
|
||||
struct timeval now = {.tv_sec = time};
|
||||
settimeofday(&now, nullptr);
|
||||
return request->reply(200);
|
||||
}
|
||||
}
|
||||
return request->reply(400);
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
#ifndef NTPSettingsService_h
|
||||
#define NTPSettingsService_h
|
||||
|
||||
/**
|
||||
* ESP32 SvelteKit
|
||||
*
|
||||
* A simple, secure and extensible framework for IoT projects for ESP32 platforms
|
||||
* with responsive Sveltekit front-end built with TailwindCSS and DaisyUI.
|
||||
* https://github.com/theelims/ESP32-sveltekit
|
||||
*
|
||||
* Copyright (C) 2018 - 2023 rjwats
|
||||
* Copyright (C) 2023 theelims
|
||||
*
|
||||
* All Rights Reserved. This software may be modified and distributed under
|
||||
* the terms of the LGPL v3 license. See the LICENSE file for details.
|
||||
**/
|
||||
|
||||
#include <HttpEndpoint.h>
|
||||
#include <FSPersistence.h>
|
||||
#include <WiFi.h>
|
||||
|
||||
#include <time.h>
|
||||
#include <lwip/apps/sntp.h>
|
||||
|
||||
#ifndef FACTORY_NTP_ENABLED
|
||||
#define FACTORY_NTP_ENABLED true
|
||||
#endif
|
||||
|
||||
#ifndef FACTORY_NTP_TIME_ZONE_LABEL
|
||||
#define FACTORY_NTP_TIME_ZONE_LABEL "Europe/London"
|
||||
#endif
|
||||
|
||||
#ifndef FACTORY_NTP_TIME_ZONE_FORMAT
|
||||
#define FACTORY_NTP_TIME_ZONE_FORMAT "GMT0BST,M3.5.0/1,M10.5.0"
|
||||
#endif
|
||||
|
||||
#ifndef FACTORY_NTP_SERVER
|
||||
#define FACTORY_NTP_SERVER "time.google.com"
|
||||
#endif
|
||||
|
||||
#define NTP_SETTINGS_FILE "/config/ntpSettings.json"
|
||||
#define NTP_SETTINGS_SERVICE_PATH "/rest/ntpSettings"
|
||||
|
||||
#define MAX_TIME_SIZE 256
|
||||
#define TIME_PATH "/rest/time"
|
||||
|
||||
class NTPSettings
|
||||
{
|
||||
public:
|
||||
bool enabled;
|
||||
String tzLabel;
|
||||
String tzFormat;
|
||||
String server;
|
||||
|
||||
static void read(NTPSettings &settings, JsonObject &root)
|
||||
{
|
||||
root["enabled"] = settings.enabled;
|
||||
root["server"] = settings.server;
|
||||
root["tz_label"] = settings.tzLabel;
|
||||
root["tz_format"] = settings.tzFormat;
|
||||
}
|
||||
|
||||
static StateUpdateResult update(JsonObject &root, NTPSettings &settings)
|
||||
{
|
||||
settings.enabled = root["enabled"] | FACTORY_NTP_ENABLED;
|
||||
settings.server = root["server"] | FACTORY_NTP_SERVER;
|
||||
settings.tzLabel = root["tz_label"] | FACTORY_NTP_TIME_ZONE_LABEL;
|
||||
settings.tzFormat = root["tz_format"] | FACTORY_NTP_TIME_ZONE_FORMAT;
|
||||
return StateUpdateResult::CHANGED;
|
||||
}
|
||||
};
|
||||
|
||||
class NTPSettingsService : public StatefulService<NTPSettings>
|
||||
{
|
||||
public:
|
||||
NTPSettingsService(PsychicHttpServer *server, FS *fs, SecurityManager *securityManager);
|
||||
|
||||
void begin();
|
||||
|
||||
private:
|
||||
PsychicHttpServer *_server;
|
||||
SecurityManager *_securityManager;
|
||||
HttpEndpoint<NTPSettings> _httpEndpoint;
|
||||
FSPersistence<NTPSettings> _fsPersistence;
|
||||
|
||||
void onStationModeGotIP(WiFiEvent_t event, WiFiEventInfo_t info);
|
||||
void onStationModeDisconnected(WiFiEvent_t event, WiFiEventInfo_t info);
|
||||
void configureNTP();
|
||||
esp_err_t configureTime(PsychicRequest *request, JsonVariant &json);
|
||||
};
|
||||
|
||||
#endif // end NTPSettingsService_h
|
||||
@@ -0,0 +1,78 @@
|
||||
/**
|
||||
* ESP32 SvelteKit
|
||||
*
|
||||
* A simple, secure and extensible framework for IoT projects for ESP32 platforms
|
||||
* with responsive Sveltekit front-end built with TailwindCSS and DaisyUI.
|
||||
* https://github.com/theelims/ESP32-sveltekit
|
||||
*
|
||||
* Copyright (C) 2018 - 2023 rjwats
|
||||
* Copyright (C) 2023 theelims
|
||||
*
|
||||
* All Rights Reserved. This software may be modified and distributed under
|
||||
* the terms of the LGPL v3 license. See the LICENSE file for details.
|
||||
**/
|
||||
|
||||
#include <NTPStatus.h>
|
||||
|
||||
NTPStatus::NTPStatus(PsychicHttpServer *server, SecurityManager *securityManager) : _server(server),
|
||||
_securityManager(securityManager)
|
||||
{
|
||||
}
|
||||
|
||||
void NTPStatus::begin()
|
||||
{
|
||||
_server->on(NTP_STATUS_SERVICE_PATH,
|
||||
HTTP_GET,
|
||||
_securityManager->wrapRequest(std::bind(&NTPStatus::ntpStatus, this, std::placeholders::_1),
|
||||
AuthenticationPredicates::IS_AUTHENTICATED));
|
||||
|
||||
ESP_LOGV("NTPStatus", "Registered GET endpoint: %s", NTP_STATUS_SERVICE_PATH);
|
||||
}
|
||||
|
||||
/*
|
||||
* Formats the time using the format provided.
|
||||
*
|
||||
* Uses a 25 byte buffer, large enough to fit an ISO time string with offset.
|
||||
*/
|
||||
String formatTime(tm *time, const char *format)
|
||||
{
|
||||
char time_string[25];
|
||||
strftime(time_string, 25, format, time);
|
||||
return String(time_string);
|
||||
}
|
||||
|
||||
String toUTCTimeString(tm *time)
|
||||
{
|
||||
return formatTime(time, "%FT%TZ");
|
||||
}
|
||||
|
||||
String toLocalTimeString(tm *time)
|
||||
{
|
||||
return formatTime(time, "%FT%T");
|
||||
}
|
||||
|
||||
esp_err_t NTPStatus::ntpStatus(PsychicRequest *request)
|
||||
{
|
||||
PsychicJsonResponse response = PsychicJsonResponse(request, false, MAX_NTP_STATUS_SIZE);
|
||||
JsonObject root = response.getRoot();
|
||||
|
||||
// grab the current instant in unix seconds
|
||||
time_t now = time(nullptr);
|
||||
|
||||
// only provide enabled/disabled status for now
|
||||
root["status"] = sntp_enabled() ? 1 : 0;
|
||||
|
||||
// the current time in UTC
|
||||
root["utc_time"] = toUTCTimeString(gmtime(&now));
|
||||
|
||||
// local time with offset
|
||||
root["local_time"] = toLocalTimeString(localtime(&now));
|
||||
|
||||
// the sntp server name
|
||||
root["server"] = sntp_getservername(0);
|
||||
|
||||
// device uptime in seconds
|
||||
root["uptime"] = millis() / 1000;
|
||||
|
||||
return response.send();
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
#ifndef NTPStatus_h
|
||||
#define NTPStatus_h
|
||||
|
||||
/**
|
||||
* ESP32 SvelteKit
|
||||
*
|
||||
* A simple, secure and extensible framework for IoT projects for ESP32 platforms
|
||||
* with responsive Sveltekit front-end built with TailwindCSS and DaisyUI.
|
||||
* https://github.com/theelims/ESP32-sveltekit
|
||||
*
|
||||
* Copyright (C) 2018 - 2023 rjwats
|
||||
* Copyright (C) 2023 theelims
|
||||
*
|
||||
* All Rights Reserved. This software may be modified and distributed under
|
||||
* the terms of the LGPL v3 license. See the LICENSE file for details.
|
||||
**/
|
||||
|
||||
#include <time.h>
|
||||
#include <WiFi.h>
|
||||
#include <lwip/apps/sntp.h>
|
||||
|
||||
#include <ArduinoJson.h>
|
||||
#include <PsychicHttp.h>
|
||||
#include <SecurityManager.h>
|
||||
|
||||
#define MAX_NTP_STATUS_SIZE 1024
|
||||
#define NTP_STATUS_SERVICE_PATH "/rest/ntpStatus"
|
||||
|
||||
class NTPStatus
|
||||
{
|
||||
public:
|
||||
NTPStatus(PsychicHttpServer *server, SecurityManager *securityManager);
|
||||
|
||||
void begin();
|
||||
|
||||
private:
|
||||
PsychicHttpServer *_server;
|
||||
SecurityManager *_securityManager;
|
||||
esp_err_t ntpStatus(PsychicRequest *request);
|
||||
};
|
||||
|
||||
#endif // end NTPStatus_h
|
||||
@@ -0,0 +1,61 @@
|
||||
#pragma once
|
||||
|
||||
/**
|
||||
* ESP32 SvelteKit
|
||||
*
|
||||
* A simple, secure and extensible framework for IoT projects for ESP32 platforms
|
||||
* with responsive Sveltekit front-end built with TailwindCSS and DaisyUI.
|
||||
* https://github.com/theelims/ESP32-sveltekit
|
||||
*
|
||||
* Copyright (C) 2023 theelims
|
||||
*
|
||||
* All Rights Reserved. This software may be modified and distributed under
|
||||
* the terms of the LGPL v3 license. See the LICENSE file for details.
|
||||
**/
|
||||
|
||||
#include <NotificationEvents.h>
|
||||
|
||||
NotificationEvents::NotificationEvents(PsychicHttpServer *server) : _server(server)
|
||||
{
|
||||
}
|
||||
|
||||
void NotificationEvents::begin()
|
||||
{
|
||||
_eventSource.onOpen([&](PsychicEventSourceClient *client) { // client->send("hello", NULL, millis(), 1000);
|
||||
Serial.printf("New client connected to Event Source: #%u connected from %s\n", client->socket(), client->remoteIP().toString());
|
||||
});
|
||||
_eventSource.onClose([&](PsychicEventSourceClient *client) { // client->send("hello", NULL, millis(), 1000);
|
||||
Serial.printf("Client closed connection to Event Source: #%u connected from %s\n", client->socket(), client->remoteIP().toString());
|
||||
});
|
||||
_server->on(EVENT_NOTIFICATION_SERVICE_PATH, &_eventSource);
|
||||
|
||||
ESP_LOGV("NotificationEvents", "Registered Event Source endpoint: %s", EVENT_NOTIFICATION_SERVICE_PATH);
|
||||
}
|
||||
|
||||
void NotificationEvents::pushNotification(String message, pushEvent event, int id)
|
||||
{
|
||||
String eventType;
|
||||
switch (event)
|
||||
{
|
||||
case (PUSHERROR):
|
||||
eventType = "errorToast";
|
||||
break;
|
||||
case (PUSHWARNING):
|
||||
eventType = "warningToast";
|
||||
break;
|
||||
case (PUSHINFO):
|
||||
eventType = "infoToast";
|
||||
break;
|
||||
case (PUSHSUCCESS):
|
||||
eventType = "successToast";
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
_eventSource.send(message.c_str(), eventType.c_str(), id);
|
||||
}
|
||||
|
||||
void NotificationEvents::send(String message, String event, int id)
|
||||
{
|
||||
_eventSource.send(message.c_str(), event.c_str(), id);
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
#pragma once
|
||||
|
||||
/**
|
||||
* ESP32 SvelteKit
|
||||
*
|
||||
* A simple, secure and extensible framework for IoT projects for ESP32 platforms
|
||||
* with responsive Sveltekit front-end built with TailwindCSS and DaisyUI.
|
||||
* https://github.com/theelims/ESP32-sveltekit
|
||||
*
|
||||
* Copyright (C) 2023 theelims
|
||||
*
|
||||
* All Rights Reserved. This software may be modified and distributed under
|
||||
* the terms of the LGPL v3 license. See the LICENSE file for details.
|
||||
**/
|
||||
|
||||
#include <WiFi.h>
|
||||
|
||||
#include <ArduinoJson.h>
|
||||
#include <PsychicHttp.h>
|
||||
|
||||
#define EVENT_NOTIFICATION_SERVICE_PATH "/events"
|
||||
|
||||
enum pushEvent
|
||||
{
|
||||
PUSHERROR,
|
||||
PUSHWARNING,
|
||||
PUSHINFO,
|
||||
PUSHSUCCESS
|
||||
};
|
||||
|
||||
class NotificationEvents
|
||||
{
|
||||
protected:
|
||||
PsychicHttpServer *_server;
|
||||
PsychicEventSource _eventSource;
|
||||
|
||||
public:
|
||||
NotificationEvents(PsychicHttpServer *server);
|
||||
|
||||
void begin();
|
||||
|
||||
void pushNotification(String message, pushEvent event, int id = 0);
|
||||
|
||||
void send(String message, String event, int id = 0);
|
||||
};
|
||||
@@ -0,0 +1,37 @@
|
||||
/**
|
||||
* ESP32 SvelteKit
|
||||
*
|
||||
* A simple, secure and extensible framework for IoT projects for ESP32 platforms
|
||||
* with responsive Sveltekit front-end built with TailwindCSS and DaisyUI.
|
||||
* https://github.com/theelims/ESP32-sveltekit
|
||||
*
|
||||
* Copyright (C) 2018 - 2023 rjwats
|
||||
* Copyright (C) 2023 theelims
|
||||
*
|
||||
* All Rights Reserved. This software may be modified and distributed under
|
||||
* the terms of the LGPL v3 license. See the LICENSE file for details.
|
||||
**/
|
||||
|
||||
#include <RestartService.h>
|
||||
|
||||
RestartService::RestartService(PsychicHttpServer *server, SecurityManager *securityManager) : _server(server),
|
||||
_securityManager(securityManager)
|
||||
{
|
||||
}
|
||||
|
||||
void RestartService::begin()
|
||||
{
|
||||
_server->on(RESTART_SERVICE_PATH,
|
||||
HTTP_POST,
|
||||
_securityManager->wrapRequest(std::bind(&RestartService::restart, this, std::placeholders::_1),
|
||||
AuthenticationPredicates::IS_ADMIN));
|
||||
|
||||
ESP_LOGV("RestartService", "Registered POST endpoint: %s", RESTART_SERVICE_PATH);
|
||||
}
|
||||
|
||||
esp_err_t RestartService::restart(PsychicRequest *request)
|
||||
{
|
||||
request->reply(200);
|
||||
restartNow();
|
||||
return ESP_OK;
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
#ifndef RestartService_h
|
||||
#define RestartService_h
|
||||
|
||||
/**
|
||||
* ESP32 SvelteKit
|
||||
*
|
||||
* A simple, secure and extensible framework for IoT projects for ESP32 platforms
|
||||
* with responsive Sveltekit front-end built with TailwindCSS and DaisyUI.
|
||||
* https://github.com/theelims/ESP32-sveltekit
|
||||
*
|
||||
* Copyright (C) 2018 - 2023 rjwats
|
||||
* Copyright (C) 2023 theelims
|
||||
*
|
||||
* All Rights Reserved. This software may be modified and distributed under
|
||||
* the terms of the LGPL v3 license. See the LICENSE file for details.
|
||||
**/
|
||||
|
||||
#include <WiFi.h>
|
||||
|
||||
#include <PsychicHttp.h>
|
||||
#include <SecurityManager.h>
|
||||
|
||||
#define RESTART_SERVICE_PATH "/rest/restart"
|
||||
|
||||
class RestartService
|
||||
{
|
||||
public:
|
||||
RestartService(PsychicHttpServer *server, SecurityManager *securityManager);
|
||||
|
||||
void begin();
|
||||
|
||||
static void restartNow()
|
||||
{
|
||||
WiFi.disconnect(true);
|
||||
delay(500);
|
||||
ESP.restart();
|
||||
}
|
||||
|
||||
private:
|
||||
PsychicHttpServer *_server;
|
||||
SecurityManager *_securityManager;
|
||||
esp_err_t restart(PsychicRequest *request);
|
||||
};
|
||||
|
||||
#endif // end RestartService_h
|
||||
@@ -0,0 +1,119 @@
|
||||
#ifndef SecurityManager_h
|
||||
#define SecurityManager_h
|
||||
|
||||
/**
|
||||
* ESP32 SvelteKit
|
||||
*
|
||||
* A simple, secure and extensible framework for IoT projects for ESP32 platforms
|
||||
* with responsive Sveltekit front-end built with TailwindCSS and DaisyUI.
|
||||
* https://github.com/theelims/ESP32-sveltekit
|
||||
*
|
||||
* Copyright (C) 2018 - 2023 rjwats
|
||||
* Copyright (C) 2023 theelims
|
||||
*
|
||||
* All Rights Reserved. This software may be modified and distributed under
|
||||
* the terms of the LGPL v3 license. See the LICENSE file for details.
|
||||
**/
|
||||
|
||||
#include <Features.h>
|
||||
#include <ArduinoJsonJWT.h>
|
||||
#include <PsychicHttp.h>
|
||||
#include <list>
|
||||
|
||||
#define ACCESS_TOKEN_PARAMATER "access_token"
|
||||
|
||||
#define AUTHORIZATION_HEADER "Authorization"
|
||||
#define AUTHORIZATION_HEADER_PREFIX "Bearer "
|
||||
#define AUTHORIZATION_HEADER_PREFIX_LEN 7
|
||||
|
||||
#define MAX_JWT_SIZE 128
|
||||
|
||||
class User
|
||||
{
|
||||
public:
|
||||
String username;
|
||||
String password;
|
||||
bool admin;
|
||||
|
||||
public:
|
||||
User(String username, String password, bool admin) : username(username), password(password), admin(admin)
|
||||
{
|
||||
}
|
||||
};
|
||||
|
||||
class Authentication
|
||||
{
|
||||
public:
|
||||
User *user;
|
||||
boolean authenticated;
|
||||
|
||||
public:
|
||||
Authentication(User &user) : user(new User(user)), authenticated(true)
|
||||
{
|
||||
}
|
||||
Authentication() : user(nullptr), authenticated(false)
|
||||
{
|
||||
}
|
||||
~Authentication()
|
||||
{
|
||||
delete (user);
|
||||
}
|
||||
};
|
||||
|
||||
typedef std::function<boolean(Authentication &authentication)> AuthenticationPredicate;
|
||||
|
||||
class AuthenticationPredicates
|
||||
{
|
||||
public:
|
||||
static bool NONE_REQUIRED(Authentication &authentication)
|
||||
{
|
||||
return true;
|
||||
};
|
||||
static bool IS_AUTHENTICATED(Authentication &authentication)
|
||||
{
|
||||
return authentication.authenticated;
|
||||
};
|
||||
static bool IS_ADMIN(Authentication &authentication)
|
||||
{
|
||||
return authentication.authenticated && authentication.user->admin;
|
||||
};
|
||||
};
|
||||
|
||||
class SecurityManager
|
||||
{
|
||||
public:
|
||||
#if FT_ENABLED(FT_SECURITY)
|
||||
/*
|
||||
* Authenticate, returning the user if found
|
||||
*/
|
||||
virtual Authentication authenticate(const String &username, const String &password) = 0;
|
||||
|
||||
/*
|
||||
* Generate a JWT for the user provided
|
||||
*/
|
||||
virtual String generateJWT(User *user) = 0;
|
||||
|
||||
#endif
|
||||
|
||||
/*
|
||||
* Check the request header for the Authorization token
|
||||
*/
|
||||
virtual Authentication authenticateRequest(PsychicRequest *request) = 0;
|
||||
|
||||
/**
|
||||
* Filter a request with the provided predicate, only returning true if the predicate matches.
|
||||
*/
|
||||
virtual PsychicRequestFilterFunction filterRequest(AuthenticationPredicate predicate) = 0;
|
||||
|
||||
/**
|
||||
* Wrap the provided request to provide validation against an AuthenticationPredicate.
|
||||
*/
|
||||
virtual PsychicHttpRequestCallback wrapRequest(PsychicHttpRequestCallback onRequest, AuthenticationPredicate predicate) = 0;
|
||||
|
||||
/**
|
||||
* Wrap the provided json request callback to provide validation against an AuthenticationPredicate.
|
||||
*/
|
||||
virtual PsychicJsonRequestCallback wrapCallback(PsychicJsonRequestCallback onRequest, AuthenticationPredicate predicate) = 0;
|
||||
};
|
||||
|
||||
#endif // end SecurityManager_h
|
||||
@@ -0,0 +1,230 @@
|
||||
/**
|
||||
* ESP32 SvelteKit
|
||||
*
|
||||
* A simple, secure and extensible framework for IoT projects for ESP32 platforms
|
||||
* with responsive Sveltekit front-end built with TailwindCSS and DaisyUI.
|
||||
* https://github.com/theelims/ESP32-sveltekit
|
||||
*
|
||||
* Copyright (C) 2018 - 2023 rjwats
|
||||
* Copyright (C) 2023 theelims
|
||||
*
|
||||
* All Rights Reserved. This software may be modified and distributed under
|
||||
* the terms of the LGPL v3 license. See the LICENSE file for details.
|
||||
**/
|
||||
|
||||
#include <SecuritySettingsService.h>
|
||||
|
||||
#if FT_ENABLED(FT_SECURITY)
|
||||
|
||||
SecuritySettingsService::SecuritySettingsService(PsychicHttpServer *server, FS *fs) : _server(server),
|
||||
_httpEndpoint(SecuritySettings::read, SecuritySettings::update, this, server, SECURITY_SETTINGS_PATH, this),
|
||||
_fsPersistence(SecuritySettings::read, SecuritySettings::update, this, fs, SECURITY_SETTINGS_FILE),
|
||||
_jwtHandler(FACTORY_JWT_SECRET)
|
||||
{
|
||||
addUpdateHandler([&](const String &originId)
|
||||
{ configureJWTHandler(); },
|
||||
false);
|
||||
}
|
||||
|
||||
void SecuritySettingsService::begin()
|
||||
{
|
||||
_server->on(GENERATE_TOKEN_PATH,
|
||||
HTTP_GET,
|
||||
wrapRequest(std::bind(&SecuritySettingsService::generateToken, this, std::placeholders::_1),
|
||||
AuthenticationPredicates::IS_ADMIN));
|
||||
|
||||
ESP_LOGV("SecuritySettingsService", "Registered GET endpoint: %s", GENERATE_TOKEN_PATH);
|
||||
|
||||
_httpEndpoint.begin();
|
||||
_fsPersistence.readFromFS();
|
||||
configureJWTHandler();
|
||||
}
|
||||
|
||||
Authentication SecuritySettingsService::authenticateRequest(PsychicRequest *request)
|
||||
{
|
||||
// Load the parameters from the request, as they are only loaded later with the regular handler
|
||||
if (request->hasHeader(AUTHORIZATION_HEADER))
|
||||
{
|
||||
auto value = request->header(AUTHORIZATION_HEADER);
|
||||
// ESP_LOGV("SecuritySettingsService", "Authorization header: %s", value.c_str());
|
||||
if (value.startsWith(AUTHORIZATION_HEADER_PREFIX))
|
||||
{
|
||||
value = value.substring(AUTHORIZATION_HEADER_PREFIX_LEN);
|
||||
return authenticateJWT(value);
|
||||
}
|
||||
}
|
||||
else if (request->hasParam(ACCESS_TOKEN_PARAMATER))
|
||||
{
|
||||
String value = request->getParam(ACCESS_TOKEN_PARAMATER)->value();
|
||||
// ESP_LOGV("SecuritySettingsService", "Access token parameter: %s", value.c_str());
|
||||
return authenticateJWT(value);
|
||||
}
|
||||
return Authentication();
|
||||
}
|
||||
|
||||
void SecuritySettingsService::configureJWTHandler()
|
||||
{
|
||||
_jwtHandler.setSecret(_state.jwtSecret);
|
||||
}
|
||||
|
||||
Authentication SecuritySettingsService::authenticateJWT(String &jwt)
|
||||
{
|
||||
DynamicJsonDocument payloadDocument(MAX_JWT_SIZE);
|
||||
_jwtHandler.parseJWT(jwt, payloadDocument);
|
||||
if (payloadDocument.is<JsonObject>())
|
||||
{
|
||||
JsonObject parsedPayload = payloadDocument.as<JsonObject>();
|
||||
String username = parsedPayload["username"];
|
||||
for (User _user : _state.users)
|
||||
{
|
||||
if (_user.username == username && validatePayload(parsedPayload, &_user))
|
||||
{
|
||||
return Authentication(_user);
|
||||
}
|
||||
}
|
||||
}
|
||||
return Authentication();
|
||||
}
|
||||
|
||||
Authentication SecuritySettingsService::authenticate(const String &username, const String &password)
|
||||
{
|
||||
for (User _user : _state.users)
|
||||
{
|
||||
if (_user.username == username && _user.password == password)
|
||||
{
|
||||
return Authentication(_user);
|
||||
}
|
||||
}
|
||||
return Authentication();
|
||||
}
|
||||
|
||||
inline void populateJWTPayload(JsonObject &payload, User *user)
|
||||
{
|
||||
payload["username"] = user->username;
|
||||
payload["admin"] = user->admin;
|
||||
}
|
||||
|
||||
boolean SecuritySettingsService::validatePayload(JsonObject &parsedPayload, User *user)
|
||||
{
|
||||
DynamicJsonDocument jsonDocument(MAX_JWT_SIZE);
|
||||
JsonObject payload = jsonDocument.to<JsonObject>();
|
||||
populateJWTPayload(payload, user);
|
||||
return payload == parsedPayload;
|
||||
}
|
||||
|
||||
String SecuritySettingsService::generateJWT(User *user)
|
||||
{
|
||||
DynamicJsonDocument jsonDocument(MAX_JWT_SIZE);
|
||||
JsonObject payload = jsonDocument.to<JsonObject>();
|
||||
populateJWTPayload(payload, user);
|
||||
return _jwtHandler.buildJWT(payload);
|
||||
}
|
||||
|
||||
PsychicRequestFilterFunction SecuritySettingsService::filterRequest(AuthenticationPredicate predicate)
|
||||
{
|
||||
return [this, predicate](PsychicRequest *request)
|
||||
{
|
||||
// ESP_LOGV("SecuritySettingsService", "Authenticating filter request: %s", request->uri().c_str());
|
||||
// ESP_LOGV("SecuritySettingsService", "Request Method: %s", request->methodStr().c_str());
|
||||
|
||||
// TODO: This is a hack to allow bogus websocket filter requests to pass through
|
||||
// This is a temporary fix until the PsychicHttp websocket handler is fixed to not send a bogus filter request
|
||||
|
||||
// Check if we have a bogus filter request and return true
|
||||
if (request->uri().isEmpty() && request->method() == HTTP_DELETE)
|
||||
{
|
||||
// ESP_LOGV("SecuritySettingsService", "Bogus filter request - allowing");
|
||||
return true;
|
||||
}
|
||||
else
|
||||
request->loadParams();
|
||||
|
||||
Authentication authentication = authenticateRequest(request);
|
||||
bool result = predicate(authentication);
|
||||
// ESP_LOGV("SecuritySettingsService", "Filter Request %s", result ? "allowed" : "denied");
|
||||
return result;
|
||||
};
|
||||
}
|
||||
|
||||
PsychicHttpRequestCallback SecuritySettingsService::wrapRequest(PsychicHttpRequestCallback onRequest, AuthenticationPredicate predicate)
|
||||
{
|
||||
return [this, onRequest, predicate](PsychicRequest *request)
|
||||
{
|
||||
Authentication authentication = authenticateRequest(request);
|
||||
if (!predicate(authentication))
|
||||
{
|
||||
return request->reply(401);
|
||||
}
|
||||
return onRequest(request);
|
||||
};
|
||||
}
|
||||
|
||||
PsychicJsonRequestCallback SecuritySettingsService::wrapCallback(PsychicJsonRequestCallback onRequest, AuthenticationPredicate predicate)
|
||||
{
|
||||
return [this, onRequest, predicate](PsychicRequest *request, JsonVariant &json)
|
||||
{
|
||||
Authentication authentication = authenticateRequest(request);
|
||||
if (!predicate(authentication))
|
||||
{
|
||||
return request->reply(401);
|
||||
}
|
||||
return onRequest(request, json);
|
||||
};
|
||||
}
|
||||
|
||||
esp_err_t SecuritySettingsService::generateToken(PsychicRequest *request)
|
||||
{
|
||||
String usernameParam = request->getParam("username")->value();
|
||||
for (User _user : _state.users)
|
||||
{
|
||||
if (_user.username == usernameParam)
|
||||
{
|
||||
PsychicJsonResponse response = PsychicJsonResponse(request, false, GENERATE_TOKEN_SIZE);
|
||||
JsonObject root = response.getRoot();
|
||||
root["token"] = generateJWT(&_user);
|
||||
return response.send();
|
||||
}
|
||||
}
|
||||
return request->reply(401);
|
||||
}
|
||||
|
||||
#else
|
||||
|
||||
User ADMIN_USER = User(FACTORY_ADMIN_USERNAME, FACTORY_ADMIN_PASSWORD, true);
|
||||
|
||||
SecuritySettingsService::SecuritySettingsService(PsychicHttpServer *server, FS *fs) : SecurityManager()
|
||||
{
|
||||
}
|
||||
SecuritySettingsService::~SecuritySettingsService()
|
||||
{
|
||||
}
|
||||
|
||||
PsychicRequestFilterFunction SecuritySettingsService::filterRequest(AuthenticationPredicate predicate)
|
||||
{
|
||||
return [this, predicate](PsychicRequest *request)
|
||||
{
|
||||
// ESP_LOGV("SecuritySettingsService", "Security disabled - all requests are allowed");
|
||||
return true;
|
||||
};
|
||||
}
|
||||
|
||||
// Return the admin user on all request - disabling security features
|
||||
Authentication SecuritySettingsService::authenticateRequest(PsychicRequest *request)
|
||||
{
|
||||
return Authentication(ADMIN_USER);
|
||||
}
|
||||
|
||||
// Return the function unwrapped
|
||||
PsychicHttpRequestCallback SecuritySettingsService::wrapRequest(PsychicHttpRequestCallback onRequest,
|
||||
AuthenticationPredicate predicate)
|
||||
{
|
||||
return onRequest;
|
||||
}
|
||||
|
||||
PsychicJsonRequestCallback SecuritySettingsService::wrapCallback(PsychicJsonRequestCallback onRequest,
|
||||
AuthenticationPredicate predicate)
|
||||
{
|
||||
return onRequest;
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,151 @@
|
||||
#ifndef SecuritySettingsService_h
|
||||
#define SecuritySettingsService_h
|
||||
|
||||
/**
|
||||
* ESP32 SvelteKit
|
||||
*
|
||||
* A simple, secure and extensible framework for IoT projects for ESP32 platforms
|
||||
* with responsive Sveltekit front-end built with TailwindCSS and DaisyUI.
|
||||
* https://github.com/theelims/ESP32-sveltekit
|
||||
*
|
||||
* Copyright (C) 2018 - 2023 rjwats
|
||||
* Copyright (C) 2023 theelims
|
||||
*
|
||||
* All Rights Reserved. This software may be modified and distributed under
|
||||
* the terms of the LGPL v3 license. See the LICENSE file for details.
|
||||
**/
|
||||
|
||||
#include <SettingValue.h>
|
||||
#include <Features.h>
|
||||
#include <SecurityManager.h>
|
||||
#include <HttpEndpoint.h>
|
||||
#include <FSPersistence.h>
|
||||
|
||||
#ifndef FACTORY_JWT_SECRET
|
||||
#define FACTORY_JWT_SECRET "#{random}-#{random}"
|
||||
#endif
|
||||
|
||||
#ifndef FACTORY_ADMIN_USERNAME
|
||||
#define FACTORY_ADMIN_USERNAME "admin"
|
||||
#endif
|
||||
|
||||
#ifndef FACTORY_ADMIN_PASSWORD
|
||||
#define FACTORY_ADMIN_PASSWORD "admin"
|
||||
#endif
|
||||
|
||||
#ifndef FACTORY_GUEST_USERNAME
|
||||
#define FACTORY_GUEST_USERNAME "guest"
|
||||
#endif
|
||||
|
||||
#ifndef FACTORY_GUEST_PASSWORD
|
||||
#define FACTORY_GUEST_PASSWORD "guest"
|
||||
#endif
|
||||
|
||||
#define SECURITY_SETTINGS_FILE "/config/securitySettings.json"
|
||||
#define SECURITY_SETTINGS_PATH "/rest/securitySettings"
|
||||
|
||||
#define GENERATE_TOKEN_SIZE 512
|
||||
#define GENERATE_TOKEN_PATH "/rest/generateToken"
|
||||
|
||||
#if FT_ENABLED(FT_SECURITY)
|
||||
|
||||
class SecuritySettings
|
||||
{
|
||||
public:
|
||||
String jwtSecret;
|
||||
std::list<User> users;
|
||||
|
||||
static void read(SecuritySettings &settings, JsonObject &root)
|
||||
{
|
||||
// secret
|
||||
root["jwt_secret"] = settings.jwtSecret;
|
||||
|
||||
// users
|
||||
JsonArray users = root.createNestedArray("users");
|
||||
for (User user : settings.users)
|
||||
{
|
||||
JsonObject userRoot = users.createNestedObject();
|
||||
userRoot["username"] = user.username;
|
||||
userRoot["password"] = user.password;
|
||||
userRoot["admin"] = user.admin;
|
||||
}
|
||||
}
|
||||
|
||||
static StateUpdateResult update(JsonObject &root, SecuritySettings &settings)
|
||||
{
|
||||
// secret
|
||||
settings.jwtSecret = root["jwt_secret"] | SettingValue::format(FACTORY_JWT_SECRET);
|
||||
|
||||
// users
|
||||
settings.users.clear();
|
||||
if (root["users"].is<JsonArray>())
|
||||
{
|
||||
for (JsonVariant user : root["users"].as<JsonArray>())
|
||||
{
|
||||
settings.users.push_back(User(user["username"], user["password"], user["admin"]));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
settings.users.push_back(User(FACTORY_ADMIN_USERNAME, FACTORY_ADMIN_PASSWORD, true));
|
||||
settings.users.push_back(User(FACTORY_GUEST_USERNAME, FACTORY_GUEST_PASSWORD, false));
|
||||
}
|
||||
return StateUpdateResult::CHANGED;
|
||||
}
|
||||
};
|
||||
|
||||
class SecuritySettingsService : public StatefulService<SecuritySettings>, public SecurityManager
|
||||
{
|
||||
public:
|
||||
SecuritySettingsService(PsychicHttpServer *server, FS *fs);
|
||||
|
||||
void begin();
|
||||
|
||||
// Functions to implement SecurityManager
|
||||
Authentication authenticate(const String &username, const String &password);
|
||||
Authentication authenticateRequest(PsychicRequest *request);
|
||||
String generateJWT(User *user);
|
||||
|
||||
PsychicRequestFilterFunction filterRequest(AuthenticationPredicate predicate);
|
||||
PsychicHttpRequestCallback wrapRequest(PsychicHttpRequestCallback onRequest, AuthenticationPredicate predicate);
|
||||
PsychicJsonRequestCallback wrapCallback(PsychicJsonRequestCallback onRequest, AuthenticationPredicate predicate);
|
||||
|
||||
private:
|
||||
PsychicHttpServer *_server;
|
||||
|
||||
HttpEndpoint<SecuritySettings> _httpEndpoint;
|
||||
FSPersistence<SecuritySettings> _fsPersistence;
|
||||
ArduinoJsonJWT _jwtHandler;
|
||||
|
||||
esp_err_t generateToken(PsychicRequest *request);
|
||||
|
||||
void configureJWTHandler();
|
||||
|
||||
/*
|
||||
* Lookup the user by JWT
|
||||
*/
|
||||
Authentication authenticateJWT(String &jwt);
|
||||
|
||||
/*
|
||||
* Verify the payload is correct
|
||||
*/
|
||||
boolean validatePayload(JsonObject &parsedPayload, User *user);
|
||||
};
|
||||
|
||||
#else
|
||||
|
||||
class SecuritySettingsService : public SecurityManager
|
||||
{
|
||||
public:
|
||||
SecuritySettingsService(PsychicHttpServer *server, FS *fs);
|
||||
~SecuritySettingsService();
|
||||
|
||||
// minimal set of functions to support framework with security settings disabled
|
||||
Authentication authenticateRequest(PsychicRequest *request);
|
||||
PsychicRequestFilterFunction filterRequest(AuthenticationPredicate predicate);
|
||||
PsychicHttpRequestCallback wrapRequest(PsychicHttpRequestCallback onRequest, AuthenticationPredicate predicate);
|
||||
PsychicJsonRequestCallback wrapCallback(PsychicJsonRequestCallback onRequest, AuthenticationPredicate predicate);
|
||||
};
|
||||
|
||||
#endif // end FT_ENABLED(FT_SECURITY)
|
||||
#endif // end SecuritySettingsService_h
|
||||
@@ -0,0 +1,67 @@
|
||||
/**
|
||||
* ESP32 SvelteKit
|
||||
*
|
||||
* A simple, secure and extensible framework for IoT projects for ESP32 platforms
|
||||
* with responsive Sveltekit front-end built with TailwindCSS and DaisyUI.
|
||||
* https://github.com/theelims/ESP32-sveltekit
|
||||
*
|
||||
* Copyright (C) 2018 - 2023 rjwats
|
||||
* Copyright (C) 2023 theelims
|
||||
*
|
||||
* All Rights Reserved. This software may be modified and distributed under
|
||||
* the terms of the LGPL v3 license. See the LICENSE file for details.
|
||||
**/
|
||||
|
||||
#include <SettingValue.h>
|
||||
|
||||
namespace SettingValue
|
||||
{
|
||||
const String PLATFORM = "esp32";
|
||||
|
||||
/**
|
||||
* Returns a new string after replacing each instance of the pattern with a value generated by calling the provided
|
||||
* callback.
|
||||
*/
|
||||
String replaceEach(String value, String pattern, String (*generateReplacement)())
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
int index = value.indexOf(pattern);
|
||||
if (index == -1)
|
||||
{
|
||||
break;
|
||||
}
|
||||
value = value.substring(0, index) + generateReplacement() + value.substring(index + pattern.length());
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a random number, encoded as a hex string.
|
||||
*/
|
||||
String getRandom()
|
||||
{
|
||||
return String(random(2147483647), HEX);
|
||||
}
|
||||
|
||||
/**
|
||||
* Uses the station's MAC address to create a unique id for each device.
|
||||
*/
|
||||
String getUniqueId()
|
||||
{
|
||||
uint8_t mac[6];
|
||||
esp_read_mac(mac, ESP_MAC_WIFI_STA);
|
||||
char macStr[13] = {0};
|
||||
sprintf(macStr, "%02x%02x%02x%02x%02x%02x", mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);
|
||||
return String(macStr);
|
||||
}
|
||||
|
||||
String format(String value)
|
||||
{
|
||||
value = replaceEach(value, "#{random}", getRandom);
|
||||
value.replace("#{unique_id}", getUniqueId());
|
||||
value.replace("#{platform}", PLATFORM);
|
||||
return value;
|
||||
}
|
||||
|
||||
}; // end namespace SettingValue
|
||||
@@ -0,0 +1,25 @@
|
||||
#ifndef SettingValue_h
|
||||
#define SettingValue_h
|
||||
|
||||
/**
|
||||
* ESP32 SvelteKit
|
||||
*
|
||||
* A simple, secure and extensible framework for IoT projects for ESP32 platforms
|
||||
* with responsive Sveltekit front-end built with TailwindCSS and DaisyUI.
|
||||
* https://github.com/theelims/ESP32-sveltekit
|
||||
*
|
||||
* Copyright (C) 2018 - 2023 rjwats
|
||||
* Copyright (C) 2023 theelims
|
||||
*
|
||||
* All Rights Reserved. This software may be modified and distributed under
|
||||
* the terms of the LGPL v3 license. See the LICENSE file for details.
|
||||
**/
|
||||
|
||||
#include <Arduino.h>
|
||||
|
||||
namespace SettingValue
|
||||
{
|
||||
String format(String value);
|
||||
};
|
||||
|
||||
#endif // end SettingValue
|
||||
@@ -0,0 +1,94 @@
|
||||
/**
|
||||
* ESP32 SvelteKit
|
||||
*
|
||||
* A simple, secure and extensible framework for IoT projects for ESP32 platforms
|
||||
* with responsive Sveltekit front-end built with TailwindCSS and DaisyUI.
|
||||
* https://github.com/theelims/ESP32-sveltekit
|
||||
*
|
||||
* Copyright (C) 2023 theelims
|
||||
*
|
||||
* All Rights Reserved. This software may be modified and distributed under
|
||||
* the terms of the LGPL v3 license. See the LICENSE file for details.
|
||||
**/
|
||||
|
||||
#include <SleepService.h>
|
||||
|
||||
// Definition of static member variable
|
||||
void (*SleepService::_callbackSleep)() = nullptr;
|
||||
|
||||
SleepService::SleepService(PsychicHttpServer *server,
|
||||
SecurityManager *securityManager) : _server(server),
|
||||
_securityManager(securityManager)
|
||||
{
|
||||
}
|
||||
|
||||
void SleepService::begin()
|
||||
{
|
||||
// OPTIONS (for CORS preflight)
|
||||
#ifdef ENABLE_CORS
|
||||
_server->on(SLEEP_SERVICE_PATH,
|
||||
HTTP_OPTIONS,
|
||||
_securityManager->wrapRequest(
|
||||
[this](PsychicRequest *request)
|
||||
{
|
||||
return request->reply(200);
|
||||
},
|
||||
AuthenticationPredicates::IS_AUTHENTICATED));
|
||||
#endif
|
||||
|
||||
_server->on(SLEEP_SERVICE_PATH,
|
||||
HTTP_POST,
|
||||
_securityManager->wrapRequest(std::bind(&SleepService::sleep, this, std::placeholders::_1),
|
||||
AuthenticationPredicates::IS_AUTHENTICATED));
|
||||
|
||||
ESP_LOGV("SleepService", "Registered POST endpoint: %s", SLEEP_SERVICE_PATH);
|
||||
}
|
||||
|
||||
esp_err_t SleepService::sleep(PsychicRequest *request)
|
||||
{
|
||||
request->reply(200);
|
||||
sleepNow();
|
||||
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
void SleepService::sleepNow()
|
||||
{
|
||||
#ifdef SERIAL_INFO
|
||||
Serial.println("Going into deep sleep now");
|
||||
#endif
|
||||
ESP_LOGI("SleepService", "Going into deep sleep now");
|
||||
// Callback for main code sleep preparation
|
||||
if (_callbackSleep != nullptr)
|
||||
{
|
||||
_callbackSleep();
|
||||
}
|
||||
delay(100);
|
||||
|
||||
MDNS.end();
|
||||
delay(100);
|
||||
|
||||
WiFi.disconnect(true);
|
||||
delay(500);
|
||||
|
||||
// Prepare ESP for sleep
|
||||
uint64_t bitmask = (uint64_t)1 << (WAKEUP_PIN_NUMBER);
|
||||
|
||||
// special treatment for ESP32-C3 because of the RISC-V architecture
|
||||
#ifdef CONFIG_IDF_TARGET_ESP32C3
|
||||
esp_deep_sleep_enable_gpio_wakeup(bitmask, (esp_deepsleep_gpio_wake_up_mode_t)WAKEUP_SIGNAL);
|
||||
#else
|
||||
esp_sleep_enable_ext1_wakeup(bitmask, (esp_sleep_ext1_wakeup_mode_t)WAKEUP_SIGNAL);
|
||||
esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_PERIPH, ESP_PD_OPTION_OFF);
|
||||
#endif
|
||||
|
||||
#ifdef SERIAL_INFO
|
||||
Serial.println("Good by!");
|
||||
#endif
|
||||
|
||||
// Just to be sure
|
||||
delay(100);
|
||||
|
||||
// Hibernate
|
||||
esp_deep_sleep_start();
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
#pragma once
|
||||
|
||||
/**
|
||||
* ESP32 SvelteKit
|
||||
*
|
||||
* A simple, secure and extensible framework for IoT projects for ESP32 platforms
|
||||
* with responsive Sveltekit front-end built with TailwindCSS and DaisyUI.
|
||||
* https://github.com/theelims/ESP32-sveltekit
|
||||
*
|
||||
* Copyright (C) 2023 theelims
|
||||
*
|
||||
* All Rights Reserved. This software may be modified and distributed under
|
||||
* the terms of the LGPL v3 license. See the LICENSE file for details.
|
||||
**/
|
||||
|
||||
#include <WiFi.h>
|
||||
#include <ESPmDNS.h>
|
||||
|
||||
#include <PsychicHttp.h>
|
||||
#include <SecurityManager.h>
|
||||
|
||||
#define SLEEP_SERVICE_PATH "/rest/sleep"
|
||||
|
||||
#ifndef WAKEUP_PIN_NUMBER
|
||||
#define WAKEUP_PIN_NUMBER 0
|
||||
#endif
|
||||
|
||||
#ifndef WAKEUP_SIGNAL
|
||||
#define WAKEUP_SIGNAL 0
|
||||
#endif
|
||||
|
||||
class SleepService
|
||||
{
|
||||
public:
|
||||
SleepService(PsychicHttpServer *server, SecurityManager *securityManager);
|
||||
|
||||
void begin();
|
||||
|
||||
static void sleepNow();
|
||||
|
||||
void attachOnSleepCallback(void (*callbackSleep)())
|
||||
{
|
||||
_callbackSleep = callbackSleep;
|
||||
}
|
||||
|
||||
private:
|
||||
PsychicHttpServer *_server;
|
||||
SecurityManager *_securityManager;
|
||||
esp_err_t sleep(PsychicRequest *request);
|
||||
|
||||
protected:
|
||||
static void (*_callbackSleep)();
|
||||
};
|
||||
@@ -0,0 +1,18 @@
|
||||
/**
|
||||
* ESP32 SvelteKit
|
||||
*
|
||||
* A simple, secure and extensible framework for IoT projects for ESP32 platforms
|
||||
* with responsive Sveltekit front-end built with TailwindCSS and DaisyUI.
|
||||
* https://github.com/theelims/ESP32-sveltekit
|
||||
*
|
||||
* Copyright (C) 2018 - 2023 rjwats
|
||||
* Copyright (C) 2023 theelims
|
||||
*
|
||||
* All Rights Reserved. This software may be modified and distributed under
|
||||
* the terms of the LGPL v3 license. See the LICENSE file for details.
|
||||
**/
|
||||
|
||||
#include <StatefulService.h>
|
||||
|
||||
update_handler_id_t StateUpdateHandlerInfo::currentUpdatedHandlerId = 0;
|
||||
hook_handler_id_t StateHookHandlerInfo::currentHookHandlerId = 0;
|
||||
@@ -0,0 +1,218 @@
|
||||
#ifndef StatefulService_h
|
||||
#define StatefulService_h
|
||||
|
||||
/**
|
||||
* ESP32 SvelteKit
|
||||
*
|
||||
* A simple, secure and extensible framework for IoT projects for ESP32 platforms
|
||||
* with responsive Sveltekit front-end built with TailwindCSS and DaisyUI.
|
||||
* https://github.com/theelims/ESP32-sveltekit
|
||||
*
|
||||
* Copyright (C) 2018 - 2023 rjwats
|
||||
* Copyright (C) 2023 theelims
|
||||
*
|
||||
* All Rights Reserved. This software may be modified and distributed under
|
||||
* the terms of the LGPL v3 license. See the LICENSE file for details.
|
||||
**/
|
||||
|
||||
#include <Arduino.h>
|
||||
#include <ArduinoJson.h>
|
||||
|
||||
#include <list>
|
||||
#include <functional>
|
||||
#include <freertos/FreeRTOS.h>
|
||||
#include <freertos/semphr.h>
|
||||
|
||||
#ifndef DEFAULT_BUFFER_SIZE
|
||||
#define DEFAULT_BUFFER_SIZE 1024
|
||||
#endif
|
||||
|
||||
enum class StateUpdateResult
|
||||
{
|
||||
CHANGED = 0, // The update changed the state and propagation should take place if required
|
||||
UNCHANGED, // The state was unchanged, propagation should not take place
|
||||
ERROR // There was a problem updating the state, propagation should not take place
|
||||
};
|
||||
|
||||
template <typename T>
|
||||
using JsonStateUpdater = std::function<StateUpdateResult(JsonObject &root, T &settings)>;
|
||||
|
||||
template <typename T>
|
||||
using JsonStateReader = std::function<void(T &settings, JsonObject &root)>;
|
||||
|
||||
typedef size_t update_handler_id_t;
|
||||
typedef size_t hook_handler_id_t;
|
||||
typedef std::function<void(const String &originId)> StateUpdateCallback;
|
||||
typedef std::function<void(const String &originId, StateUpdateResult &result)> StateHookCallback;
|
||||
|
||||
typedef struct StateUpdateHandlerInfo
|
||||
{
|
||||
static update_handler_id_t currentUpdatedHandlerId;
|
||||
update_handler_id_t _id;
|
||||
StateUpdateCallback _cb;
|
||||
bool _allowRemove;
|
||||
StateUpdateHandlerInfo(StateUpdateCallback cb, bool allowRemove) : _id(++currentUpdatedHandlerId), _cb(cb), _allowRemove(allowRemove){};
|
||||
} StateUpdateHandlerInfo_t;
|
||||
|
||||
typedef struct StateHookHandlerInfo
|
||||
{
|
||||
static hook_handler_id_t currentHookHandlerId;
|
||||
hook_handler_id_t _id;
|
||||
StateHookCallback _cb;
|
||||
bool _allowRemove;
|
||||
StateHookHandlerInfo(StateHookCallback cb, bool allowRemove) : _id(++currentHookHandlerId), _cb(cb), _allowRemove(allowRemove){};
|
||||
} StateHookHandlerInfo_t;
|
||||
|
||||
template <class T>
|
||||
class StatefulService
|
||||
{
|
||||
public:
|
||||
template <typename... Args>
|
||||
StatefulService(Args &&...args) : _state(std::forward<Args>(args)...), _accessMutex(xSemaphoreCreateRecursiveMutex())
|
||||
{
|
||||
}
|
||||
|
||||
update_handler_id_t addUpdateHandler(StateUpdateCallback cb, bool allowRemove = true)
|
||||
{
|
||||
if (!cb)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
StateUpdateHandlerInfo_t updateHandler(cb, allowRemove);
|
||||
_updateHandlers.push_back(updateHandler);
|
||||
return updateHandler._id;
|
||||
}
|
||||
|
||||
void removeUpdateHandler(update_handler_id_t id)
|
||||
{
|
||||
for (auto i = _updateHandlers.begin(); i != _updateHandlers.end();)
|
||||
{
|
||||
if ((*i)._allowRemove && (*i)._id == id)
|
||||
{
|
||||
i = _updateHandlers.erase(i);
|
||||
}
|
||||
else
|
||||
{
|
||||
++i;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
hook_handler_id_t addHookHandler(StateHookCallback cb, bool allowRemove = true)
|
||||
{
|
||||
if (!cb)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
StateHookHandlerInfo_t hookHandler(cb, allowRemove);
|
||||
_hookHandlers.push_back(hookHandler);
|
||||
return hookHandler._id;
|
||||
}
|
||||
|
||||
void removeHookHandler(hook_handler_id_t id)
|
||||
{
|
||||
for (auto i = _hookHandlers.begin(); i != _hookHandlers.end();)
|
||||
{
|
||||
if ((*i)._allowRemove && (*i)._id == id)
|
||||
{
|
||||
i = _hookHandlers.erase(i);
|
||||
}
|
||||
else
|
||||
{
|
||||
++i;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StateUpdateResult update(std::function<StateUpdateResult(T &)> stateUpdater, const String &originId)
|
||||
{
|
||||
beginTransaction();
|
||||
StateUpdateResult result = stateUpdater(_state);
|
||||
endTransaction();
|
||||
callHookHandlers(originId, result);
|
||||
if (result == StateUpdateResult::CHANGED)
|
||||
{
|
||||
callUpdateHandlers(originId);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
StateUpdateResult updateWithoutPropagation(std::function<StateUpdateResult(T &)> stateUpdater)
|
||||
{
|
||||
beginTransaction();
|
||||
StateUpdateResult result = stateUpdater(_state);
|
||||
endTransaction();
|
||||
return result;
|
||||
}
|
||||
|
||||
StateUpdateResult update(JsonObject &jsonObject, JsonStateUpdater<T> stateUpdater, const String &originId)
|
||||
{
|
||||
beginTransaction();
|
||||
StateUpdateResult result = stateUpdater(jsonObject, _state);
|
||||
endTransaction();
|
||||
callHookHandlers(originId, result);
|
||||
if (result == StateUpdateResult::CHANGED)
|
||||
{
|
||||
callUpdateHandlers(originId);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
StateUpdateResult updateWithoutPropagation(JsonObject &jsonObject, JsonStateUpdater<T> stateUpdater)
|
||||
{
|
||||
beginTransaction();
|
||||
StateUpdateResult result = stateUpdater(jsonObject, _state);
|
||||
endTransaction();
|
||||
return result;
|
||||
}
|
||||
|
||||
void read(std::function<void(T &)> stateReader)
|
||||
{
|
||||
beginTransaction();
|
||||
stateReader(_state);
|
||||
endTransaction();
|
||||
}
|
||||
|
||||
void read(JsonObject &jsonObject, JsonStateReader<T> stateReader)
|
||||
{
|
||||
beginTransaction();
|
||||
stateReader(_state, jsonObject);
|
||||
endTransaction();
|
||||
}
|
||||
|
||||
void callUpdateHandlers(const String &originId)
|
||||
{
|
||||
for (const StateUpdateHandlerInfo_t &updateHandler : _updateHandlers)
|
||||
{
|
||||
updateHandler._cb(originId);
|
||||
}
|
||||
}
|
||||
|
||||
void callHookHandlers(const String &originId, StateUpdateResult &result)
|
||||
{
|
||||
for (const StateHookHandlerInfo_t &hookHandler : _hookHandlers)
|
||||
{
|
||||
hookHandler._cb(originId, result);
|
||||
}
|
||||
}
|
||||
|
||||
protected:
|
||||
T _state;
|
||||
|
||||
inline void beginTransaction()
|
||||
{
|
||||
xSemaphoreTakeRecursive(_accessMutex, portMAX_DELAY);
|
||||
}
|
||||
|
||||
inline void endTransaction()
|
||||
{
|
||||
xSemaphoreGiveRecursive(_accessMutex);
|
||||
}
|
||||
|
||||
private:
|
||||
SemaphoreHandle_t _accessMutex;
|
||||
std::list<StateUpdateHandlerInfo_t> _updateHandlers;
|
||||
std::list<StateHookHandlerInfo_t> _hookHandlers;
|
||||
};
|
||||
|
||||
#endif // end StatefulService_h
|
||||
@@ -0,0 +1,141 @@
|
||||
/**
|
||||
* ESP32 SvelteKit
|
||||
*
|
||||
* A simple, secure and extensible framework for IoT projects for ESP32 platforms
|
||||
* with responsive Sveltekit front-end built with TailwindCSS and DaisyUI.
|
||||
* https://github.com/theelims/ESP32-sveltekit
|
||||
*
|
||||
* Copyright (C) 2018 - 2023 rjwats
|
||||
* Copyright (C) 2023 theelims
|
||||
*
|
||||
* All Rights Reserved. This software may be modified and distributed under
|
||||
* the terms of the LGPL v3 license. See the LICENSE file for details.
|
||||
**/
|
||||
|
||||
#include <SystemStatus.h>
|
||||
#include <esp32-hal.h>
|
||||
|
||||
#if CONFIG_IDF_TARGET_ESP32 // ESP32/PICO-D4
|
||||
#include "esp32/rom/rtc.h"
|
||||
#define ESP_PLATFORM "ESP32";
|
||||
#elif CONFIG_IDF_TARGET_ESP32S2
|
||||
#include "esp32/rom/rtc.h"
|
||||
#define ESP_PLATFORM "ESP32-S2";
|
||||
#elif CONFIG_IDF_TARGET_ESP32C3
|
||||
#include "esp32c3/rom/rtc.h"
|
||||
#define ESP_PLATFORM "ESP32-C3";
|
||||
#elif CONFIG_IDF_TARGET_ESP32S3
|
||||
#include "esp32s3/rom/rtc.h"
|
||||
#define ESP_PLATFORM "ESP32-S3";
|
||||
#else
|
||||
#error Target CONFIG_IDF_TARGET is not supported
|
||||
#endif
|
||||
|
||||
#ifndef ARDUINO_VERSION
|
||||
#ifndef STRINGIZE
|
||||
#define STRINGIZE(s) #s
|
||||
#endif
|
||||
#define ARDUINO_VERSION_STR(major, minor, patch) "v" STRINGIZE(major) "." STRINGIZE(minor) "." STRINGIZE(patch)
|
||||
#define ARDUINO_VERSION ARDUINO_VERSION_STR(ESP_ARDUINO_VERSION_MAJOR, ESP_ARDUINO_VERSION_MINOR, ESP_ARDUINO_VERSION_PATCH)
|
||||
#endif
|
||||
|
||||
String verbosePrintResetReason(int reason)
|
||||
{
|
||||
switch (reason)
|
||||
{
|
||||
case 1:
|
||||
return ("Vbat power on reset");
|
||||
break;
|
||||
case 3:
|
||||
return ("Software reset digital core");
|
||||
break;
|
||||
case 4:
|
||||
return ("Legacy watch dog reset digital core");
|
||||
break;
|
||||
case 5:
|
||||
return ("Deep Sleep reset digital core");
|
||||
break;
|
||||
case 6:
|
||||
return ("Reset by SLC module, reset digital core");
|
||||
break;
|
||||
case 7:
|
||||
return ("Timer Group0 Watch dog reset digital core");
|
||||
break;
|
||||
case 8:
|
||||
return ("Timer Group1 Watch dog reset digital core");
|
||||
break;
|
||||
case 9:
|
||||
return ("RTC Watch dog Reset digital core");
|
||||
break;
|
||||
case 10:
|
||||
return ("Intrusion tested to reset CPU");
|
||||
break;
|
||||
case 11:
|
||||
return ("Time Group reset CPU");
|
||||
break;
|
||||
case 12:
|
||||
return ("Software reset CPU");
|
||||
break;
|
||||
case 13:
|
||||
return ("RTC Watch dog Reset CPU");
|
||||
break;
|
||||
case 14:
|
||||
return ("for APP CPU, reseted by PRO CPU");
|
||||
break;
|
||||
case 15:
|
||||
return ("Reset when the vdd voltage is not stable");
|
||||
break;
|
||||
case 16:
|
||||
return ("RTC Watch dog reset digital core and rtc module");
|
||||
break;
|
||||
default:
|
||||
return ("NO_MEAN");
|
||||
}
|
||||
}
|
||||
|
||||
SystemStatus::SystemStatus(PsychicHttpServer *server,
|
||||
SecurityManager *securityManager) : _server(server),
|
||||
_securityManager(securityManager)
|
||||
{
|
||||
}
|
||||
|
||||
void SystemStatus::begin()
|
||||
{
|
||||
_server->on(SYSTEM_STATUS_SERVICE_PATH,
|
||||
HTTP_GET,
|
||||
_securityManager->wrapRequest(std::bind(&SystemStatus::systemStatus, this, std::placeholders::_1),
|
||||
AuthenticationPredicates::IS_AUTHENTICATED));
|
||||
|
||||
ESP_LOGV("SystemStatus", "Registered GET endpoint: %s", SYSTEM_STATUS_SERVICE_PATH);
|
||||
}
|
||||
|
||||
esp_err_t SystemStatus::systemStatus(PsychicRequest *request)
|
||||
{
|
||||
PsychicJsonResponse response = PsychicJsonResponse(request, false, MAX_ESP_STATUS_SIZE);
|
||||
JsonObject root = response.getRoot();
|
||||
|
||||
root["esp_platform"] = ESP_PLATFORM;
|
||||
root["firmware_version"] = APP_VERSION;
|
||||
root["max_alloc_heap"] = ESP.getMaxAllocHeap();
|
||||
root["psram_size"] = ESP.getPsramSize();
|
||||
root["free_psram"] = ESP.getFreePsram();
|
||||
root["cpu_freq_mhz"] = ESP.getCpuFreqMHz();
|
||||
root["cpu_type"] = ESP.getChipModel();
|
||||
root["cpu_rev"] = ESP.getChipRevision();
|
||||
root["cpu_cores"] = ESP.getChipCores();
|
||||
root["free_heap"] = ESP.getFreeHeap();
|
||||
root["min_free_heap"] = ESP.getMinFreeHeap();
|
||||
root["sketch_size"] = ESP.getSketchSize();
|
||||
root["free_sketch_space"] = ESP.getFreeSketchSpace();
|
||||
root["sdk_version"] = ESP.getSdkVersion();
|
||||
root["arduino_version"] = ARDUINO_VERSION;
|
||||
root["flash_chip_size"] = ESP.getFlashChipSize();
|
||||
root["flash_chip_speed"] = ESP.getFlashChipSpeed();
|
||||
root["fs_total"] = ESPFS.totalBytes();
|
||||
root["fs_used"] = ESPFS.usedBytes();
|
||||
root["core_temp"] = temperatureRead();
|
||||
root["cpu_reset_reason"] = verbosePrintResetReason(rtc_get_reset_reason(0));
|
||||
root["uptime"] = millis() / 1000;
|
||||
|
||||
return response.send();
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
#ifndef SystemStatus_h
|
||||
#define SystemStatus_h
|
||||
|
||||
/**
|
||||
* ESP32 SvelteKit
|
||||
*
|
||||
* A simple, secure and extensible framework for IoT projects for ESP32 platforms
|
||||
* with responsive Sveltekit front-end built with TailwindCSS and DaisyUI.
|
||||
* https://github.com/theelims/ESP32-sveltekit
|
||||
*
|
||||
* Copyright (C) 2018 - 2023 rjwats
|
||||
* Copyright (C) 2023 theelims
|
||||
*
|
||||
* All Rights Reserved. This software may be modified and distributed under
|
||||
* the terms of the LGPL v3 license. See the LICENSE file for details.
|
||||
**/
|
||||
|
||||
#include <WiFi.h>
|
||||
|
||||
#include <ArduinoJson.h>
|
||||
#include <PsychicHttp.h>
|
||||
#include <SecurityManager.h>
|
||||
#include <ESPFS.h>
|
||||
|
||||
#define MAX_ESP_STATUS_SIZE 1024
|
||||
#define SYSTEM_STATUS_SERVICE_PATH "/rest/systemStatus"
|
||||
|
||||
class SystemStatus
|
||||
{
|
||||
public:
|
||||
SystemStatus(PsychicHttpServer *server, SecurityManager *securityManager);
|
||||
|
||||
void begin();
|
||||
|
||||
private:
|
||||
PsychicHttpServer *_server;
|
||||
SecurityManager *_securityManager;
|
||||
esp_err_t systemStatus(PsychicRequest *request);
|
||||
};
|
||||
|
||||
#endif // end SystemStatus_h
|
||||
@@ -0,0 +1,205 @@
|
||||
/**
|
||||
* ESP32 SvelteKit
|
||||
*
|
||||
* A simple, secure and extensible framework for IoT projects for ESP32 platforms
|
||||
* with responsive Sveltekit front-end built with TailwindCSS and DaisyUI.
|
||||
* https://github.com/theelims/ESP32-sveltekit
|
||||
*
|
||||
* Copyright (C) 2018 - 2023 rjwats
|
||||
* Copyright (C) 2023 theelims
|
||||
*
|
||||
* All Rights Reserved. This software may be modified and distributed under
|
||||
* the terms of the LGPL v3 license. See the LICENSE file for details.
|
||||
**/
|
||||
|
||||
#include <UploadFirmwareService.h>
|
||||
#include <esp_ota_ops.h>
|
||||
#include <esp_app_format.h>
|
||||
|
||||
using namespace std::placeholders; // for `_1` etc
|
||||
|
||||
static char md5[33] = "\0";
|
||||
|
||||
static FileType fileType = ft_none;
|
||||
|
||||
UploadFirmwareService::UploadFirmwareService(PsychicHttpServer *server,
|
||||
SecurityManager *securityManager) : _server(server),
|
||||
_securityManager(securityManager)
|
||||
{
|
||||
}
|
||||
|
||||
void UploadFirmwareService::begin()
|
||||
{
|
||||
_server->maxUploadSize = 2300000; // 2.3 MB
|
||||
|
||||
PsychicUploadHandler *uploadHandler = new PsychicUploadHandler();
|
||||
|
||||
uploadHandler->onUpload(std::bind(&UploadFirmwareService::handleUpload, this, _1, _2, _3, _4, _5, _6));
|
||||
uploadHandler->onRequest(std::bind(&UploadFirmwareService::uploadComplete, this, _1)); // gets called after upload has been handled
|
||||
uploadHandler->onClose(std::bind(&UploadFirmwareService::handleEarlyDisconnect, this)); // gets called if client disconnects
|
||||
_server->on(UPLOAD_FIRMWARE_PATH, HTTP_POST, uploadHandler);
|
||||
|
||||
ESP_LOGV("UploadFirmwareService", "Registered POST endpoint: %s", UPLOAD_FIRMWARE_PATH);
|
||||
}
|
||||
|
||||
esp_err_t UploadFirmwareService::handleUpload(PsychicRequest *request,
|
||||
const String &filename,
|
||||
uint64_t index,
|
||||
uint8_t *data,
|
||||
size_t len,
|
||||
bool final)
|
||||
{
|
||||
// quit if not authorized
|
||||
Authentication authentication = _securityManager->authenticateRequest(request);
|
||||
if (!AuthenticationPredicates::IS_ADMIN(authentication))
|
||||
{
|
||||
return handleError(request, 403); // forbidden
|
||||
}
|
||||
|
||||
// at init
|
||||
if (!index)
|
||||
{
|
||||
// check details of the file, to see if its a valid bin or json file
|
||||
std::string fname(filename.c_str());
|
||||
auto position = fname.find_last_of(".");
|
||||
std::string extension = fname.substr(position + 1);
|
||||
size_t fsize = request->contentLength();
|
||||
|
||||
fileType = ft_none;
|
||||
if ((extension == "bin") && (fsize > 1000000))
|
||||
{
|
||||
fileType = ft_firmware;
|
||||
}
|
||||
else if (extension == "md5")
|
||||
{
|
||||
fileType = ft_md5;
|
||||
if (len == 32)
|
||||
{
|
||||
memcpy(md5, data, 32);
|
||||
md5[32] = '\0';
|
||||
}
|
||||
return ESP_OK;
|
||||
}
|
||||
else
|
||||
{
|
||||
md5[0] = '\0';
|
||||
return handleError(request, 406); // Not Acceptable - unsupported file type
|
||||
}
|
||||
|
||||
if (fileType == ft_firmware)
|
||||
{
|
||||
// Check firmware header, 0xE9 magic offset 0 indicates esp bin, chip offset 12: esp32:0, S2:2, C3:5
|
||||
#if CONFIG_IDF_TARGET_ESP32 // ESP32/PICO-D4
|
||||
if (len > 12 && (data[0] != 0xE9 || data[12] != 0))
|
||||
{
|
||||
return handleError(request, 503); // service unavailable
|
||||
}
|
||||
#elif CONFIG_IDF_TARGET_ESP32S2
|
||||
if (len > 12 && (data[0] != 0xE9 || data[12] != 2))
|
||||
{
|
||||
return handleError(request, 503); // service unavailable
|
||||
}
|
||||
#elif CONFIG_IDF_TARGET_ESP32C3
|
||||
if (len > 12 && (data[0] != 0xE9 || data[12] != 5))
|
||||
{
|
||||
return handleError(request, 503); // service unavailable
|
||||
}
|
||||
#elif CONFIG_IDF_TARGET_ESP32S3
|
||||
if (len > 12 && (data[0] != 0xE9 || data[12] != 9))
|
||||
{
|
||||
return handleError(request, 503); // service unavailable
|
||||
}
|
||||
#endif
|
||||
// it's firmware - initialize the ArduinoOTA updater
|
||||
if (Update.begin(fsize - sizeof(esp_image_header_t)))
|
||||
{
|
||||
if (strlen(md5) == 32)
|
||||
{
|
||||
Update.setMD5(md5);
|
||||
md5[0] = '\0';
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
return handleError(request, 507); // failed to begin, send an error response Insufficient Storage
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// if we haven't delt with an error, continue with the firmware update
|
||||
if (!request->_tempObject)
|
||||
{
|
||||
if (Update.write(data, len) != len)
|
||||
{
|
||||
handleError(request, 500);
|
||||
}
|
||||
if (final)
|
||||
{
|
||||
if (!Update.end(true))
|
||||
{
|
||||
handleError(request, 500);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t UploadFirmwareService::uploadComplete(PsychicRequest *request)
|
||||
{
|
||||
// if we completed uploading a md5 file create a JSON response
|
||||
if (fileType == ft_md5)
|
||||
{
|
||||
if (strlen(md5) == 32)
|
||||
{
|
||||
PsychicJsonResponse response = PsychicJsonResponse(request, false, 256);
|
||||
JsonObject root = response.getRoot();
|
||||
root["md5"] = md5;
|
||||
return response.send();
|
||||
}
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
// if no error, send the success response
|
||||
if (!request->_tempObject)
|
||||
{
|
||||
request->reply(200);
|
||||
RestartService::restartNow();
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
// if updated has an error send 500 response and log on Serial
|
||||
if (Update.hasError())
|
||||
{
|
||||
Update.printError(Serial);
|
||||
Update.abort();
|
||||
handleError(request, 500);
|
||||
}
|
||||
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t UploadFirmwareService::handleError(PsychicRequest *request, int code)
|
||||
{
|
||||
// if we have had an error already, do nothing
|
||||
if (request->_tempObject)
|
||||
{
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
// send the error code to the client and record the error code in the temp object
|
||||
request->_tempObject = new int(code);
|
||||
return request->reply(code);
|
||||
}
|
||||
|
||||
esp_err_t UploadFirmwareService::handleEarlyDisconnect()
|
||||
{
|
||||
// if updated has not ended on connection close, abort it
|
||||
if (!Update.end(true))
|
||||
{
|
||||
Update.printError(Serial);
|
||||
Update.abort();
|
||||
return ESP_OK;
|
||||
}
|
||||
return ESP_OK;
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
#ifndef UploadFirmwareService_h
|
||||
#define UploadFirmwareService_h
|
||||
|
||||
/**
|
||||
* ESP32 SvelteKit
|
||||
*
|
||||
* A simple, secure and extensible framework for IoT projects for ESP32 platforms
|
||||
* with responsive Sveltekit front-end built with TailwindCSS and DaisyUI.
|
||||
* https://github.com/theelims/ESP32-sveltekit
|
||||
*
|
||||
* Copyright (C) 2018 - 2023 rjwats
|
||||
* Copyright (C) 2023 theelims
|
||||
*
|
||||
* All Rights Reserved. This software may be modified and distributed under
|
||||
* the terms of the LGPL v3 license. See the LICENSE file for details.
|
||||
**/
|
||||
|
||||
#include <Arduino.h>
|
||||
|
||||
#include <Update.h>
|
||||
#include <WiFi.h>
|
||||
|
||||
#include <PsychicHttp.h>
|
||||
#include <SecurityManager.h>
|
||||
#include <RestartService.h>
|
||||
|
||||
#define UPLOAD_FIRMWARE_PATH "/rest/uploadFirmware"
|
||||
|
||||
enum FileType
|
||||
{
|
||||
ft_none = 0,
|
||||
ft_firmware = 1,
|
||||
ft_md5 = 2
|
||||
};
|
||||
|
||||
class UploadFirmwareService
|
||||
{
|
||||
public:
|
||||
UploadFirmwareService(PsychicHttpServer *server, SecurityManager *securityManager);
|
||||
|
||||
void begin();
|
||||
|
||||
private:
|
||||
PsychicHttpServer *_server;
|
||||
SecurityManager *_securityManager;
|
||||
|
||||
esp_err_t handleUpload(PsychicRequest *request,
|
||||
const String &filename,
|
||||
uint64_t index,
|
||||
uint8_t *data,
|
||||
size_t len,
|
||||
bool final);
|
||||
esp_err_t uploadComplete(PsychicRequest *request);
|
||||
esp_err_t handleError(PsychicRequest *request, int code);
|
||||
esp_err_t handleEarlyDisconnect();
|
||||
};
|
||||
|
||||
#endif // end UploadFirmwareService_h
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,243 @@
|
||||
#pragma once
|
||||
/**
|
||||
* ESP32 SvelteKit
|
||||
*
|
||||
* A simple, secure and extensible framework for IoT projects for ESP32 platforms
|
||||
* with responsive Sveltekit front-end built with TailwindCSS and DaisyUI.
|
||||
* https://github.com/theelims/ESP32-sveltekit
|
||||
*
|
||||
* Copyright (C) 2023 theelims
|
||||
*
|
||||
* All Rights Reserved. This software may be modified and distributed under
|
||||
* the terms of the LGPL v3 license. See the LICENSE file for details.
|
||||
**/
|
||||
|
||||
#include <StatefulService.h>
|
||||
#include <SecurityManager.h>
|
||||
#include <esp_websocket_client.h>
|
||||
#include <esp_int_wdt.h>
|
||||
#include "freertos/timers.h"
|
||||
|
||||
#define WEB_SOCKET_CLIENT_ORIGIN "wsclient"
|
||||
|
||||
static const char root_CA[] PROGMEM = R"EOF(
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIFazCCA1OgAwIBAgIRAIIQz7DSQONZRGPgu2OCiwAwDQYJKoZIhvcNAQELBQAw
|
||||
TzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2Vh
|
||||
cmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwHhcNMTUwNjA0MTEwNDM4
|
||||
WhcNMzUwNjA0MTEwNDM4WjBPMQswCQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJu
|
||||
ZXQgU2VjdXJpdHkgUmVzZWFyY2ggR3JvdXAxFTATBgNVBAMTDElTUkcgUm9vdCBY
|
||||
MTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAK3oJHP0FDfzm54rVygc
|
||||
h77ct984kIxuPOZXoHj3dcKi/vVqbvYATyjb3miGbESTtrFj/RQSa78f0uoxmyF+
|
||||
0TM8ukj13Xnfs7j/EvEhmkvBioZxaUpmZmyPfjxwv60pIgbz5MDmgK7iS4+3mX6U
|
||||
A5/TR5d8mUgjU+g4rk8Kb4Mu0UlXjIB0ttov0DiNewNwIRt18jA8+o+u3dpjq+sW
|
||||
T8KOEUt+zwvo/7V3LvSye0rgTBIlDHCNAymg4VMk7BPZ7hm/ELNKjD+Jo2FR3qyH
|
||||
B5T0Y3HsLuJvW5iB4YlcNHlsdu87kGJ55tukmi8mxdAQ4Q7e2RCOFvu396j3x+UC
|
||||
B5iPNgiV5+I3lg02dZ77DnKxHZu8A/lJBdiB3QW0KtZB6awBdpUKD9jf1b0SHzUv
|
||||
KBds0pjBqAlkd25HN7rOrFleaJ1/ctaJxQZBKT5ZPt0m9STJEadao0xAH0ahmbWn
|
||||
OlFuhjuefXKnEgV4We0+UXgVCwOPjdAvBbI+e0ocS3MFEvzG6uBQE3xDk3SzynTn
|
||||
jh8BCNAw1FtxNrQHusEwMFxIt4I7mKZ9YIqioymCzLq9gwQbooMDQaHWBfEbwrbw
|
||||
qHyGO0aoSCqI3Haadr8faqU9GY/rOPNk3sgrDQoo//fb4hVC1CLQJ13hef4Y53CI
|
||||
rU7m2Ys6xt0nUW7/vGT1M0NPAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNV
|
||||
HRMBAf8EBTADAQH/MB0GA1UdDgQWBBR5tFnme7bl5AFzgAiIyBpY9umbbjANBgkq
|
||||
hkiG9w0BAQsFAAOCAgEAVR9YqbyyqFDQDLHYGmkgJykIrGF1XIpu+ILlaS/V9lZL
|
||||
ubhzEFnTIZd+50xx+7LSYK05qAvqFyFWhfFQDlnrzuBZ6brJFe+GnY+EgPbk6ZGQ
|
||||
3BebYhtF8GaV0nxvwuo77x/Py9auJ/GpsMiu/X1+mvoiBOv/2X/qkSsisRcOj/KK
|
||||
NFtY2PwByVS5uCbMiogziUwthDyC3+6WVwW6LLv3xLfHTjuCvjHIInNzktHCgKQ5
|
||||
ORAzI4JMPJ+GslWYHb4phowim57iaztXOoJwTdwJx4nLCgdNbOhdjsnvzqvHu7Ur
|
||||
TkXWStAmzOVyyghqpZXjFaH3pO3JLF+l+/+sKAIuvtd7u+Nxe5AW0wdeRlN8NwdC
|
||||
jNPElpzVmbUq4JUagEiuTDkHzsxHpFKVK7q4+63SM1N95R1NbdWhscdCb+ZAJzVc
|
||||
oyi3B43njTOQ5yOf+1CceWxG1bQVs5ZufpsMljq4Ui0/1lvh+wjChP4kqKOJ2qxq
|
||||
4RgqsahDYVvTH9w7jXbyLeiNdd8XM2w9U/t7y0Ff/9yi0GE44Za4rF2LN9d11TPA
|
||||
mRGunUHBcnWEvgJBQl9nJEiU0Zsnvgc/ubhPgXRR4Xq37Z0j4r7g1SgEEzwxA57d
|
||||
emyPxgcYxn/eR44/KJ4EBs+lVDR3veyJm+kXQ99b21/+jh5Xos1AnX5iItreGCc=
|
||||
-----END CERTIFICATE-----
|
||||
)EOF";
|
||||
|
||||
static const char *wsTAG = "WS_Client";
|
||||
|
||||
template <class T>
|
||||
class WebSocketClient
|
||||
{
|
||||
public:
|
||||
WebSocketClient(JsonStateReader<T> stateReader,
|
||||
JsonStateUpdater<T> stateUpdater,
|
||||
StatefulService<T> *statefulService,
|
||||
const char *webSocketUri,
|
||||
size_t bufferSize = DEFAULT_BUFFER_SIZE) : _stateReader(stateReader),
|
||||
_stateUpdater(stateUpdater),
|
||||
_statefulService(statefulService),
|
||||
_bufferSize(bufferSize)
|
||||
{
|
||||
_statefulService->addUpdateHandler(
|
||||
[&](const String &originId)
|
||||
{ transmitData(originId); },
|
||||
false);
|
||||
|
||||
xTaskCreatePinnedToCore(
|
||||
this->loopImpl,
|
||||
"initiateClientTask",
|
||||
4096,
|
||||
this,
|
||||
tskIDLE_PRIORITY + 1,
|
||||
NULL,
|
||||
ESP32SVELTEKIT_RUNNING_CORE);
|
||||
|
||||
wsClientConfig(webSocketUri);
|
||||
|
||||
wsClientConnect();
|
||||
}
|
||||
|
||||
boolean wsClientConnected()
|
||||
{
|
||||
return esp_websocket_client_is_connected(client);
|
||||
}
|
||||
|
||||
void wsClientConnect()
|
||||
{
|
||||
if (client)
|
||||
{
|
||||
esp_websocket_client_start(client);
|
||||
freshConnected = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
ESP_LOGE(wsTAG, "Websocket client is not initialized.");
|
||||
}
|
||||
}
|
||||
|
||||
void wsClientDisconnect()
|
||||
{
|
||||
esp_websocket_client_close(client, pdTICKS_TO_MS(200));
|
||||
esp_websocket_client_destroy(client);
|
||||
client = nullptr;
|
||||
}
|
||||
|
||||
void wsClientConfig(const char *webSocketUri)
|
||||
{
|
||||
if (client)
|
||||
{
|
||||
wsClientDisconnect();
|
||||
}
|
||||
|
||||
// Configure WS Client
|
||||
websocket_cfg.uri = "ws://192.168.1.91:1880/ws/request";
|
||||
//.uri = "wss://webhook.xtoys.app/ypFXRRx9kzkn?token=91ffdf3434946903a6d34acba97e9b69",
|
||||
|
||||
websocket_cfg.buffer_size = _bufferSize;
|
||||
|
||||
// websocket_cfg.cert_pem = (const char *)root_CA;
|
||||
// websocket_cfg.cert_len = sizeof(root_CA);
|
||||
|
||||
client = esp_websocket_client_init(&websocket_cfg);
|
||||
|
||||
// Register event handler
|
||||
esp_websocket_register_events(client, WEBSOCKET_EVENT_ANY, &onWSEventStatic, (void *)client);
|
||||
ESP_LOGI(wsTAG, "Websocket Client configured to %s", websocket_cfg.uri);
|
||||
}
|
||||
|
||||
protected:
|
||||
StatefulService<T> *_statefulService;
|
||||
esp_websocket_client_handle_t client = nullptr;
|
||||
esp_websocket_client_config_t websocket_cfg;
|
||||
size_t _bufferSize;
|
||||
JsonStateUpdater<T> _stateUpdater;
|
||||
JsonStateReader<T> _stateReader;
|
||||
boolean freshConnected = true;
|
||||
|
||||
static void onWSEventStatic(void *handler_args,
|
||||
esp_event_base_t base,
|
||||
int32_t event_id,
|
||||
void *event_data)
|
||||
{
|
||||
// Since this is a static function, we need to cast the first argument (void*) back to the class instance type
|
||||
WebSocketClient *instance = (WebSocketClient *)handler_args;
|
||||
instance->onWSEvent(handler_args, base, event_id, event_data);
|
||||
}
|
||||
|
||||
void onWSEvent(void *handler_args,
|
||||
esp_event_base_t base,
|
||||
int32_t event_id,
|
||||
void *event_data)
|
||||
{
|
||||
switch (event_id)
|
||||
{
|
||||
case WEBSOCKET_EVENT_DISCONNECTED:
|
||||
ESP_LOGI(wsTAG, "WEBSOCKET_EVENT_DISCONNECTED");
|
||||
// mark as disconnected
|
||||
freshConnected = true;
|
||||
break;
|
||||
case WEBSOCKET_EVENT_ERROR:
|
||||
ESP_LOGI(wsTAG, "WEBSOCKET_EVENT_ERROR");
|
||||
break;
|
||||
case WEBSOCKET_EVENT_DATA:
|
||||
|
||||
esp_websocket_event_data_t *data = (esp_websocket_event_data_t *)event_data;
|
||||
|
||||
ESP_LOGV(wsTAG, "WEBSOCKET_EVENT_DATA");
|
||||
ESP_LOGV(wsTAG, "Received opcode=%d", data->op_code);
|
||||
if (data->op_code == 0x08 && data->data_len == 2)
|
||||
{
|
||||
ESP_LOGW(wsTAG, "Received closed message with code=%d", 256 * data->data_ptr[0] + data->data_ptr[1]);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Filter for Text-Messages
|
||||
if (data->op_code == 0x01)
|
||||
{
|
||||
ESP_LOGV(wsTAG, "Total payload length=%d, data_len=%d, current payload offset=%d", data->payload_len, data->data_len, data->payload_offset);
|
||||
ESP_LOGV(wsTAG, "Received=%.*s", data->data_len, (char *)data->data_ptr);
|
||||
|
||||
// Copy the characters from data->data_ptr to c-string
|
||||
char payload[_bufferSize];
|
||||
strncpy(payload, (char *)data->data_ptr, data->data_len);
|
||||
payload[data->data_len] = '\0';
|
||||
ESP_LOGV(wsTAG, "Payload=%s", payload);
|
||||
|
||||
// DynamicJsonDocument jsonDocument = DynamicJsonDocument(_bufferSize);
|
||||
// DeserializationError error = deserializeJson(jsonDocument, payload);
|
||||
|
||||
// ESP_LOGE(wsTAG, "deserializeJson() status: %s", error.c_str());
|
||||
|
||||
/* if (!error && jsonDocument.is<JsonObject>())
|
||||
{
|
||||
JsonObject jsonObject = jsonDocument.as<JsonObject>();
|
||||
_statefulService->update(
|
||||
jsonObject, _stateUpdater, WEB_SOCKET_CLIENT_ORIGIN);
|
||||
ESP_LOGV(wsTAG, "Updated StatefulService");
|
||||
} */
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
static void loopImpl(void *_this) { static_cast<WebSocketClient *>(_this)->loop(); }
|
||||
|
||||
void loop()
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
// Workaround for a bug in the WEBSOCKET_EVENT_CONNECTED event handler causing a kernel panic. Revisit with ESP-IDF v5.1
|
||||
if (wsClientConnected() && freshConnected)
|
||||
{
|
||||
ESP_LOGI(wsTAG, "Websocket Client Connected");
|
||||
transmitData(WEB_SOCKET_ORIGIN);
|
||||
freshConnected = false;
|
||||
}
|
||||
|
||||
vTaskDelay(pdMS_TO_TICKS(150));
|
||||
}
|
||||
}
|
||||
|
||||
void transmitData(const String &originId)
|
||||
{
|
||||
DynamicJsonDocument jsonDocument = DynamicJsonDocument(_bufferSize);
|
||||
JsonObject payload = jsonDocument.to<JsonObject>();
|
||||
_statefulService->read(payload, _stateReader);
|
||||
String json_string;
|
||||
serializeJson(jsonDocument, json_string);
|
||||
esp_websocket_client_send_text(client, json_string.c_str(), json_string.length() + 1, portMAX_DELAY);
|
||||
ESP_LOGI(wsTAG, "Websocket Client Transmission: %s", json_string.c_str());
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,245 @@
|
||||
#pragma once
|
||||
/**
|
||||
* ESP32 SvelteKit
|
||||
*
|
||||
* A simple, secure and extensible framework for IoT projects for ESP32 platforms
|
||||
* with responsive Sveltekit front-end built with TailwindCSS and DaisyUI.
|
||||
* https://github.com/theelims/ESP32-sveltekit
|
||||
*
|
||||
* Copyright (C) 2023 theelims
|
||||
*
|
||||
* All Rights Reserved. This software may be modified and distributed under
|
||||
* the terms of the LGPL v3 license. See the LICENSE file for details.
|
||||
**/
|
||||
|
||||
#include <StatefulService.h>
|
||||
#include <SecurityManager.h>
|
||||
#include <esp_websocket_client.h>
|
||||
#include <esp_int_wdt.h>
|
||||
#include "freertos/timers.h"
|
||||
|
||||
#define WEB_SOCKET_CLIENT_ORIGIN "wsclient"
|
||||
|
||||
static const char root_CA[] PROGMEM = R"EOF(
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIFazCCA1OgAwIBAgIRAIIQz7DSQONZRGPgu2OCiwAwDQYJKoZIhvcNAQELBQAw
|
||||
TzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2Vh
|
||||
cmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwHhcNMTUwNjA0MTEwNDM4
|
||||
WhcNMzUwNjA0MTEwNDM4WjBPMQswCQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJu
|
||||
ZXQgU2VjdXJpdHkgUmVzZWFyY2ggR3JvdXAxFTATBgNVBAMTDElTUkcgUm9vdCBY
|
||||
MTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAK3oJHP0FDfzm54rVygc
|
||||
h77ct984kIxuPOZXoHj3dcKi/vVqbvYATyjb3miGbESTtrFj/RQSa78f0uoxmyF+
|
||||
0TM8ukj13Xnfs7j/EvEhmkvBioZxaUpmZmyPfjxwv60pIgbz5MDmgK7iS4+3mX6U
|
||||
A5/TR5d8mUgjU+g4rk8Kb4Mu0UlXjIB0ttov0DiNewNwIRt18jA8+o+u3dpjq+sW
|
||||
T8KOEUt+zwvo/7V3LvSye0rgTBIlDHCNAymg4VMk7BPZ7hm/ELNKjD+Jo2FR3qyH
|
||||
B5T0Y3HsLuJvW5iB4YlcNHlsdu87kGJ55tukmi8mxdAQ4Q7e2RCOFvu396j3x+UC
|
||||
B5iPNgiV5+I3lg02dZ77DnKxHZu8A/lJBdiB3QW0KtZB6awBdpUKD9jf1b0SHzUv
|
||||
KBds0pjBqAlkd25HN7rOrFleaJ1/ctaJxQZBKT5ZPt0m9STJEadao0xAH0ahmbWn
|
||||
OlFuhjuefXKnEgV4We0+UXgVCwOPjdAvBbI+e0ocS3MFEvzG6uBQE3xDk3SzynTn
|
||||
jh8BCNAw1FtxNrQHusEwMFxIt4I7mKZ9YIqioymCzLq9gwQbooMDQaHWBfEbwrbw
|
||||
qHyGO0aoSCqI3Haadr8faqU9GY/rOPNk3sgrDQoo//fb4hVC1CLQJ13hef4Y53CI
|
||||
rU7m2Ys6xt0nUW7/vGT1M0NPAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNV
|
||||
HRMBAf8EBTADAQH/MB0GA1UdDgQWBBR5tFnme7bl5AFzgAiIyBpY9umbbjANBgkq
|
||||
hkiG9w0BAQsFAAOCAgEAVR9YqbyyqFDQDLHYGmkgJykIrGF1XIpu+ILlaS/V9lZL
|
||||
ubhzEFnTIZd+50xx+7LSYK05qAvqFyFWhfFQDlnrzuBZ6brJFe+GnY+EgPbk6ZGQ
|
||||
3BebYhtF8GaV0nxvwuo77x/Py9auJ/GpsMiu/X1+mvoiBOv/2X/qkSsisRcOj/KK
|
||||
NFtY2PwByVS5uCbMiogziUwthDyC3+6WVwW6LLv3xLfHTjuCvjHIInNzktHCgKQ5
|
||||
ORAzI4JMPJ+GslWYHb4phowim57iaztXOoJwTdwJx4nLCgdNbOhdjsnvzqvHu7Ur
|
||||
TkXWStAmzOVyyghqpZXjFaH3pO3JLF+l+/+sKAIuvtd7u+Nxe5AW0wdeRlN8NwdC
|
||||
jNPElpzVmbUq4JUagEiuTDkHzsxHpFKVK7q4+63SM1N95R1NbdWhscdCb+ZAJzVc
|
||||
oyi3B43njTOQ5yOf+1CceWxG1bQVs5ZufpsMljq4Ui0/1lvh+wjChP4kqKOJ2qxq
|
||||
4RgqsahDYVvTH9w7jXbyLeiNdd8XM2w9U/t7y0Ff/9yi0GE44Za4rF2LN9d11TPA
|
||||
mRGunUHBcnWEvgJBQl9nJEiU0Zsnvgc/ubhPgXRR4Xq37Z0j4r7g1SgEEzwxA57d
|
||||
emyPxgcYxn/eR44/KJ4EBs+lVDR3veyJm+kXQ99b21/+jh5Xos1AnX5iItreGCc=
|
||||
-----END CERTIFICATE-----
|
||||
)EOF";
|
||||
|
||||
static const char *wsTAG = "WS_Client";
|
||||
|
||||
template <class T>
|
||||
class WebSocketClient
|
||||
{
|
||||
public:
|
||||
WebSocketClient(JsonStateReader<T> stateReader,
|
||||
JsonStateUpdater<T> stateUpdater,
|
||||
StatefulService<T> *statefulService,
|
||||
const char *webSocketUri,
|
||||
size_t bufferSize = DEFAULT_BUFFER_SIZE) : _stateReader(stateReader),
|
||||
_stateUpdater(stateUpdater),
|
||||
_statefulService(statefulService),
|
||||
_bufferSize(bufferSize)
|
||||
{
|
||||
_statefulService->addUpdateHandler(
|
||||
[&](const String &originId)
|
||||
{ transmitData(originId); },
|
||||
false);
|
||||
|
||||
xTaskCreatePinnedToCore(
|
||||
this->loopImpl,
|
||||
"initiateClientTask",
|
||||
4096,
|
||||
this,
|
||||
tskIDLE_PRIORITY + 1,
|
||||
NULL,
|
||||
ESP32SVELTEKIT_RUNNING_CORE);
|
||||
|
||||
wsClientConfig(webSocketUri);
|
||||
|
||||
wsClientConnect();
|
||||
}
|
||||
|
||||
boolean wsClientConnected()
|
||||
{
|
||||
return esp_websocket_client_is_connected(client);
|
||||
}
|
||||
|
||||
void wsClientConnect()
|
||||
{
|
||||
if (client)
|
||||
{
|
||||
esp_websocket_client_start(client);
|
||||
freshConnected = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
ESP_LOGE(wsTAG, "Websocket client is not initialized.");
|
||||
}
|
||||
}
|
||||
|
||||
void wsClientDisconnect()
|
||||
{
|
||||
esp_websocket_client_close(client, pdTICKS_TO_MS(200));
|
||||
esp_websocket_client_destroy(client);
|
||||
client = nullptr;
|
||||
}
|
||||
|
||||
void wsClientConfig(const char *webSocketUri)
|
||||
{
|
||||
if (client)
|
||||
{
|
||||
wsClientDisconnect();
|
||||
}
|
||||
|
||||
// Configure WS Client
|
||||
websocket_cfg.uri = "ws://192.168.1.91:1880/ws/request";
|
||||
//.uri = "wss://webhook.xtoys.app/ypFXRRx9kzkn?token=91ffdf3434946903a6d34acba97e9b69",
|
||||
|
||||
websocket_cfg.buffer_size = _bufferSize;
|
||||
|
||||
// websocket_cfg.cert_pem = (const char *)root_CA;
|
||||
// websocket_cfg.cert_len = sizeof(root_CA);
|
||||
|
||||
client = esp_websocket_client_init(&websocket_cfg);
|
||||
|
||||
// Register event handler
|
||||
esp_websocket_register_events(client, WEBSOCKET_EVENT_ANY, &onWSEventStatic, (void *)client);
|
||||
ESP_LOGI(wsTAG, "Websocket Client configured to %s", websocket_cfg.uri);
|
||||
}
|
||||
|
||||
protected:
|
||||
// TimerHandle_t timerHandle;
|
||||
StatefulService<T> *_statefulService;
|
||||
esp_websocket_client_handle_t client = nullptr;
|
||||
esp_websocket_client_config_t websocket_cfg;
|
||||
size_t _bufferSize;
|
||||
JsonStateUpdater<T> _stateUpdater;
|
||||
JsonStateReader<T> _stateReader;
|
||||
boolean freshConnected = true;
|
||||
|
||||
static void
|
||||
onWSEventStatic(void *handler_args,
|
||||
esp_event_base_t base,
|
||||
int32_t event_id,
|
||||
void *event_data)
|
||||
{
|
||||
// Since this is a static function, we need to cast the first argument (void*) back to the class instance type
|
||||
WebSocketClient *instance = (WebSocketClient *)handler_args;
|
||||
instance->onWSEvent(handler_args, base, event_id, event_data);
|
||||
}
|
||||
|
||||
void onWSEvent(void *handler_args,
|
||||
esp_event_base_t base,
|
||||
int32_t event_id,
|
||||
void *event_data)
|
||||
{
|
||||
switch (event_id)
|
||||
{
|
||||
case WEBSOCKET_EVENT_DISCONNECTED:
|
||||
ESP_LOGI(wsTAG, "WEBSOCKET_EVENT_DISCONNECTED");
|
||||
// mark as disconnected
|
||||
freshConnected = true;
|
||||
break;
|
||||
case WEBSOCKET_EVENT_ERROR:
|
||||
ESP_LOGI(wsTAG, "WEBSOCKET_EVENT_ERROR");
|
||||
break;
|
||||
case WEBSOCKET_EVENT_DATA:
|
||||
|
||||
esp_websocket_event_data_t *data = (esp_websocket_event_data_t *)event_data;
|
||||
|
||||
ESP_LOGV(wsTAG, "WEBSOCKET_EVENT_DATA");
|
||||
ESP_LOGV(wsTAG, "Received opcode=%d", data->op_code);
|
||||
if (data->op_code == 0x08 && data->data_len == 2)
|
||||
{
|
||||
ESP_LOGW(wsTAG, "Received closed message with code=%d", 256 * data->data_ptr[0] + data->data_ptr[1]);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Filter for Text-Messages
|
||||
if (data->op_code == 0x01)
|
||||
{
|
||||
ESP_LOGV(wsTAG, "Total payload length=%d, data_len=%d, current payload offset=%d", data->payload_len, data->data_len, data->payload_offset);
|
||||
ESP_LOGV(wsTAG, "Received=%.*s", data->data_len, (char *)data->data_ptr);
|
||||
|
||||
// Copy the characters from data->data_ptr to c-string
|
||||
char payload[_bufferSize];
|
||||
strncpy(payload, (char *)data->data_ptr, data->data_len);
|
||||
payload[data->data_len] = '\0';
|
||||
ESP_LOGV(wsTAG, "Payload=%s", payload);
|
||||
|
||||
// DynamicJsonDocument jsonDocument = DynamicJsonDocument(_bufferSize);
|
||||
// DeserializationError error = deserializeJson(jsonDocument, payload);
|
||||
|
||||
// ESP_LOGE(wsTAG, "deserializeJson() status: %s", error.c_str());
|
||||
|
||||
/* if (!error && jsonDocument.is<JsonObject>())
|
||||
{
|
||||
JsonObject jsonObject = jsonDocument.as<JsonObject>();
|
||||
_statefulService->update(
|
||||
jsonObject, _stateUpdater, WEB_SOCKET_CLIENT_ORIGIN);
|
||||
ESP_LOGV(wsTAG, "Updated StatefulService");
|
||||
} */
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
static void loopImpl(void *_this) { static_cast<WebSocketClient *>(_this)->loop(); }
|
||||
|
||||
void loop()
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
// Workaround for a bug in the WEBSOCKET_EVENT_CONNECTED event handler causing a kernel panic. Revisit with ESP-IDF v5.1
|
||||
if (wsClientConnected() && freshConnected)
|
||||
{
|
||||
ESP_LOGI(wsTAG, "Websocket Client Connected");
|
||||
transmitData(WEB_SOCKET_ORIGIN);
|
||||
freshConnected = false;
|
||||
}
|
||||
|
||||
vTaskDelay(pdMS_TO_TICKS(150));
|
||||
}
|
||||
}
|
||||
|
||||
void transmitData(const String &originId)
|
||||
{
|
||||
DynamicJsonDocument jsonDocument = DynamicJsonDocument(_bufferSize);
|
||||
JsonObject payload = jsonDocument.to<JsonObject>();
|
||||
_statefulService->read(payload, _stateReader);
|
||||
String json_string;
|
||||
serializeJson(jsonDocument, json_string);
|
||||
esp_websocket_client_send_text(client, json_string.c_str(), json_string.length() + 1, portMAX_DELAY);
|
||||
ESP_LOGI(wsTAG, "Websocket Client Transmission: %s", json_string.c_str());
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,163 @@
|
||||
#ifndef WebSocketServer_h
|
||||
#define WebSocketServer_h
|
||||
|
||||
/**
|
||||
* ESP32 SvelteKit
|
||||
*
|
||||
* A simple, secure and extensible framework for IoT projects for ESP32 platforms
|
||||
* with responsive Sveltekit front-end built with TailwindCSS and DaisyUI.
|
||||
* https://github.com/theelims/ESP32-sveltekit
|
||||
*
|
||||
* Copyright (C) 2018 - 2023 rjwats
|
||||
* Copyright (C) 2023 theelims
|
||||
*
|
||||
* All Rights Reserved. This software may be modified and distributed under
|
||||
* the terms of the LGPL v3 license. See the LICENSE file for details.
|
||||
**/
|
||||
|
||||
#include <StatefulService.h>
|
||||
#include <PsychicHttp.h>
|
||||
#include <SecurityManager.h>
|
||||
|
||||
#define WEB_SOCKET_CLIENT_ID_MSG_SIZE 128
|
||||
|
||||
#define WEB_SOCKET_ORIGIN "wsserver"
|
||||
#define WEB_SOCKET_ORIGIN_CLIENT_ID_PREFIX "wsserver:"
|
||||
|
||||
template <class T>
|
||||
class WebSocketServer
|
||||
{
|
||||
public:
|
||||
WebSocketServer(JsonStateReader<T> stateReader,
|
||||
JsonStateUpdater<T> stateUpdater,
|
||||
StatefulService<T> *statefulService,
|
||||
PsychicHttpServer *server,
|
||||
const char *webSocketPath,
|
||||
SecurityManager *securityManager,
|
||||
AuthenticationPredicate authenticationPredicate = AuthenticationPredicates::IS_ADMIN,
|
||||
size_t bufferSize = DEFAULT_BUFFER_SIZE) : _stateReader(stateReader),
|
||||
_stateUpdater(stateUpdater),
|
||||
_statefulService(statefulService),
|
||||
_server(server),
|
||||
_bufferSize(bufferSize),
|
||||
_webSocketPath(webSocketPath),
|
||||
_authenticationPredicate(authenticationPredicate),
|
||||
_securityManager(securityManager)
|
||||
{
|
||||
_statefulService->addUpdateHandler(
|
||||
[&](const String &originId)
|
||||
{ transmitData(nullptr, originId); },
|
||||
false);
|
||||
}
|
||||
|
||||
void begin()
|
||||
{
|
||||
_webSocket.setFilter(_securityManager->filterRequest(_authenticationPredicate));
|
||||
_webSocket.onOpen(std::bind(&WebSocketServer::onWSOpen,
|
||||
this,
|
||||
std::placeholders::_1));
|
||||
_webSocket.onClose(std::bind(&WebSocketServer::onWSClose,
|
||||
this,
|
||||
std::placeholders::_1));
|
||||
_webSocket.onFrame(std::bind(&WebSocketServer::onWSFrame,
|
||||
this,
|
||||
std::placeholders::_1,
|
||||
std::placeholders::_2));
|
||||
_server->on(_webSocketPath.c_str(), &_webSocket);
|
||||
|
||||
ESP_LOGV("WebSocketServer", "Registered WebSocket handler: %s", _webSocketPath.c_str());
|
||||
}
|
||||
|
||||
void onWSOpen(PsychicWebSocketClient *client)
|
||||
{
|
||||
|
||||
// when a client connects, we transmit it's id and the current payload
|
||||
transmitId(client);
|
||||
transmitData(client, WEB_SOCKET_ORIGIN);
|
||||
ESP_LOGI("WebSocketServer", "ws[%s][%u] connect", client->remoteIP().toString().c_str(), client->socket());
|
||||
}
|
||||
|
||||
void onWSClose(PsychicWebSocketClient *client)
|
||||
{
|
||||
ESP_LOGI("WebSocketServer", "ws[%s][%u] disconnect", client->remoteIP().toString().c_str(), client->socket());
|
||||
}
|
||||
|
||||
esp_err_t onWSFrame(PsychicWebSocketRequest *request, httpd_ws_frame *frame)
|
||||
{
|
||||
ESP_LOGV("WebSocketServer", "ws[%s][%u] opcode[%d]", request->client()->remoteIP().toString().c_str(), request->client()->socket(), frame->type);
|
||||
|
||||
if (frame->type == HTTPD_WS_TYPE_TEXT)
|
||||
{
|
||||
ESP_LOGV("WebSocketServer", "ws[%s][%u] request: %s", request->client()->remoteIP().toString().c_str(), request->client()->socket(), (char *)frame->payload);
|
||||
|
||||
DynamicJsonDocument jsonDocument = DynamicJsonDocument(_bufferSize);
|
||||
DeserializationError error = deserializeJson(jsonDocument, (char *)frame->payload, frame->len);
|
||||
|
||||
if (!error && jsonDocument.is<JsonObject>())
|
||||
{
|
||||
JsonObject jsonObject = jsonDocument.as<JsonObject>();
|
||||
_statefulService->update(jsonObject, _stateUpdater, clientId(request->client()));
|
||||
return ESP_OK;
|
||||
}
|
||||
}
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
String clientId(PsychicWebSocketClient *client)
|
||||
{
|
||||
return WEB_SOCKET_ORIGIN_CLIENT_ID_PREFIX + String(client->socket());
|
||||
}
|
||||
|
||||
private:
|
||||
JsonStateReader<T> _stateReader;
|
||||
JsonStateUpdater<T> _stateUpdater;
|
||||
StatefulService<T> *_statefulService;
|
||||
AuthenticationPredicate _authenticationPredicate;
|
||||
SecurityManager *_securityManager;
|
||||
PsychicHttpServer *_server;
|
||||
PsychicWebSocketHandler _webSocket;
|
||||
String _webSocketPath;
|
||||
size_t _bufferSize;
|
||||
|
||||
void transmitId(PsychicWebSocketClient *client)
|
||||
{
|
||||
DynamicJsonDocument jsonDocument = DynamicJsonDocument(WEB_SOCKET_CLIENT_ID_MSG_SIZE);
|
||||
JsonObject root = jsonDocument.to<JsonObject>();
|
||||
root["type"] = "id";
|
||||
root["id"] = clientId(client);
|
||||
|
||||
// serialize the json to a string
|
||||
String buffer;
|
||||
serializeJson(jsonDocument, buffer);
|
||||
client->sendMessage(buffer.c_str());
|
||||
}
|
||||
|
||||
/**
|
||||
* Broadcasts the payload to the destination, if provided. Otherwise broadcasts to all clients except the origin, if
|
||||
* specified.
|
||||
*
|
||||
* Original implementation sent clients their own IDs so they could ignore updates they initiated. This approach
|
||||
* simplifies the client and the server implementation but may not be sufficient for all use-cases.
|
||||
*/
|
||||
void transmitData(PsychicWebSocketClient *client, const String &originId)
|
||||
{
|
||||
DynamicJsonDocument jsonDocument = DynamicJsonDocument(_bufferSize);
|
||||
JsonObject root = jsonDocument.to<JsonObject>();
|
||||
String buffer;
|
||||
|
||||
_statefulService->read(root, _stateReader);
|
||||
|
||||
// serialize the json to a string
|
||||
serializeJson(jsonDocument, buffer);
|
||||
if (client)
|
||||
{
|
||||
client->sendMessage(buffer.c_str());
|
||||
}
|
||||
else
|
||||
{
|
||||
_webSocket.sendAll(buffer.c_str());
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,78 @@
|
||||
/**
|
||||
* ESP32 SvelteKit
|
||||
*
|
||||
* A simple, secure and extensible framework for IoT projects for ESP32 platforms
|
||||
* with responsive Sveltekit front-end built with TailwindCSS and DaisyUI.
|
||||
* https://github.com/theelims/ESP32-sveltekit
|
||||
*
|
||||
* Copyright (C) 2018 - 2023 rjwats
|
||||
* Copyright (C) 2023 theelims
|
||||
*
|
||||
* All Rights Reserved. This software may be modified and distributed under
|
||||
* the terms of the LGPL v3 license. See the LICENSE file for details.
|
||||
**/
|
||||
|
||||
#include <WiFiScanner.h>
|
||||
|
||||
WiFiScanner::WiFiScanner(PsychicHttpServer *server,
|
||||
SecurityManager *securityManager) : _server(server),
|
||||
_securityManager(securityManager)
|
||||
{
|
||||
}
|
||||
|
||||
void WiFiScanner::begin()
|
||||
{
|
||||
_server->on(SCAN_NETWORKS_SERVICE_PATH,
|
||||
HTTP_GET,
|
||||
_securityManager->wrapRequest(std::bind(&WiFiScanner::scanNetworks, this, std::placeholders::_1),
|
||||
AuthenticationPredicates::IS_ADMIN));
|
||||
|
||||
ESP_LOGV("WiFiScanner", "Registered GET endpoint: %s", SCAN_NETWORKS_SERVICE_PATH);
|
||||
|
||||
_server->on(LIST_NETWORKS_SERVICE_PATH,
|
||||
HTTP_GET,
|
||||
_securityManager->wrapRequest(std::bind(&WiFiScanner::listNetworks, this, std::placeholders::_1),
|
||||
AuthenticationPredicates::IS_ADMIN));
|
||||
|
||||
ESP_LOGV("WiFiScanner", "Registered GET endpoint: %s", LIST_NETWORKS_SERVICE_PATH);
|
||||
}
|
||||
|
||||
esp_err_t WiFiScanner::scanNetworks(PsychicRequest *request)
|
||||
{
|
||||
if (WiFi.scanComplete() != -1)
|
||||
{
|
||||
WiFi.scanDelete();
|
||||
WiFi.scanNetworks(true);
|
||||
}
|
||||
return request->reply(202);
|
||||
}
|
||||
|
||||
esp_err_t WiFiScanner::listNetworks(PsychicRequest *request)
|
||||
{
|
||||
int numNetworks = WiFi.scanComplete();
|
||||
if (numNetworks > -1)
|
||||
{
|
||||
PsychicJsonResponse response = PsychicJsonResponse(request, false, MAX_WIFI_SCANNER_SIZE);
|
||||
JsonObject root = response.getRoot();
|
||||
JsonArray networks = root.createNestedArray("networks");
|
||||
for (int i = 0; i < numNetworks; i++)
|
||||
{
|
||||
JsonObject network = networks.createNestedObject();
|
||||
network["rssi"] = WiFi.RSSI(i);
|
||||
network["ssid"] = WiFi.SSID(i);
|
||||
network["bssid"] = WiFi.BSSIDstr(i);
|
||||
network["channel"] = WiFi.channel(i);
|
||||
network["encryption_type"] = (uint8_t)WiFi.encryptionType(i);
|
||||
}
|
||||
|
||||
return response.send();
|
||||
}
|
||||
else if (numNetworks == -1)
|
||||
{
|
||||
return request->reply(202);
|
||||
}
|
||||
else
|
||||
{
|
||||
return scanNetworks(request);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
#ifndef WiFiScanner_h
|
||||
#define WiFiScanner_h
|
||||
|
||||
/**
|
||||
* ESP32 SvelteKit
|
||||
*
|
||||
* A simple, secure and extensible framework for IoT projects for ESP32 platforms
|
||||
* with responsive Sveltekit front-end built with TailwindCSS and DaisyUI.
|
||||
* https://github.com/theelims/ESP32-sveltekit
|
||||
*
|
||||
* Copyright (C) 2018 - 2023 rjwats
|
||||
* Copyright (C) 2023 theelims
|
||||
*
|
||||
* All Rights Reserved. This software may be modified and distributed under
|
||||
* the terms of the LGPL v3 license. See the LICENSE file for details.
|
||||
**/
|
||||
|
||||
#include <WiFi.h>
|
||||
|
||||
#include <ArduinoJson.h>
|
||||
#include <PsychicHttp.h>
|
||||
#include <SecurityManager.h>
|
||||
|
||||
#define SCAN_NETWORKS_SERVICE_PATH "/rest/scanNetworks"
|
||||
#define LIST_NETWORKS_SERVICE_PATH "/rest/listNetworks"
|
||||
|
||||
#define MAX_WIFI_SCANNER_SIZE 1024
|
||||
|
||||
class WiFiScanner
|
||||
{
|
||||
public:
|
||||
WiFiScanner(PsychicHttpServer *server, SecurityManager *securityManager);
|
||||
|
||||
void begin();
|
||||
|
||||
private:
|
||||
PsychicHttpServer *_server;
|
||||
SecurityManager *_securityManager;
|
||||
|
||||
esp_err_t scanNetworks(PsychicRequest *request);
|
||||
esp_err_t listNetworks(PsychicRequest *request);
|
||||
};
|
||||
|
||||
#endif // end WiFiScanner_h
|
||||
@@ -0,0 +1,233 @@
|
||||
/**
|
||||
* ESP32 SvelteKit
|
||||
*
|
||||
* A simple, secure and extensible framework for IoT projects for ESP32 platforms
|
||||
* with responsive Sveltekit front-end built with TailwindCSS and DaisyUI.
|
||||
* https://github.com/theelims/ESP32-sveltekit
|
||||
*
|
||||
* Copyright (C) 2018 - 2023 rjwats
|
||||
* Copyright (C) 2023 theelims
|
||||
*
|
||||
* All Rights Reserved. This software may be modified and distributed under
|
||||
* the terms of the LGPL v3 license. See the LICENSE file for details.
|
||||
**/
|
||||
|
||||
#include <WiFiSettingsService.h>
|
||||
|
||||
WiFiSettingsService::WiFiSettingsService(PsychicHttpServer *server,
|
||||
FS *fs,
|
||||
SecurityManager *securityManager,
|
||||
EventSocket *socket) : _server(server),
|
||||
_securityManager(securityManager),
|
||||
_httpEndpoint(WiFiSettings::read, WiFiSettings::update, this, server, WIFI_SETTINGS_SERVICE_PATH, securityManager,
|
||||
AuthenticationPredicates::IS_ADMIN, WIFI_SETTINGS_BUFFER_SIZE),
|
||||
_fsPersistence(WiFiSettings::read, WiFiSettings::update, this, fs, WIFI_SETTINGS_FILE), _lastConnectionAttempt(0),
|
||||
_socket(socket)
|
||||
{
|
||||
addUpdateHandler([&](const String &originId)
|
||||
{ reconfigureWiFiConnection(); },
|
||||
false);
|
||||
}
|
||||
|
||||
void WiFiSettingsService::initWiFi()
|
||||
{
|
||||
WiFi.mode(WIFI_MODE_STA); // this is the default.
|
||||
|
||||
// Disable WiFi config persistance and auto reconnect
|
||||
WiFi.persistent(false);
|
||||
WiFi.setAutoReconnect(false);
|
||||
|
||||
WiFi.onEvent(
|
||||
std::bind(&WiFiSettingsService::onStationModeDisconnected, this, std::placeholders::_1, std::placeholders::_2),
|
||||
WiFiEvent_t::ARDUINO_EVENT_WIFI_STA_DISCONNECTED);
|
||||
WiFi.onEvent(std::bind(&WiFiSettingsService::onStationModeStop, this, std::placeholders::_1, std::placeholders::_2),
|
||||
WiFiEvent_t::ARDUINO_EVENT_WIFI_STA_STOP);
|
||||
|
||||
_fsPersistence.readFromFS();
|
||||
reconfigureWiFiConnection();
|
||||
}
|
||||
|
||||
void WiFiSettingsService::begin()
|
||||
{
|
||||
_socket->registerEvent(EVENT_RSSI);
|
||||
|
||||
_httpEndpoint.begin();
|
||||
}
|
||||
|
||||
void WiFiSettingsService::reconfigureWiFiConnection()
|
||||
{
|
||||
// reset last connection attempt to force loop to reconnect immediately
|
||||
_lastConnectionAttempt = 0;
|
||||
|
||||
// disconnect and de-configure wifi
|
||||
if (WiFi.disconnect(true))
|
||||
{
|
||||
_stopping = true;
|
||||
}
|
||||
}
|
||||
|
||||
void WiFiSettingsService::loop()
|
||||
{
|
||||
unsigned long currentMillis = millis();
|
||||
if (!_lastConnectionAttempt || (unsigned long)(currentMillis - _lastConnectionAttempt) >= WIFI_RECONNECTION_DELAY)
|
||||
{
|
||||
_lastConnectionAttempt = currentMillis;
|
||||
manageSTA();
|
||||
}
|
||||
|
||||
if (!_lastRssiUpdate || (unsigned long)(currentMillis - _lastRssiUpdate) >= RSSI_EVENT_DELAY)
|
||||
{
|
||||
_lastRssiUpdate = currentMillis;
|
||||
updateRSSI();
|
||||
}
|
||||
}
|
||||
|
||||
String WiFiSettingsService::getHostname()
|
||||
{
|
||||
return _state.hostname;
|
||||
}
|
||||
|
||||
void WiFiSettingsService::manageSTA()
|
||||
{
|
||||
// Abort if already connected, or if we have no SSID
|
||||
if (WiFi.isConnected() || _state.wifiSettings.empty())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Connect or reconnect as required
|
||||
if ((WiFi.getMode() & WIFI_STA) == 0)
|
||||
{
|
||||
#ifdef SERIAL_INFO
|
||||
Serial.println("Connecting to WiFi...");
|
||||
#endif
|
||||
connectToWiFi();
|
||||
}
|
||||
}
|
||||
|
||||
void WiFiSettingsService::connectToWiFi()
|
||||
{
|
||||
// reset availability flag for all stored networks
|
||||
for (auto &network : _state.wifiSettings)
|
||||
{
|
||||
network.available = false;
|
||||
}
|
||||
|
||||
// scanning for available networks
|
||||
int scanResult = WiFi.scanNetworks();
|
||||
if (scanResult == WIFI_SCAN_FAILED)
|
||||
{
|
||||
ESP_LOGE("WiFiSettingsService", "WiFi scan failed.");
|
||||
}
|
||||
else if (scanResult == 0)
|
||||
{
|
||||
ESP_LOGW("WiFiSettingsService", "No networks found.");
|
||||
}
|
||||
else
|
||||
{
|
||||
ESP_LOGI("WiFiSettingsService", "%d networks found.", scanResult);
|
||||
|
||||
// find the best network to connect
|
||||
wifi_settings_t *bestNetwork = NULL;
|
||||
int bestNetworkDb = FACTORY_WIFI_RSSI_THRESHOLD;
|
||||
|
||||
for (int i = 0; i < scanResult; ++i)
|
||||
{
|
||||
String ssid_scan;
|
||||
int32_t rssi_scan;
|
||||
uint8_t sec_scan;
|
||||
uint8_t *BSSID_scan;
|
||||
int32_t chan_scan;
|
||||
|
||||
WiFi.getNetworkInfo(i, ssid_scan, sec_scan, rssi_scan, BSSID_scan, chan_scan);
|
||||
ESP_LOGV("WiFiSettingsService", "SSID: %s, RSSI: %d dbm", ssid_scan.c_str(), rssi_scan);
|
||||
|
||||
for (auto &network : _state.wifiSettings)
|
||||
{
|
||||
if (ssid_scan == network.ssid)
|
||||
{ // SSID match
|
||||
if (rssi_scan > bestNetworkDb)
|
||||
{ // best network
|
||||
bestNetworkDb = rssi_scan;
|
||||
bestNetwork = &network;
|
||||
network.available = true;
|
||||
}
|
||||
else if (rssi_scan >= FACTORY_WIFI_RSSI_THRESHOLD)
|
||||
{ // available network
|
||||
network.available = true;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// if configured to prioritize signal strength, use the best network else use the first available network
|
||||
if (_state.priorityBySignalStrength == false)
|
||||
{
|
||||
for (auto &network : _state.wifiSettings)
|
||||
{
|
||||
if (network.available == true)
|
||||
{
|
||||
ESP_LOGI("WiFiSettingsService", "Connecting to first available network: %s", network.ssid.c_str());
|
||||
configureNetwork(network);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (_state.priorityBySignalStrength == true && bestNetwork)
|
||||
{
|
||||
ESP_LOGI("WiFiSettingsService", "Connecting to strongest network: %s", bestNetwork->ssid.c_str());
|
||||
configureNetwork(*bestNetwork);
|
||||
WiFi.begin(bestNetwork->ssid.c_str(), bestNetwork->password.c_str());
|
||||
}
|
||||
else // no suitable network to connect
|
||||
{
|
||||
ESP_LOGI("WiFiSettingsService", "No known networks found.");
|
||||
}
|
||||
|
||||
// delete scan results
|
||||
WiFi.scanDelete();
|
||||
}
|
||||
}
|
||||
|
||||
void WiFiSettingsService::configureNetwork(wifi_settings_t &network)
|
||||
{
|
||||
if (network.staticIPConfig)
|
||||
{
|
||||
// configure for static IP
|
||||
WiFi.config(network.localIP, network.gatewayIP, network.subnetMask, network.dnsIP1, network.dnsIP2);
|
||||
}
|
||||
else
|
||||
{
|
||||
// configure for DHCP
|
||||
WiFi.config(INADDR_NONE, INADDR_NONE, INADDR_NONE);
|
||||
}
|
||||
WiFi.setHostname(_state.hostname.c_str());
|
||||
|
||||
// attempt to connect to the network
|
||||
WiFi.begin(network.ssid.c_str(), network.password.c_str());
|
||||
|
||||
#if CONFIG_IDF_TARGET_ESP32C3
|
||||
WiFi.setTxPower(WIFI_POWER_8_5dBm); // https://www.wemos.cc/en/latest/c3/c3_mini_1_0_0.html#about-wifi
|
||||
#endif
|
||||
}
|
||||
|
||||
void WiFiSettingsService::updateRSSI()
|
||||
{
|
||||
char buffer[16];
|
||||
snprintf(buffer, sizeof(buffer), WiFi.isConnected() ? "%d" : "disconnected", WiFi.RSSI());
|
||||
_socket->emit(EVENT_RSSI, buffer);
|
||||
}
|
||||
|
||||
void WiFiSettingsService::onStationModeDisconnected(WiFiEvent_t event, WiFiEventInfo_t info)
|
||||
{
|
||||
WiFi.disconnect(true);
|
||||
}
|
||||
void WiFiSettingsService::onStationModeStop(WiFiEvent_t event, WiFiEventInfo_t info)
|
||||
{
|
||||
if (_stopping)
|
||||
{
|
||||
_lastConnectionAttempt = 0;
|
||||
_stopping = false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,233 @@
|
||||
#ifndef WiFiSettingsService_h
|
||||
#define WiFiSettingsService_h
|
||||
|
||||
/**
|
||||
* ESP32 SvelteKit
|
||||
*
|
||||
* A simple, secure and extensible framework for IoT projects for ESP32 platforms
|
||||
* with responsive Sveltekit front-end built with TailwindCSS and DaisyUI.
|
||||
* https://github.com/theelims/ESP32-sveltekit
|
||||
*
|
||||
* Copyright (C) 2018 - 2023 rjwats
|
||||
* Copyright (C) 2023 theelims
|
||||
*
|
||||
* All Rights Reserved. This software may be modified and distributed under
|
||||
* the terms of the LGPL v3 license. See the LICENSE file for details.
|
||||
**/
|
||||
|
||||
#include <WiFi.h>
|
||||
#include <WiFiMulti.h>
|
||||
#include <SettingValue.h>
|
||||
#include <StatefulService.h>
|
||||
#include <EventSocket.h>
|
||||
#include <FSPersistence.h>
|
||||
#include <HttpEndpoint.h>
|
||||
#include <JsonUtils.h>
|
||||
#include <SecurityManager.h>
|
||||
#include <PsychicHttp.h>
|
||||
#include <vector>
|
||||
|
||||
#ifndef FACTORY_WIFI_SSID
|
||||
#define FACTORY_WIFI_SSID ""
|
||||
#endif
|
||||
|
||||
#ifndef FACTORY_WIFI_PASSWORD
|
||||
#define FACTORY_WIFI_PASSWORD ""
|
||||
#endif
|
||||
|
||||
#ifndef FACTORY_WIFI_HOSTNAME
|
||||
#define FACTORY_WIFI_HOSTNAME "#{platform}-#{unique_id}"
|
||||
#endif
|
||||
|
||||
#ifndef FACTORY_WIFI_RSSI_THRESHOLD
|
||||
#define FACTORY_WIFI_RSSI_THRESHOLD -80
|
||||
#endif
|
||||
|
||||
#define WIFI_SETTINGS_FILE "/config/wifiSettings.json"
|
||||
#define WIFI_SETTINGS_SERVICE_PATH "/rest/wifiSettings"
|
||||
|
||||
#define WIFI_RECONNECTION_DELAY 1000 * 30
|
||||
#define RSSI_EVENT_DELAY 200
|
||||
|
||||
#define WIFI_SETTINGS_BUFFER_SIZE 2048
|
||||
|
||||
#define EVENT_RSSI "rssi"
|
||||
|
||||
// Struct defining the wifi settings
|
||||
typedef struct
|
||||
{
|
||||
String ssid;
|
||||
String password;
|
||||
bool staticIPConfig;
|
||||
IPAddress localIP;
|
||||
IPAddress gatewayIP;
|
||||
IPAddress subnetMask;
|
||||
IPAddress dnsIP1;
|
||||
IPAddress dnsIP2;
|
||||
bool available;
|
||||
} wifi_settings_t;
|
||||
|
||||
class WiFiSettings
|
||||
{
|
||||
public:
|
||||
// core wifi configuration
|
||||
String hostname;
|
||||
bool priorityBySignalStrength;
|
||||
std::vector<wifi_settings_t> wifiSettings;
|
||||
|
||||
static void read(WiFiSettings &settings, JsonObject &root)
|
||||
{
|
||||
root["hostname"] = settings.hostname;
|
||||
root["priority_RSSI"] = settings.priorityBySignalStrength;
|
||||
|
||||
// create JSON array from root
|
||||
JsonArray wifiNetworks = root.createNestedArray("wifi_networks");
|
||||
|
||||
// iterate over the wifiSettings
|
||||
for (auto &wifi : settings.wifiSettings)
|
||||
{
|
||||
// create JSON object for each wifi network
|
||||
JsonObject wifiNetwork = wifiNetworks.createNestedObject();
|
||||
|
||||
// add the ssid and password to the JSON object
|
||||
wifiNetwork["ssid"] = wifi.ssid;
|
||||
wifiNetwork["password"] = wifi.password;
|
||||
wifiNetwork["static_ip_config"] = wifi.staticIPConfig;
|
||||
|
||||
// extended settings
|
||||
JsonUtils::writeIP(root, "local_ip", wifi.localIP);
|
||||
JsonUtils::writeIP(root, "gateway_ip", wifi.gatewayIP);
|
||||
JsonUtils::writeIP(root, "subnet_mask", wifi.subnetMask);
|
||||
JsonUtils::writeIP(root, "dns_ip_1", wifi.dnsIP1);
|
||||
JsonUtils::writeIP(root, "dns_ip_2", wifi.dnsIP2);
|
||||
}
|
||||
|
||||
ESP_LOGV("WiFiSettings", "WiFi Settings read");
|
||||
}
|
||||
|
||||
static StateUpdateResult update(JsonObject &root, WiFiSettings &settings)
|
||||
{
|
||||
settings.hostname = root["hostname"] | SettingValue::format(FACTORY_WIFI_HOSTNAME);
|
||||
settings.priorityBySignalStrength = root["priority_RSSI"] | true;
|
||||
|
||||
settings.wifiSettings.clear();
|
||||
|
||||
// create JSON array from root
|
||||
JsonArray wifiNetworks = root["wifi_networks"];
|
||||
if (root["wifi_networks"].is<JsonArray>())
|
||||
{
|
||||
// iterate over the wifiSettings
|
||||
int i = 0;
|
||||
for (auto wifiNetwork : wifiNetworks)
|
||||
{
|
||||
// max 5 wifi networks
|
||||
if (i++ >= 5)
|
||||
{
|
||||
ESP_LOGE("WiFiSettings", "Too many wifi networks");
|
||||
break;
|
||||
}
|
||||
|
||||
// create JSON object for each wifi network
|
||||
JsonObject wifi = wifiNetwork.as<JsonObject>();
|
||||
|
||||
// Check if SSID length is between 1 and 31 characters and password between 0 and 64 characters
|
||||
if (wifi["ssid"].as<String>().length() < 1 || wifi["ssid"].as<String>().length() > 31 || wifi["password"].as<String>().length() > 64)
|
||||
{
|
||||
ESP_LOGE("WiFiSettings", "SSID or password length is invalid");
|
||||
}
|
||||
else
|
||||
{
|
||||
// add the ssid and password to the JSON object
|
||||
wifi_settings_t wifiSettings;
|
||||
|
||||
wifiSettings.ssid = wifi["ssid"].as<String>();
|
||||
wifiSettings.password = wifi["password"].as<String>();
|
||||
wifiSettings.staticIPConfig = wifi["static_ip_config"];
|
||||
|
||||
// extended settings
|
||||
JsonUtils::readIP(wifi, "local_ip", wifiSettings.localIP);
|
||||
JsonUtils::readIP(wifi, "gateway_ip", wifiSettings.gatewayIP);
|
||||
JsonUtils::readIP(wifi, "subnet_mask", wifiSettings.subnetMask);
|
||||
JsonUtils::readIP(wifi, "dns_ip_1", wifiSettings.dnsIP1);
|
||||
JsonUtils::readIP(wifi, "dns_ip_2", wifiSettings.dnsIP2);
|
||||
|
||||
// Swap around the dns servers if 2 is populated but 1 is not
|
||||
if (IPUtils::isNotSet(wifiSettings.dnsIP1) && IPUtils::isSet(wifiSettings.dnsIP2))
|
||||
{
|
||||
wifiSettings.dnsIP1 = wifiSettings.dnsIP2;
|
||||
wifiSettings.dnsIP2 = INADDR_NONE;
|
||||
}
|
||||
|
||||
// Turning off static ip config if we don't meet the minimum requirements
|
||||
// of ipAddress, gateway and subnet. This may change to static ip only
|
||||
// as sensible defaults can be assumed for gateway and subnet
|
||||
if (wifiSettings.staticIPConfig && (IPUtils::isNotSet(wifiSettings.localIP) || IPUtils::isNotSet(wifiSettings.gatewayIP) ||
|
||||
IPUtils::isNotSet(wifiSettings.subnetMask)))
|
||||
{
|
||||
wifiSettings.staticIPConfig = false;
|
||||
}
|
||||
|
||||
// reset scan result
|
||||
wifiSettings.available = false;
|
||||
settings.wifiSettings.push_back(wifiSettings);
|
||||
|
||||
// increment the wifi network index
|
||||
i++;
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// populate with factory defaults if they are present
|
||||
if (String(FACTORY_WIFI_SSID).length() > 0)
|
||||
{
|
||||
settings.wifiSettings.push_back(wifi_settings_t{
|
||||
.ssid = FACTORY_WIFI_SSID,
|
||||
.password = FACTORY_WIFI_PASSWORD,
|
||||
.staticIPConfig = false,
|
||||
.localIP = INADDR_NONE,
|
||||
.gatewayIP = INADDR_NONE,
|
||||
.subnetMask = INADDR_NONE,
|
||||
.dnsIP1 = INADDR_NONE,
|
||||
.dnsIP2 = INADDR_NONE,
|
||||
.available = false,
|
||||
});
|
||||
}
|
||||
}
|
||||
ESP_LOGV("WiFiSettings", "WiFi Settings updated");
|
||||
|
||||
return StateUpdateResult::CHANGED;
|
||||
};
|
||||
};
|
||||
|
||||
class WiFiSettingsService : public StatefulService<WiFiSettings>
|
||||
{
|
||||
public:
|
||||
WiFiSettingsService(PsychicHttpServer *server, FS *fs, SecurityManager *securityManager, EventSocket *socket);
|
||||
|
||||
void initWiFi();
|
||||
void begin();
|
||||
void loop();
|
||||
String getHostname();
|
||||
|
||||
private:
|
||||
PsychicHttpServer *_server;
|
||||
SecurityManager *_securityManager;
|
||||
HttpEndpoint<WiFiSettings> _httpEndpoint;
|
||||
FSPersistence<WiFiSettings> _fsPersistence;
|
||||
EventSocket *_socket;
|
||||
unsigned long _lastConnectionAttempt;
|
||||
unsigned long _lastRssiUpdate;
|
||||
|
||||
bool _stopping;
|
||||
void onStationModeDisconnected(WiFiEvent_t event, WiFiEventInfo_t info);
|
||||
void onStationModeStop(WiFiEvent_t event, WiFiEventInfo_t info);
|
||||
|
||||
void reconfigureWiFiConnection();
|
||||
void manageSTA();
|
||||
void connectToWiFi();
|
||||
void configureNetwork(wifi_settings_t &network);
|
||||
void updateRSSI();
|
||||
};
|
||||
|
||||
#endif // end WiFiSettingsService_h
|
||||
@@ -0,0 +1,93 @@
|
||||
/**
|
||||
* ESP32 SvelteKit
|
||||
*
|
||||
* A simple, secure and extensible framework for IoT projects for ESP32 platforms
|
||||
* with responsive Sveltekit front-end built with TailwindCSS and DaisyUI.
|
||||
* https://github.com/theelims/ESP32-sveltekit
|
||||
*
|
||||
* Copyright (C) 2018 - 2023 rjwats
|
||||
* Copyright (C) 2023 theelims
|
||||
*
|
||||
* All Rights Reserved. This software may be modified and distributed under
|
||||
* the terms of the LGPL v3 license. See the LICENSE file for details.
|
||||
**/
|
||||
|
||||
#include <WiFiStatus.h>
|
||||
|
||||
WiFiStatus::WiFiStatus(PsychicHttpServer *server,
|
||||
SecurityManager *securityManager) : _server(server),
|
||||
_securityManager(securityManager)
|
||||
{
|
||||
}
|
||||
|
||||
void WiFiStatus::begin()
|
||||
{
|
||||
_server->on(WIFI_STATUS_SERVICE_PATH,
|
||||
HTTP_GET,
|
||||
_securityManager->wrapRequest(std::bind(&WiFiStatus::wifiStatus, this, std::placeholders::_1),
|
||||
AuthenticationPredicates::IS_AUTHENTICATED));
|
||||
|
||||
ESP_LOGV("WiFiStatus", "Registered GET endpoint: %s", WIFI_STATUS_SERVICE_PATH);
|
||||
|
||||
WiFi.onEvent(onStationModeConnected, WiFiEvent_t::ARDUINO_EVENT_WIFI_STA_CONNECTED);
|
||||
WiFi.onEvent(onStationModeDisconnected, WiFiEvent_t::ARDUINO_EVENT_WIFI_STA_DISCONNECTED);
|
||||
WiFi.onEvent(onStationModeGotIP, WiFiEvent_t::ARDUINO_EVENT_WIFI_STA_GOT_IP);
|
||||
}
|
||||
|
||||
void WiFiStatus::onStationModeConnected(WiFiEvent_t event, WiFiEventInfo_t info)
|
||||
{
|
||||
ESP_LOGI("WiFiStatus", "WiFi Connected.");
|
||||
|
||||
#ifdef SERIAL_INFO
|
||||
Serial.println("WiFi Connected.");
|
||||
#endif
|
||||
}
|
||||
|
||||
void WiFiStatus::onStationModeDisconnected(WiFiEvent_t event, WiFiEventInfo_t info)
|
||||
{
|
||||
ESP_LOGI("WiFiStatus", "WiFi Disconnected. Reason code=%d", info.wifi_sta_disconnected.reason);
|
||||
|
||||
#ifdef SERIAL_INFO
|
||||
Serial.print("WiFi Disconnected. Reason code=");
|
||||
Serial.println(info.wifi_sta_disconnected.reason);
|
||||
#endif
|
||||
}
|
||||
|
||||
void WiFiStatus::onStationModeGotIP(WiFiEvent_t event, WiFiEventInfo_t info)
|
||||
{
|
||||
ESP_LOGI("WiFiStatus", "WiFi Got IP. localIP=%s, hostName=%s", WiFi.localIP().toString().c_str(), WiFi.getHostname());
|
||||
#ifdef SERIAL_INFO
|
||||
Serial.printf("WiFi Got IP. localIP=%s, hostName=%s\r\n", WiFi.localIP().toString().c_str(), WiFi.getHostname());
|
||||
#endif
|
||||
}
|
||||
|
||||
esp_err_t WiFiStatus::wifiStatus(PsychicRequest *request)
|
||||
{
|
||||
PsychicJsonResponse response = PsychicJsonResponse(request, false, MAX_WIFI_STATUS_SIZE);
|
||||
JsonObject root = response.getRoot();
|
||||
wl_status_t status = WiFi.status();
|
||||
root["status"] = (uint8_t)status;
|
||||
if (status == WL_CONNECTED)
|
||||
{
|
||||
root["local_ip"] = WiFi.localIP().toString();
|
||||
root["mac_address"] = WiFi.macAddress();
|
||||
root["rssi"] = WiFi.RSSI();
|
||||
root["ssid"] = WiFi.SSID();
|
||||
root["bssid"] = WiFi.BSSIDstr();
|
||||
root["channel"] = WiFi.channel();
|
||||
root["subnet_mask"] = WiFi.subnetMask().toString();
|
||||
root["gateway_ip"] = WiFi.gatewayIP().toString();
|
||||
IPAddress dnsIP1 = WiFi.dnsIP(0);
|
||||
IPAddress dnsIP2 = WiFi.dnsIP(1);
|
||||
if (IPUtils::isSet(dnsIP1))
|
||||
{
|
||||
root["dns_ip_1"] = dnsIP1.toString();
|
||||
}
|
||||
if (IPUtils::isSet(dnsIP2))
|
||||
{
|
||||
root["dns_ip_2"] = dnsIP2.toString();
|
||||
}
|
||||
}
|
||||
|
||||
return response.send();
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
#ifndef WiFiStatus_h
|
||||
#define WiFiStatus_h
|
||||
|
||||
/**
|
||||
* ESP32 SvelteKit
|
||||
*
|
||||
* A simple, secure and extensible framework for IoT projects for ESP32 platforms
|
||||
* with responsive Sveltekit front-end built with TailwindCSS and DaisyUI.
|
||||
* https://github.com/theelims/ESP32-sveltekit
|
||||
*
|
||||
* Copyright (C) 2018 - 2023 rjwats
|
||||
* Copyright (C) 2023 theelims
|
||||
*
|
||||
* All Rights Reserved. This software may be modified and distributed under
|
||||
* the terms of the LGPL v3 license. See the LICENSE file for details.
|
||||
**/
|
||||
|
||||
#include <WiFi.h>
|
||||
|
||||
#include <ArduinoJson.h>
|
||||
#include <PsychicHttp.h>
|
||||
#include <IPUtils.h>
|
||||
#include <SecurityManager.h>
|
||||
|
||||
#define MAX_WIFI_STATUS_SIZE 1024
|
||||
#define WIFI_STATUS_SERVICE_PATH "/rest/wifiStatus"
|
||||
|
||||
class WiFiStatus
|
||||
{
|
||||
public:
|
||||
WiFiStatus(PsychicHttpServer *server, SecurityManager *securityManager);
|
||||
|
||||
void begin();
|
||||
|
||||
private:
|
||||
PsychicHttpServer *_server;
|
||||
SecurityManager *_securityManager;
|
||||
|
||||
// static functions for logging WiFi events to the UART
|
||||
static void onStationModeConnected(WiFiEvent_t event, WiFiEventInfo_t info);
|
||||
static void onStationModeDisconnected(WiFiEvent_t event, WiFiEventInfo_t info);
|
||||
static void onStationModeGotIP(WiFiEvent_t event, WiFiEventInfo_t info);
|
||||
esp_err_t wifiStatus(PsychicRequest *request);
|
||||
};
|
||||
|
||||
#endif // end WiFiStatus_h
|
||||
@@ -0,0 +1,16 @@
|
||||
# v1.1
|
||||
|
||||
* Changed the internal structure to support request handlers on endpoints and generic requests that do not match an endpoint
|
||||
* websockets, uploads, etc should now create an appropriate handler and attach to an endpoint with the server.on() syntax
|
||||
* Added PsychicClient to abstract away some of the internals of ESP-IDF sockets + add convenience
|
||||
* onOpen and onClose callbacks have changed as a result
|
||||
* Added support for EventSource / SSE
|
||||
* Added support for multipart file uploads
|
||||
* changed getParam() to return a PsychicWebParameter in line with ESPAsyncWebserver
|
||||
* Renamed various classes / files:
|
||||
* PsychicHttpFileResponse -> PsychicFileResponse
|
||||
* PsychicHttpServerEndpoint -> PsychicEndpoint
|
||||
* PsychicHttpServerRequest -> PsychicRequest
|
||||
* PsychicHttpServerResponse -> PsychicResponse
|
||||
* PsychicHttpWebsocket.h -> PsychicWebSocket.h
|
||||
* Websocket => WebSocket
|
||||
@@ -0,0 +1,165 @@
|
||||
GNU LESSER GENERAL PUBLIC LICENSE
|
||||
Version 3, 29 June 2007
|
||||
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
|
||||
This version of the GNU Lesser General Public License incorporates
|
||||
the terms and conditions of version 3 of the GNU General Public
|
||||
License, supplemented by the additional permissions listed below.
|
||||
|
||||
0. Additional Definitions.
|
||||
|
||||
As used herein, "this License" refers to version 3 of the GNU Lesser
|
||||
General Public License, and the "GNU GPL" refers to version 3 of the GNU
|
||||
General Public License.
|
||||
|
||||
"The Library" refers to a covered work governed by this License,
|
||||
other than an Application or a Combined Work as defined below.
|
||||
|
||||
An "Application" is any work that makes use of an interface provided
|
||||
by the Library, but which is not otherwise based on the Library.
|
||||
Defining a subclass of a class defined by the Library is deemed a mode
|
||||
of using an interface provided by the Library.
|
||||
|
||||
A "Combined Work" is a work produced by combining or linking an
|
||||
Application with the Library. The particular version of the Library
|
||||
with which the Combined Work was made is also called the "Linked
|
||||
Version".
|
||||
|
||||
The "Minimal Corresponding Source" for a Combined Work means the
|
||||
Corresponding Source for the Combined Work, excluding any source code
|
||||
for portions of the Combined Work that, considered in isolation, are
|
||||
based on the Application, and not on the Linked Version.
|
||||
|
||||
The "Corresponding Application Code" for a Combined Work means the
|
||||
object code and/or source code for the Application, including any data
|
||||
and utility programs needed for reproducing the Combined Work from the
|
||||
Application, but excluding the System Libraries of the Combined Work.
|
||||
|
||||
1. Exception to Section 3 of the GNU GPL.
|
||||
|
||||
You may convey a covered work under sections 3 and 4 of this License
|
||||
without being bound by section 3 of the GNU GPL.
|
||||
|
||||
2. Conveying Modified Versions.
|
||||
|
||||
If you modify a copy of the Library, and, in your modifications, a
|
||||
facility refers to a function or data to be supplied by an Application
|
||||
that uses the facility (other than as an argument passed when the
|
||||
facility is invoked), then you may convey a copy of the modified
|
||||
version:
|
||||
|
||||
a) under this License, provided that you make a good faith effort to
|
||||
ensure that, in the event an Application does not supply the
|
||||
function or data, the facility still operates, and performs
|
||||
whatever part of its purpose remains meaningful, or
|
||||
|
||||
b) under the GNU GPL, with none of the additional permissions of
|
||||
this License applicable to that copy.
|
||||
|
||||
3. Object Code Incorporating Material from Library Header Files.
|
||||
|
||||
The object code form of an Application may incorporate material from
|
||||
a header file that is part of the Library. You may convey such object
|
||||
code under terms of your choice, provided that, if the incorporated
|
||||
material is not limited to numerical parameters, data structure
|
||||
layouts and accessors, or small macros, inline functions and templates
|
||||
(ten or fewer lines in length), you do both of the following:
|
||||
|
||||
a) Give prominent notice with each copy of the object code that the
|
||||
Library is used in it and that the Library and its use are
|
||||
covered by this License.
|
||||
|
||||
b) Accompany the object code with a copy of the GNU GPL and this license
|
||||
document.
|
||||
|
||||
4. Combined Works.
|
||||
|
||||
You may convey a Combined Work under terms of your choice that,
|
||||
taken together, effectively do not restrict modification of the
|
||||
portions of the Library contained in the Combined Work and reverse
|
||||
engineering for debugging such modifications, if you also do each of
|
||||
the following:
|
||||
|
||||
a) Give prominent notice with each copy of the Combined Work that
|
||||
the Library is used in it and that the Library and its use are
|
||||
covered by this License.
|
||||
|
||||
b) Accompany the Combined Work with a copy of the GNU GPL and this license
|
||||
document.
|
||||
|
||||
c) For a Combined Work that displays copyright notices during
|
||||
execution, include the copyright notice for the Library among
|
||||
these notices, as well as a reference directing the user to the
|
||||
copies of the GNU GPL and this license document.
|
||||
|
||||
d) Do one of the following:
|
||||
|
||||
0) Convey the Minimal Corresponding Source under the terms of this
|
||||
License, and the Corresponding Application Code in a form
|
||||
suitable for, and under terms that permit, the user to
|
||||
recombine or relink the Application with a modified version of
|
||||
the Linked Version to produce a modified Combined Work, in the
|
||||
manner specified by section 6 of the GNU GPL for conveying
|
||||
Corresponding Source.
|
||||
|
||||
1) Use a suitable shared library mechanism for linking with the
|
||||
Library. A suitable mechanism is one that (a) uses at run time
|
||||
a copy of the Library already present on the user's computer
|
||||
system, and (b) will operate properly with a modified version
|
||||
of the Library that is interface-compatible with the Linked
|
||||
Version.
|
||||
|
||||
e) Provide Installation Information, but only if you would otherwise
|
||||
be required to provide such information under section 6 of the
|
||||
GNU GPL, and only to the extent that such information is
|
||||
necessary to install and execute a modified version of the
|
||||
Combined Work produced by recombining or relinking the
|
||||
Application with a modified version of the Linked Version. (If
|
||||
you use option 4d0, the Installation Information must accompany
|
||||
the Minimal Corresponding Source and Corresponding Application
|
||||
Code. If you use option 4d1, you must provide the Installation
|
||||
Information in the manner specified by section 6 of the GNU GPL
|
||||
for conveying Corresponding Source.)
|
||||
|
||||
5. Combined Libraries.
|
||||
|
||||
You may place library facilities that are a work based on the
|
||||
Library side by side in a single library together with other library
|
||||
facilities that are not Applications and are not covered by this
|
||||
License, and convey such a combined library under terms of your
|
||||
choice, if you do both of the following:
|
||||
|
||||
a) Accompany the combined library with a copy of the same work based
|
||||
on the Library, uncombined with any other library facilities,
|
||||
conveyed under the terms of this License.
|
||||
|
||||
b) Give prominent notice with the combined library that part of it
|
||||
is a work based on the Library, and explaining where to find the
|
||||
accompanying uncombined form of the same work.
|
||||
|
||||
6. Revised Versions of the GNU Lesser General Public License.
|
||||
|
||||
The Free Software Foundation may publish revised and/or new versions
|
||||
of the GNU Lesser General Public License from time to time. Such new
|
||||
versions will be similar in spirit to the present version, but may
|
||||
differ in detail to address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the
|
||||
Library as you received it specifies that a certain numbered version
|
||||
of the GNU Lesser General Public License "or any later version"
|
||||
applies to it, you have the option of following the terms and
|
||||
conditions either of that published version or of any later version
|
||||
published by the Free Software Foundation. If the Library as you
|
||||
received it does not specify a version number of the GNU Lesser
|
||||
General Public License, you may choose any version of the GNU Lesser
|
||||
General Public License ever published by the Free Software Foundation.
|
||||
|
||||
If the Library as you received it specifies that a proxy can decide
|
||||
whether future versions of the GNU Lesser General Public License shall
|
||||
apply, that proxy's public statement of acceptance of any version is
|
||||
permanent authorization for you to choose that version for the
|
||||
Library.
|
||||
@@ -0,0 +1,619 @@
|
||||
# PsychicHttp - HTTP on your ESP 🧙🔮
|
||||
|
||||
PsychicHttp is a webserver library for ESP32 + Arduino framework which uses the [ESP-IDF HTTP Server](https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-reference/protocols/esp_http_server.html) library under the hood. It is written in a similar style to the [Arduino WebServer](https://github.com/espressif/arduino-esp32/tree/master/libraries/WebServer), [ESPAsyncWebServer](https://github.com/me-no-dev/ESPAsyncWebServer), and [ArduinoMongoose](https://github.com/jeremypoulter/ArduinoMongoose) libraries to make writing code simple and porting from those other libraries straightforward.
|
||||
|
||||
# Features
|
||||
|
||||
* Asynchronous approach (server runs in its own FreeRTOS thread)
|
||||
* Handles all HTTP methods with lots of convenience functions:
|
||||
* GET/POST parameters
|
||||
* get/set headers
|
||||
* get/set cookies
|
||||
* basic key/value session data storage
|
||||
* authentication (basic and digest mode)
|
||||
* HTTPS / SSL support
|
||||
* Static fileserving (SPIFFS, LittleFS, etc.)
|
||||
* Chunked response serving for large files
|
||||
* File uploads (Basic + Multipart)
|
||||
* Websocket support with onOpen, onFrame, and onClose callbacks
|
||||
* EventSource / SSE support with onOpen, and onClose callbacks
|
||||
* Request filters, including Client vs AP mode (ON_STA_FILTER / ON_AP_FILTER)
|
||||
|
||||
## Differences from ESPAsyncWebserver
|
||||
|
||||
* No templating system (anyone actually use this?)
|
||||
* No url rewriting (but you can use request->redirect)
|
||||
|
||||
# Usage
|
||||
|
||||
## Installation
|
||||
|
||||
### Platformio
|
||||
|
||||
[PlatformIO](http://platformio.org) is an open source ecosystem for IoT development.
|
||||
|
||||
Add "PsychicHttp" to project using [Project Configuration File `platformio.ini`](http://docs.platformio.org/page/projectconf.html) and [lib_deps](http://docs.platformio.org/page/projectconf/section_env_library.html#lib-deps) option:
|
||||
|
||||
```ini
|
||||
[env:myboard]
|
||||
platform = espressif...
|
||||
board = ...
|
||||
framework = arduino
|
||||
|
||||
# using the latest stable version
|
||||
lib_deps = hoeken/PsychicHttp
|
||||
|
||||
# or using GIT Url (the latest development version)
|
||||
lib_deps = https://github.com/hoeken/PsychicHttp
|
||||
```
|
||||
|
||||
### Installation - Arduino
|
||||
|
||||
Open *Tools -> Manage Libraries...* and search for PsychicHttp.
|
||||
|
||||
# Principles of Operation
|
||||
|
||||
## Things to Note
|
||||
|
||||
* PsychicHttp is a fully asynchronous server and as such does not run on the loop thread.
|
||||
* You should not use yield or delay or any function that uses them inside the callbacks.
|
||||
* The server is smart enough to know when to close the connection and free resources.
|
||||
* You can not send more than one response to a single request.
|
||||
|
||||
## PsychicHttp
|
||||
|
||||
* Listens for connections.
|
||||
* Wraps the incoming request into PsychicRequest.
|
||||
* Keeps track of clients + calls optional callbacks on client open and close.
|
||||
* Find the appropriate handler (if any) for a request and pass it on.
|
||||
|
||||
## Request Life Cycle
|
||||
|
||||
* TCP connection is received by the server.
|
||||
* HTTP request is wrapped inside ```PsychicRequest``` object + TCP Connection wrapped inside PsychicConnection object.
|
||||
* When the request head is received, the server goes through all ```PsychicEndpoints``` and finds one that matches the url + method.
|
||||
* ```handler->filter()``` and ```handler->canHandle()``` are called on the handler to verify the handler should process the request.
|
||||
* ```handler->needsAuthentication()``` is called and sends an authorization response if required.
|
||||
* ```handler->handleRequest()``` is called to actually process the HTTP request.
|
||||
* If the handler cannot process the request, the server will loop through any global handlers and call that handler if it passes filter(), canHandle(), and needsAuthentication().
|
||||
* If no global handlers are called, the server.defaultEndpoint handler will be called.
|
||||
* Each handler is responsible for processing the request and sending a response.
|
||||
* When the response is sent, the client is closed and freed from the memory.
|
||||
* Unless its a special handler like websockets or eventsource.
|
||||
|
||||

|
||||
|
||||
### Handlers
|
||||
|
||||
* ```PsychicHandler``` is used for processing and responding to specific HTTP requests.
|
||||
* ```PsychicHandler``` instances can be attached to any endpoint or as global handlers.
|
||||
* Setting a ```Filter``` to the ```PsychicHandler``` controls when to apply the handler, decision can be based on
|
||||
request method, url, request host/port/target host, the request client's localIP or remoteIP.
|
||||
* Two filter callbacks are provided: ```ON_AP_FILTER``` to execute the rewrite when request is made to the AP interface,
|
||||
```ON_STA_FILTER``` to execute the rewrite when request is made to the STA interface.
|
||||
* The ```canHandle``` method is used for handler specific control on whether the requests can be handled. Decision can be based on request method, request url, request host/port/target host.
|
||||
* Depending on how the handler is implemented, it may provide callbacks for adding your own custom processing code to the handler.
|
||||
* Global ```Handlers``` are evaluated in the order they are attached to the server. The ```canHandle``` is called only
|
||||
if the ```Filter``` that was set to the ```Handler``` return true.
|
||||
* The first global ```Handler``` that can handle the request is selected, no further processing of handlers is called.
|
||||
|
||||

|
||||
|
||||
### Responses and how do they work
|
||||
|
||||
* The ```PsychicResponse``` objects are used to send the response data back to the client.
|
||||
* Typically the response should be fully generated and sent from the callback.
|
||||
* It may be possible to generate the response outside the callback, but it will be difficult.
|
||||
* The exceptions are websockets + eventsource where the response is sent, but the connection is maintained and new data can be sent/received outside the handler.
|
||||
|
||||
# Porting From ESPAsyncWebserver
|
||||
|
||||
If you have existing code using ESPAsyncWebserver, you will feel right at home with PsychicHttp. Even if internally it is much different, the external interface is very similar. Some things are mostly cosmetic, like different class names and callback definitions. A few things might require a bit more in-depth approach. If you're porting your code and run into issues that aren't covered here, please post and issue.
|
||||
|
||||
## Globals Stuff
|
||||
|
||||
* Change your #include to ```#include <PsychicHttp.h>```
|
||||
* Change your server instance: ```PsychicHttpServer server;```
|
||||
* Define websocket handler if you have one: ```PsychicWebSocketHandler websocketHandler;```
|
||||
* Define eventsource if you have one: ```PsychicEventSource eventSource;```
|
||||
|
||||
## setup() Stuff
|
||||
|
||||
* no more server.begin(), call server.listen(80), before you add your handlers
|
||||
* server has a configurable limit on .on() endpoints. change it with ```server.config.max_uri_handlers = 20;``` as needed.
|
||||
* check your callback function definitions:
|
||||
* AsyncWebServerRequest -> PsychicRequest
|
||||
* no more onBody() event
|
||||
* for small bodies (server.maxRequestBodySize, default 16k) it will be automatically loaded and accessed by request->body()
|
||||
* for large bodies, use an upload handler and onUpload()
|
||||
* websocket callbacks are much different (and simpler!)
|
||||
* websocket / eventsource handlers get attached to url in server.on("/url", &handler) instead of passing url to handler constructor.
|
||||
* eventsource callbacks are onOpen and onClose now.
|
||||
* HTTP_ANY is not supported by ESP-IDF, so we can't use it either.
|
||||
* NO server.onFileUpload(onUpload); (you could attach an UploadHandler to the default endpoint i guess?)
|
||||
* NO server.onRequestBody(onBody); (same)
|
||||
|
||||
## Requests / Responses
|
||||
|
||||
* request->send is now request->reply()
|
||||
* if you create a response, call response->send() directly, not request->send(reply)
|
||||
* request->headers() is not supported by ESP-IDF, you have to just check for the header you need.
|
||||
* No AsyncCallbackJsonWebHandler (for now... can add if needed)
|
||||
* No request->beginResponse(). Instanciate a PsychicResponse instead: ```PsychicResponse response(request);```
|
||||
* No PROGMEM suppport (its not relevant to ESP32: https://esp32.com/viewtopic.php?t=20595)
|
||||
* No Stream response support just yet
|
||||
|
||||
# Usage
|
||||
|
||||
## Create the Server
|
||||
|
||||
Here is an example of the typical server setup:
|
||||
|
||||
```cpp
|
||||
#include <PsychicHttp.h>
|
||||
PsychicHttpServer server;
|
||||
|
||||
void setup()
|
||||
{
|
||||
//optional low level setup server config stuff here.
|
||||
//server.config is an ESP-IDF httpd_config struct
|
||||
//see: https://docs.espressif.com/projects/esp-idf/en/v4.4.6/esp32/api-reference/protocols/esp_http_server.html#_CPPv412httpd_config
|
||||
//increase maximum number of uri endpoint handlers (.on() calls)
|
||||
server.config.max_uri_handlers = 20;
|
||||
|
||||
//connect to wifi
|
||||
|
||||
//start the server listening on port 80 (standard HTTP port)
|
||||
server.listen(80);
|
||||
|
||||
//call server methods to attach endpoints and handlers
|
||||
server.on(...);
|
||||
server.serveStatic(...);
|
||||
server.attachHandler(...);
|
||||
}
|
||||
```
|
||||
|
||||
## Add Handlers
|
||||
|
||||
One major difference from ESPAsyncWebserver is that handlers can be attached to a specific url (endpoint) or as a global handler. The reason for this, is that attaching to a specific URL is more efficient and makes for cleaner code.
|
||||
|
||||
### Endpoint Handlers
|
||||
|
||||
An endpoint is basically just the URL path (eg. /path/to/file) without any query string. The ```server.on(...)``` function is a convenience function for creating endpoints and attaching a handler to them. There are two main styles: attaching a basic ```WebRequest``` handler and attaching an external handler.
|
||||
|
||||
```cpp
|
||||
//creates a basic PsychicWebHandler that calls the request_callback callback
|
||||
server.on("/url", HTTP_GET, request_callback);
|
||||
|
||||
//same as above, but defaults to HTTP_GET
|
||||
server.on("/url", request_callback);
|
||||
|
||||
//attaches a websocket handler to /ws
|
||||
PsychicWebSocketHandler websocketHandler;
|
||||
server.on("/ws", &websocketHandler);
|
||||
```
|
||||
|
||||
The ```server.on(...)``` returns a pointer to the endpoint, which can be used to call various functions like ```setHandler()```, ```setFilter()```, and ```setAuthentication()```.
|
||||
|
||||
```cpp
|
||||
//respond to /url only from requests to the AP
|
||||
server.on("/url", HTTP_GET, request_callback)->setFilter(ON_AP_FILTER);
|
||||
|
||||
//require authentication on /url
|
||||
server.on("/url", HTTP_GET, request_callback)->setAuthentication("user", "pass");
|
||||
|
||||
//attach websocket handler to /ws
|
||||
PsychicWebSocketHandler websocketHandler;
|
||||
server.on("/ws")->attachHandler(&websocketHandler);
|
||||
```
|
||||
|
||||
### Basic Requests
|
||||
|
||||
The ```PsychicWebHandler``` class is for handling standard web requests. It provides a single callback: ```onRequest()```. This callback is called when the handler receives a valid HTTP request.
|
||||
|
||||
One major difference from ESPAsyncWebserver is that this callback needs to return an esp_err_t variable to let the server know the result of processing the request. The ```response->reply()``` and ```request->send()``` functions will return this. It is a good habit to return the result of these functions as sending the response will close the connection.
|
||||
|
||||
The function definition for the onRequest callback is:
|
||||
|
||||
```cpp
|
||||
esp_err_t function_name(PsychicRequest *request);
|
||||
```
|
||||
|
||||
Here is a simple example that sends back the client's IP on the URL /ip
|
||||
|
||||
```cpp
|
||||
server.on("/ip", [](PsychicRequest *request)
|
||||
{
|
||||
String output = "Your IP is: " + request->client()->remoteIP().toString();
|
||||
return request->reply(output.c_str());
|
||||
});
|
||||
```
|
||||
|
||||
### Uploads
|
||||
|
||||
The ```PsychicUploadHandler``` class is for handling uploads, both large POST bodies and multipart encoded forms. It provides two callbacks: ```onUpload()``` and ```onRequest()```.
|
||||
|
||||
```onUpload(...)``` is called when there is new data. This function may be called multiple times so that you can process the data in chunks. The function definition for the onUpload callback is:
|
||||
|
||||
```cpp
|
||||
esp_err_t function_name(PsychicRequest *request, const String& filename, uint64_t index, uint8_t *data, size_t len, bool final);
|
||||
```
|
||||
|
||||
* request is a pointer to the Request object
|
||||
* filename is the name of the uploaded file
|
||||
* index is the overall byte position of the current data
|
||||
* data is a pointer to the data buffer
|
||||
* len is the length of the data buffer
|
||||
* final is a flag to tell if its the last chunk of data
|
||||
|
||||
```onRequest(...)``` is called after the successful handling of the upload. Its definition and usage is the same as the basic request example as above.
|
||||
|
||||
#### Basic Upload (file is the entire POST body)
|
||||
|
||||
It's worth noting that there is no standard way of passing in a filename for this method, so the handler attempts to guess the filename with the following methods:
|
||||
|
||||
* Checking the Content-Disposition header
|
||||
* Checking the _filename query parameter (eg. /upload?filename=filename.txt becomes filename.txt)
|
||||
* Checking the url and taking the last part as filename (eg. /upload/filename.txt becomes filename.txt). You must set a wildcard url for this to work as in the example below.
|
||||
|
||||
```cpp
|
||||
//handle a very basic upload as post body
|
||||
PsychicUploadHandler *uploadHandler = new PsychicUploadHandler();
|
||||
uploadHandler->onUpload([](PsychicRequest *request, const String& filename, uint64_t index, uint8_t *data, size_t len, bool last) {
|
||||
File file;
|
||||
String path = "/www/" + filename;
|
||||
|
||||
Serial.printf("Writing %d/%d bytes to: %s\n", (int)index+(int)len, request->contentLength(), path.c_str());
|
||||
|
||||
if (last)
|
||||
Serial.printf("%s is finished. Total bytes: %d\n", path.c_str(), (int)index+(int)len);
|
||||
|
||||
//our first call?
|
||||
if (!index)
|
||||
file = LittleFS.open(path, FILE_WRITE);
|
||||
else
|
||||
file = LittleFS.open(path, FILE_APPEND);
|
||||
|
||||
if(!file) {
|
||||
Serial.println("Failed to open file");
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
if(!file.write(data, len)) {
|
||||
Serial.println("Write failed");
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
return ESP_OK;
|
||||
});
|
||||
|
||||
//gets called after upload has been handled
|
||||
uploadHandler->onRequest([](PsychicRequest *request)
|
||||
{
|
||||
String url = "/" + request->getFilename();
|
||||
String output = "<a href=\"" + url + "\">" + url + "</a>";
|
||||
|
||||
return request->reply(output.c_str());
|
||||
});
|
||||
|
||||
//wildcard basic file upload - POST to /upload/filename.ext
|
||||
server.on("/upload/*", HTTP_POST, uploadHandler);
|
||||
```
|
||||
|
||||
#### Multipart Upload
|
||||
|
||||
Very similar to the basic upload, with 2 key differences:
|
||||
|
||||
* multipart requests don't know the total size of the file until after it has been fully processed. You can get a rough idea with request->contentLength(), but that is the length of the entire multipart encoded request.
|
||||
* you can access form variables, including multipart file infor (name + size) in the onRequest handler using request->getParam()
|
||||
|
||||
```cpp
|
||||
//a little bit more complicated multipart form
|
||||
PsychicUploadHandler *multipartHandler = new PsychicUploadHandler();
|
||||
multipartHandler->onUpload([](PsychicRequest *request, const String& filename, uint64_t index, uint8_t *data, size_t len, bool last) {
|
||||
File file;
|
||||
String path = "/www/" + filename;
|
||||
|
||||
//some progress over serial.
|
||||
Serial.printf("Writing %d bytes to: %s\n", (int)len, path.c_str());
|
||||
if (last)
|
||||
Serial.printf("%s is finished. Total bytes: %d\n", path.c_str(), (int)index+(int)len);
|
||||
|
||||
//our first call?
|
||||
if (!index)
|
||||
file = LittleFS.open(path, FILE_WRITE);
|
||||
else
|
||||
file = LittleFS.open(path, FILE_APPEND);
|
||||
|
||||
if(!file) {
|
||||
Serial.println("Failed to open file");
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
if(!file.write(data, len)) {
|
||||
Serial.println("Write failed");
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
return ESP_OK;
|
||||
});
|
||||
|
||||
//gets called after upload has been handled
|
||||
multipartHandler->onRequest([](PsychicRequest *request)
|
||||
{
|
||||
PsychicWebParameter *file = request->getParam("file_upload");
|
||||
|
||||
String url = "/" + file->value();
|
||||
String output;
|
||||
|
||||
output += "<a href=\"" + url + "\">" + url + "</a><br/>\n";
|
||||
output += "Bytes: " + String(file->size()) + "<br/>\n";
|
||||
output += "Param 1: " + request->getParam("param1")->value() + "<br/>\n";
|
||||
output += "Param 2: " + request->getParam("param2")->value() + "<br/>\n";
|
||||
|
||||
return request->reply(output.c_str());
|
||||
});
|
||||
|
||||
//upload to /multipart url
|
||||
server.on("/multipart", HTTP_POST, multipartHandler);
|
||||
```
|
||||
|
||||
### Static File Serving
|
||||
|
||||
The ```PsychicStaticFileHandler``` is a special handler that does not provide any callbacks. It is used to serve a file or files from a specific directory in a filesystem to a directory on the webserver. The syntax is exactly the same as ESPAsyncWebserver. Anything that is derived from the ```FS``` class should work (eg. SPIFFS, LittleFS, SD, etc)
|
||||
|
||||
A couple important notes:
|
||||
|
||||
* If it finds a file with an extra .gz extension, it will serve it as gzip encoded (eg: /targetfile.ext -> {targetfile.ext}.gz)
|
||||
* If the file is larger than FILE_CHUNK_SIZE (default 8kb) then it will send it as a chunked response.
|
||||
* It will detect most basic filetypes and automatically set the appropriate Content-Type
|
||||
|
||||
The ```server.serveStatic()``` function handles creating the handler and assigning it to the server:
|
||||
|
||||
```cpp
|
||||
//serve static files from LittleFS/www on / only to clients on same wifi network
|
||||
//this is where our /index.html file lives
|
||||
server.serveStatic("/", LittleFS, "/www/")->setFilter(ON_STA_FILTER);
|
||||
|
||||
//serve static files from LittleFS/www-ap on / only to clients on SoftAP
|
||||
//this is where our /index.html file lives
|
||||
server.serveStatic("/", LittleFS, "/www-ap/")->setFilter(ON_AP_FILTER);
|
||||
|
||||
//serve static files from LittleFS/img on /img
|
||||
//it's more efficient to serve everything from a single www directory, but this is also possible.
|
||||
server.serveStatic("/img", LittleFS, "/img/");
|
||||
|
||||
//you can also serve single files
|
||||
server.serveStatic("/myfile.txt", LittleFS, "/custom.txt");
|
||||
```
|
||||
|
||||
You could also theoretically use the file response directly:
|
||||
|
||||
```cpp
|
||||
server.on("/ip", [](PsychicRequest *request)
|
||||
{
|
||||
String filename = "/path/to/file";
|
||||
PsychicFileResponse response(request, LittleFS, filename);
|
||||
|
||||
return response.send();
|
||||
});
|
||||
PsychicFileResponse(PsychicRequest *request, FS &fs, const String& path)
|
||||
```
|
||||
|
||||
### Websockets
|
||||
|
||||
The ```PsychicWebSocketHandler``` class is for handling WebSocket connections. It provides 3 callbacks:
|
||||
|
||||
```onOpen(...)``` is called when a new WebSocket client connects.
|
||||
```onFrame(...)``` is called when a new WebSocket frame has arrived.
|
||||
```onClose(...)``` is called when a new WebSocket client disconnects.
|
||||
|
||||
Here are the callback definitions:
|
||||
|
||||
```cpp
|
||||
void open_function(PsychicWebSocketClient *client);
|
||||
esp_err_t frame_function(PsychicWebSocketRequest *request, httpd_ws_frame *frame);
|
||||
void close_function(PsychicWebSocketClient *client);
|
||||
```
|
||||
|
||||
WebSockets were the main reason for starting PsychicHttp, so they are well tested. They are also much simplified from the ESPAsyncWebserver style. You do not need to worry about error handling, partial frame assembly, PONG messages, etc. The onFrame() function is called when a complete frame has been received, and can handle frames up to the entire available heap size.
|
||||
|
||||
Here is a basic example of using WebSockets:
|
||||
|
||||
```cpp
|
||||
//create our handler... note this should be located as a global or somewhere it wont go out of scope and be destroyed.
|
||||
PsychicWebSocketHandler websocketHandler();
|
||||
|
||||
websocketHandler.onOpen([](PsychicWebSocketClient *client) {
|
||||
Serial.printf("[socket] connection #%u connected from %s\n", client->socket(), client->remoteIP().toString());
|
||||
client->sendMessage("Hello!");
|
||||
});
|
||||
|
||||
websocketHandler.onFrame([](PsychicWebSocketRequest *request, httpd_ws_frame *frame) {
|
||||
Serial.printf("[socket] #%d sent: %s\n", request->client()->socket(), (char *)frame->payload);
|
||||
return request->reply(frame);
|
||||
});
|
||||
|
||||
websocketHandler.onClose([](PsychicWebSocketClient *client) {
|
||||
Serial.printf("[socket] connection #%u closed from %s\n", client->socket(), client->remoteIP().toString());
|
||||
});
|
||||
|
||||
//attach the handler to /ws. You can then connect to ws://ip.address/ws
|
||||
server.on("/ws", &websocketHandler);
|
||||
```
|
||||
|
||||
The onFrame() callback has 2 parameters:
|
||||
|
||||
* ```PsychicWebSocketRequest *request``` a special request with helper functions for replying in websocket format.
|
||||
* ```httpd_ws_frame *frame``` ESP-IDF websocket struct. The important struct members we care about are:
|
||||
* ```uint8_t *payload; /*!< Pre-allocated data buffer */```
|
||||
* ```size_t len; /*!< Length of the WebSocket data */```
|
||||
|
||||
For sending data on the websocket connection, there are 3 methods:
|
||||
|
||||
* ```request->reply()``` - only available in the onFrame() callback context.
|
||||
* ```webSocketHandler.sendAll()``` - can be used anywhere to send websocket messages to all connected clients.
|
||||
* ```client->send()``` - can be used anywhere* to send a websocket message to a specific client
|
||||
|
||||
All of the above functions either accept simple ```char *``` string of you can construct your own httpd_ws_frame.
|
||||
|
||||
*Special Note:* Do not hold on to the ```PsychicWebSocketClient``` for sending messages to clients outside the callbacks. That pointer is destroyed when a client disconnects. Instead, store the ```int client->socket()```. Then when you want to send a message, use this code:
|
||||
|
||||
```cpp
|
||||
//make sure our client is still connected.
|
||||
PsychicWebSocketClient *client = websocketHandler.getClient(socket);
|
||||
if (client != NULL)
|
||||
client->send("Your Message")
|
||||
```
|
||||
|
||||
### EventSource / SSE
|
||||
|
||||
The ```PsychicEventSource``` class is for handling EventSource / SSE connections. It provides 2 callbacks:
|
||||
|
||||
```onOpen(...)``` is called when a new EventSource client connects.
|
||||
```onClose(...)``` is called when a new EventSource client disconnects.
|
||||
|
||||
Here are the callback definitions:
|
||||
|
||||
```cpp
|
||||
void open_function(PsychicEventSourceClient *client);
|
||||
void close_function(PsychicEventSourceClient *client);
|
||||
```
|
||||
|
||||
Here is a basic example of using PsychicEventSource:
|
||||
|
||||
```cpp
|
||||
//create our handler... note this should be located as a global or somewhere it wont go out of scope and be destroyed.
|
||||
PsychicEventSource eventSource;
|
||||
|
||||
eventSource.onOpen([](PsychicEventSourceClient *client) {
|
||||
Serial.printf("[eventsource] connection #%u connected from %s\n", client->socket(), client->remoteIP().toString());
|
||||
client->send("Hello user!", NULL, millis(), 1000);
|
||||
});
|
||||
|
||||
eventSource.onClose([](PsychicEventSourceClient *client) {
|
||||
Serial.printf("[eventsource] connection #%u closed from %s\n", client->socket(), client->remoteIP().toString());
|
||||
});
|
||||
|
||||
//attach the handler to /events
|
||||
server.on("/events", &eventSource);
|
||||
```
|
||||
|
||||
For sending data on the EventSource connection, there are 2 methods:
|
||||
|
||||
* ```eventSource.send()``` - can be used anywhere to send events to all connected clients.
|
||||
* ```client->send()``` - can be used anywhere* to send events to a specific client
|
||||
|
||||
All of the above functions accept a simple ```char *``` message, and optionally: ```char *``` event name, id, and reconnect time.
|
||||
|
||||
*Special Note:* Do not hold on to the ```PsychicEventSourceClient``` for sending messages to clients outside the callbacks. That pointer is destroyed when a client disconnects. Instead, store the ```int client->socket()```. Then when you want to send a message, use this code:
|
||||
|
||||
```cpp
|
||||
//make sure our client is still connected.
|
||||
PsychicEventSourceClient *client = eventSource.getClient(socket);
|
||||
if (client != NULL)
|
||||
client->send("Your Event")
|
||||
```
|
||||
|
||||
### HTTPS / SSL
|
||||
|
||||
PsychicHttp supports HTTPS / SSL out of the box, however there are some limitations (see performance below). Enabling it also increases the code size by about 100kb. To use HTTPS, you need to modify your setup like so:
|
||||
|
||||
```cpp
|
||||
#include <PsychicHttp.h>
|
||||
#include <PsychicHttpsServer.h>
|
||||
PsychicHttpsServer server;
|
||||
server.listen(443, server_cert, server_key);
|
||||
```
|
||||
|
||||
```server_cert``` and ```server_key``` are both ```const char *``` parameters which contain the server certificate and private key, respectively.
|
||||
|
||||
To generate your own key and self signed certificate, you can use the command below:
|
||||
|
||||
```
|
||||
openssl req -x509 -newkey rsa:4096 -nodes -keyout server.key -out server.crt -sha256 -days 365
|
||||
```
|
||||
|
||||
Including the ```PsychicHttpsServer.h``` also defines ```PSY_ENABLE_SSL``` which you can use in your code to allow enabling / disabling calls in your code based on if the HTTPS server is available:
|
||||
|
||||
```cpp
|
||||
//our main server object
|
||||
#ifdef PSY_ENABLE_SSL
|
||||
PsychicHttpsServer server;
|
||||
#else
|
||||
PsychicHttpServer server;
|
||||
#endif
|
||||
```
|
||||
|
||||
Last, but not least, you can create a separate HTTP server on port 80 that redirects all requests to the HTTPS server:
|
||||
|
||||
```cpp
|
||||
//this creates a 2nd server listening on port 80 and redirects all requests HTTPS
|
||||
PsychicHttpServer *redirectServer = new PsychicHttpServer();
|
||||
redirectServer->config.ctrl_port = 20420; // just a random port different from the default one
|
||||
redirectServer->listen(80);
|
||||
redirectServer->onNotFound([](PsychicRequest *request) {
|
||||
String url = "https://" + request->host() + request->url();
|
||||
return request->redirect(url.c_str());
|
||||
});
|
||||
```
|
||||
|
||||
# Performance
|
||||
|
||||
In order to really see the differences between libraries, I created some basic benchmark firmwares for PsychicHttp, ESPAsyncWebserver, and ArduinoMongoose. I then ran the loadtest-http.sh and loadtest-websocket.sh scripts against each firmware to get some real numbers on the performance of each server library. All of the code and results are available in the /benchmark folder. If you want to see the collated data and graphs, there is a [LibreOffice spreadsheet](/benchmark/comparison.ods).
|
||||
|
||||

|
||||

|
||||
|
||||
## HTTPS / SSL
|
||||
|
||||
Yes, PsychicHttp supports SSL out of the box, but there are a few caveats:
|
||||
|
||||
* Due to memory limitations, it can only handle 2 connections at a time. Each SSL connection takes about 45k ram, and a blank PsychicHttp sketch has about 150k ram free.
|
||||
* Speed and latency are still pretty good (see graph above) but the SSH handshake seems to take 1500ms. With websockets or browser its not an issue since the connection is kept alive, but if you are loading requests in another way it will be a bit slow
|
||||
* Unless you want to expose your ESP to the internet, you are limited to self signed keys and the annoying browser security warnings that come with them.
|
||||
|
||||
## Analysis
|
||||
|
||||
The results clearly show some of the reasons for writing PsychicHttp: ESPAsyncWebserver crashes under heavy load on each test, across the board in a 60s test. That means in normal usage, you're just rolling the dice with how long it will go until it crashes. Every other number is moot, IMHO.
|
||||
|
||||
ArduinoMongoose doesn't crash under heavy load, but it does bog down with extremely high latency (15s) for web requests and appears to not even respond at the highest loadings as the loadtest script crashes instead. The code itself doesnt crash, so bonus points there. After the high load, it does go back to serving normally. One area ArduinoMongoose does shine, is in websockets where its performance is almost 2x the performance of PsychicHttp. Both in requests per second and latency. Clearly an area of improvement for PsychicHttp.
|
||||
|
||||
PsychicHttp has good performance across the board. No crashes and continously responds during each test. It is a clear winner in requests per second when serving files from memory, dynamic JSON, and has consistent performance when serving files from LittleFS. The only real downside is the lower performance of the websockets with a single connection handling 38rps, and maxing out at 120rps across multiple connections.
|
||||
|
||||
## Takeaways
|
||||
|
||||
With all due respect to @me-no-dev who has done some amazing work in the open source community, I cannot recommend anyone use the ESPAsyncWebserver for anything other than simple projects that don't need to be reliable. Even then, PsychicHttp has taken the arcane api of the ESP-IDF web server library and made it nice and friendly to use with a very similar API to ESPAsyncWebserver. Also, ESPAsyncWebserver is more or less abandoned, with 150 open issues, 77 pending pull requests, and the last commit in over 2 years.
|
||||
|
||||
ArduinoMongoose is a good alternative, although the latency issues when it gets fully loaded can be very annoying. I believe it is also cross platform to other microcontrollers as well, but I haven't tested that. The other issue here is that it is based on an old version of a modified Mongoose library that will be difficult to update as it is a major revision behind and several security updates behind as well. Big thanks to @jeremypoulter though as PsychicHttp is a fork of ArduinoMongoose so it's built on strong bones.
|
||||
|
||||
# Roadmap
|
||||
|
||||
## v1.1: Event Source + Handlers
|
||||
|
||||
* Fix all outstanding issues on Github
|
||||
* Another pass over the docs
|
||||
* DefaultHeaders
|
||||
|
||||
|
||||
## v1.2: ESPAsyncWebserver Parity
|
||||
|
||||
* HTTP_ANY support (by abusing httpd_req_handle_err)
|
||||
* Issue: it would log a warning on every request (httpd_uri.c:298)
|
||||
* Issue: req->user_ctx is not passed in. (httpd_uri.c:309)
|
||||
* Websocket support assumes an endpoint with matching url / method (httpd_uri.c:312)
|
||||
|
||||
* templating system
|
||||
* rewrite
|
||||
* regex url matching
|
||||
* What else are we missing?
|
||||
|
||||
## Longterm Wants
|
||||
|
||||
* investigate websocket performance gap
|
||||
* support for esp-idf framework
|
||||
* support for arduino 3.0 framework
|
||||
* Enable worker based multithreading with esp-idf v5.x
|
||||
* 100-continue support?
|
||||
|
||||
If anyone wants to take a crack at implementing any of the above features I am more than happy to accept pull requests.
|
||||
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 20 KiB |
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 31 KiB |
@@ -0,0 +1,48 @@
|
||||
{
|
||||
"name": "PsychicHttp",
|
||||
"version": "1.0.1",
|
||||
"description": "Arduino style wrapper around ESP-IDF HTTP library. HTTP server with SSL + websockets. Works on esp32 and probably esp8266",
|
||||
"keywords": "network,http,https,tcp,ssl,tls,websocket,espasyncwebserver",
|
||||
"repository":
|
||||
{
|
||||
"type": "git",
|
||||
"url": "https://github.com/hoeken/PsychicHttp"
|
||||
},
|
||||
"authors":
|
||||
[
|
||||
{
|
||||
"name": "Zach Hoeken",
|
||||
"email": "hoeken@gmail.com",
|
||||
"maintainer": true
|
||||
}
|
||||
],
|
||||
"license" : "LGPL-3.0-or-later",
|
||||
"examples": [
|
||||
{
|
||||
"name": "platformio",
|
||||
"base": "examples/platformio",
|
||||
"files": [
|
||||
"src/main.cpp"
|
||||
]
|
||||
}
|
||||
],
|
||||
"frameworks": "arduino",
|
||||
"platforms": "espressif32",
|
||||
"dependencies": [
|
||||
{
|
||||
"owner": "bblanchon",
|
||||
"name": "ArduinoJson",
|
||||
"version": "^6.21.4"
|
||||
},
|
||||
{
|
||||
"owner": "bblanchon",
|
||||
"name": "ArduinoTrace",
|
||||
"version": "^1.2.0"
|
||||
},
|
||||
{
|
||||
"owner": "plageoj",
|
||||
"name" : "UrlEncode",
|
||||
"version" : "^1.0.1"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
name=PsychicHttp
|
||||
version=1.0.1
|
||||
author=Zach Hoeken <hoeken@gmail.com>
|
||||
maintainer=Zach Hoeken <hoeken@gmail.com>
|
||||
sentence=PsychicHttp is a robust webserver that supports http/https + websockets.
|
||||
paragraph=This library is based on the ESP-IDF HTTP Server library which is asynchronous, does http / https+ssl and supports websockets.
|
||||
category=Communication
|
||||
architectures=esp32
|
||||
url=https://github.com/hoeken/PsychicHttp
|
||||
includes=PsychicHttp.h
|
||||
depends=ArduinoJson,ArduinoTrace,UrlEncode
|
||||
@@ -0,0 +1 @@
|
||||
<mxfile host="Electron" modified="2023-12-09T15:02:09.427Z" agent="5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) draw.io/20.8.10 Chrome/106.0.5249.199 Electron/21.3.5 Safari/537.36" etag="cOtONeHkqmkM2fyBsENB" version="20.8.10" type="device" pages="2"><diagram id="C5RBs43oDa-KdzZeNtuy" name="Request Flow">7Vxbd5s4EP41Pmf3wTkgAcaPza3Zbq/bbNM+yiAbNhi5ICd2f/0KIxmQFIypsZ1jpw+1hBCS5pv5ZgaJHryaLt4maBZ8ID6OesDwFz143QPANIDD/stqlnnNwIJ5xSQJfd6oqPga/sLiTl47D32cVhpSQiIazqqVHolj7NFKHUoS8lxtNiZR9akzNMFKxVcPRWrtQ+jTIK91waCov8PhJBBPNp1hfmWKRGM+kzRAPnkuVcGbHrxKCKH5r+niCkfZ4ol1efhr+RC9f3TevvuS/kT/Xv59//FbP+/sdptb1lNIcExbd+0/B97S/uJ9MrxvX76P0/f9+Gff5MvwhKI5XzA+WboUK5iQeezjrBezBy+fg5DirzPkZVefGWZYXUCnEb+8XiSDFcYkphwRbL3ZRXZbGE9Y0c6uhlF0RSKSrB4Dx3b2j99Vqs//srtpQh5x6Yqz+mNXGq4PX8cnnFC8KKGDr9dbTKaYJkvWRFwVIObYN21efi6QZA95XVBCkevwSsTRO1n3XUiI/eBC0gvsevkufbie3t/Yd9blDfz0bvHQ70NzG4EZVeH8hyldcoGgOSWsiiQ0IBMSo+g9ITPeriQ3Myvj2H+TqSIrjyLiPeZVt2E29NUzWIm3d9eCEtqmoCJCIxxdIu9xshqoEGhMYpx15TP15XMpBndT1LKHsYX8nnV2YYvij/VAWOF6USktRWkR0tJtrPSDDy/7XdyUFcQ9KUUJFXPnI9wObSmZJx6uUUEuP/agCaZ1kueQyhaoFrwJjhANn6r2TwdEfutnErKJrEEPbFgBPRyCahf5jPhdZYOzoSPbkDrKp6x0tNKL9XwaqYpWAODkNaU9UrXtoIpUbTtn10BtajHrRl2Cwd39/WdW8w/+Occp/T2+KwOAzfFyEqE0XRuPWulK/Ocj7I49Lc95Lh6Nu+Q5t6qpQGhqiedMoOE5awc0p52CpQjtB3MiT0x92xLdjtXeaaj2ptlQ7znq+sYFgIZbQV7f5N53WxITTch4nOJ6foKGdVB+shWMfyQKxDej5FUrQTdQ3YhA4c8b+wObVTWxljHYK9gcBWw3sT/jY/yAqBdkELpVTWxApqN52iLwu0XTMMpme4ejJ0xDD2noEkXhJGYFj8kfJ3o0rYNGpyjdr9DLaEINI8dj4Glp1HdGjt1puDiUPGdnoNKooQsXu6JRU40WP6dLLwi9OxT7EVvxs/MjOz+2uUfnRxvpaaR2it5Pi3B9/x6TXoCamH5XLpMNoC25TPtjMcuQWAxILNY2N2ANm9EhwydalpqtGCzVTErQrmSSLRfUj0um6Wp79iMfQVturoVLSd9XjJyxHnAiBqDLEbPUzoSuoOqgaWaH41Ga/ScM+Zm7f4sFZP9sr9ytR4WauLjGYzSPMrgWztvJ87cle12udWE3ZHC7K9mp+YvTi+0KMq5QccHMOyHjjXlzoUdHGBRam4LCpnQKZTo1O4su9Wus5jLOHuur8lib5vZbeKyOpST5Duix2m1VTNJVW3Z9d+Sx2rIqH6XHqqaTPuJnVnGV7+IISax1SA/jnBwyJSS/WXEbJhdAZ26luuMjxQmb0QWJP81wnAkRRdGImcFjkZ9vY9e3dPJzwQh2ugMEwqrpAoODJ4eGivxUx7KgRS9b9dCriqbKw4K2zBJpFRS2iba69O0205bm3VQt6jfSVnlnj0as9m8y1xpWktEWsNqalVw5ISN11LHjB9QgtUQBzJREJH3hxcLJc4Fra1IMe+UCoIapQZ5OYmSwEp2af1qTxQvXBXnUef8nwx6Sb+ZCt2lqQnZTdydzNVQ7Xf4AmrCnVlOOlD9Mw2ob1khGadgwEb8rAhG7GktwvA3jMA2OxXyMXQ/rCWPk2pa9PSjbE8YQNCSMzpxPCGoIg0cPPaC8o0hn2AvHzIoAwzvTw4v0MBCm+HDyVd25kyAH7cZstyE3CKU4Fm4wq6hybPNiWP5rSRTWVt3ujja0smmA0y6Tzes3HK9ub2ibHHV7vapNIZcVq+5kROcHHuTMrt0yGJc7GkCwV6VQXSnGdRQnf/yZ0fI5Bte4VPY+87H6E3iK0DwU55s6znJ7UW4OPLTcVE+YcUnAph16iJ5FVyO6g6ucmvVK8qNAbwoJhiTOZHgc0sMmi1IGOukNnQFE+4xRtNKzzD1K70TzV03PjtYh/kgjlIHV0uFSAmi5o44dLleBYp4N4ScLzwZEY/4HB2du9QVqgtMZiVN8kTLLcZaaRmpu00MR3fnJanRzULu/r+1adfZ8o90fHrfdbxtoy9sdlY52tRdLvPYRzg//FMJL45LbD7bcuzXY8d4tvSKpkcvpKpKhKlLtEY1j1ST56HJTTbLEkQaxed6W7PeONMmxqs8RX815aVxyewjr28vnfKT2HWnSkb0tOagmNd6Of9ya5LTXJAmBg272B8uaIcqvW5PUlIw4p0zprFf6VotxGzFlkrWMebW0qldVl5m/Vin717xKOcSW+cihh6I3/MI09P3VGxed21/VXuVzZ8q30PiIre10r7nbDiTh6T7kAsUOj7I2we3ddlYsvn2XA6H4giC8+R8=</diagram><diagram name="Standard Handlers" id="NiY0WBS-fA4Nieu9lBZ8">7Vtbc5s4FP41ntk+tGMQAvyYOLdNp9NOvTttn3ZkkDGNQA7It/z6FUayBcK3xNhMA34I5+iCdL5P5xwJ0gH9aHGfoMn4C/Ux6Zhdf9EBNx3TNLqmzf9kmmWucSyQK4Ik9EWljWIQvmDZUminoY/TQkVGKWHhpKj0aBxjjxV0KEnovFhtREnxqRMUYE0x8BDRtT9Cn41zrWs6G/0DDoOxfLJh9/KSCMnKYibpGPl0rqjAbQf0E0pZfhct+phkxpN2ue69/J493t0Yzy/92Xf/3v3nKviYd3Z3TJP1FBIcs9N2beZdzxCZCnuJubKlNGBCp7GPs06MDriej0OGBxPkZaVzThmuG7OIiOK1jbpcGNGYCUJwc/NC3iyMAy7CrDQkpE8JTVaPASOY/UQrRZ9fWWuW0CeslNiri5dEdIaGq+FmQ0hwGr6oMmWIKTInOFZl7IeqKHioaA60vkBphhOGFwr3BBr3mEaYJUteRZTyueVNxMoCUBBtvuEpsIVurHDUcoQSibURrPve4M9vBAWOoENPo8MPPHxAsU9w8jZiKFwwuE2uA4LSVPAkR1auTp1GRaL4CLsjr5IQnouHozoRc7sFxCwANcQMswqxugCTA1AQE3Clq2HxUZndPiJkiLynVIOQz5uVFnDBqDGNcQkBoUIkDGIuetzEnBvgOrNiyN3ulSiIQt/PHlNJjA11yn4CVjoRMWKrNmANWFyKbsVS7Dk6sOAEwBqfH8jEnz7+9yv4m4HH5+gruhe+XMWVxt/x8xSn7K8PjVmKELu+VbUUXXMIVr65LsQsGcDlUrQOXIpmXYgZGmIaTDj2r7KUJls3mdFDr4hMcVlwiyXLn6rwKxM+QSneLNTCm6WUFiH7KYMbv1dacWnTKBNkm+NgSuk08fD+OMJQEmC2w2YCC+wXcjgddAVUWIGp1CWYIBbOiplfFdDiCd9oyKercKrk3iEsdpHPW7RSU61yR3axI2Bbn3rqVew2N5PW7YqFayO8nph6jseD+oB6T5i1oX1baLfNM4b2SthARQT4OsFx6/4r3L/dvbT7tyrguktQhFu8qvA6NHOuDS89caZxn9C0xasSL/vSeNnvM73aFdH3pldWs9IroxhiofHa9Ao4uzuqOaFymkLF42i1ly6gWXTpljIy60R0sctp/Ra6cADRUqk2ySqkh/NbDnjruMr1jUJ9fpOP4KTcdf9Q7sJ3wl2zodx1zsBd/XT63wmhyG93sdt2sevIeLFdrORJe5B5YKYNL3+Q+U5PMnf5nL3xR/K8KQGodJQJwYmOMi3rNUeZbw1HcviHhiMIzxCODLPCteURqfVsVZ7t4md0hn5IdyHPdpzN93sfs1nep3sq71Pe6dvnSX+P9jfn2LoZ+pHl7YzzZ5DHsKY4nOZkv845P8+ohkw/tWxf4mwNEM6h79zqCxD68Uj7VmAHYBd/K2Dq28s/JKLbjY7ozqs/jQBbKFRzRHfgcRHdAWeI6GZjdtqnZq/7TthbTmybwt63HcdycfNZe159888B4PZ/</diagram></mxfile>
|
||||
@@ -0,0 +1,86 @@
|
||||
|
||||
#include "ChunkPrinter.h"
|
||||
|
||||
ChunkPrinter::ChunkPrinter(PsychicResponse *response, uint8_t *buffer, size_t len) :
|
||||
_response(response),
|
||||
_buffer(buffer),
|
||||
_length(len),
|
||||
_pos(0)
|
||||
{}
|
||||
|
||||
ChunkPrinter::~ChunkPrinter()
|
||||
{
|
||||
flush();
|
||||
}
|
||||
|
||||
size_t ChunkPrinter::write(uint8_t c)
|
||||
{
|
||||
esp_err_t err;
|
||||
|
||||
//if we're full, send a chunk
|
||||
if (_pos == _length)
|
||||
{
|
||||
_pos = 0;
|
||||
err = _response->sendChunk(_buffer, _length);
|
||||
|
||||
if (err != ESP_OK)
|
||||
return 0;
|
||||
}
|
||||
|
||||
_buffer[_pos] = c;
|
||||
_pos++;
|
||||
return 1;
|
||||
}
|
||||
|
||||
size_t ChunkPrinter::write(const uint8_t *buffer, size_t size)
|
||||
{
|
||||
esp_err_t err;
|
||||
size_t written = 0;
|
||||
|
||||
while (written < size)
|
||||
{
|
||||
size_t space = _length - _pos;
|
||||
size_t blockSize = std::min(space, size - written);
|
||||
|
||||
memcpy(_buffer + _pos, buffer + written, blockSize);
|
||||
_pos += blockSize;
|
||||
|
||||
if (_pos == _length)
|
||||
{
|
||||
_pos = 0;
|
||||
|
||||
if (_response->sendChunk(_buffer, _length) != ESP_OK)
|
||||
return written;
|
||||
}
|
||||
written += blockSize; //Update if sent correctly.
|
||||
}
|
||||
return written;
|
||||
}
|
||||
|
||||
void ChunkPrinter::flush()
|
||||
{
|
||||
if (_pos)
|
||||
{
|
||||
_response->sendChunk(_buffer, _pos);
|
||||
_pos = 0;
|
||||
}
|
||||
}
|
||||
|
||||
size_t ChunkPrinter::copyFrom(Stream &stream)
|
||||
{
|
||||
size_t count = 0;
|
||||
|
||||
while (stream.available()){
|
||||
|
||||
if (_pos == _length)
|
||||
{
|
||||
_response->sendChunk(_buffer, _length);
|
||||
_pos = 0;
|
||||
}
|
||||
|
||||
size_t readBytes = stream.readBytes(_buffer + _pos, _length - _pos);
|
||||
_pos += readBytes;
|
||||
count += readBytes;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
#ifndef ChunkPrinter_h
|
||||
#define ChunkPrinter_h
|
||||
|
||||
#include "PsychicResponse.h"
|
||||
#include <Print.h>
|
||||
|
||||
class ChunkPrinter : public Print
|
||||
{
|
||||
private:
|
||||
PsychicResponse *_response;
|
||||
uint8_t *_buffer;
|
||||
size_t _length;
|
||||
size_t _pos;
|
||||
|
||||
public:
|
||||
ChunkPrinter(PsychicResponse *response, uint8_t *buffer, size_t len);
|
||||
~ChunkPrinter();
|
||||
|
||||
size_t write(uint8_t c) override;
|
||||
size_t write(const uint8_t *buffer, size_t size) override;
|
||||
|
||||
size_t copyFrom(Stream &stream);
|
||||
|
||||
void flush() override;
|
||||
};
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,77 @@
|
||||
#include "PsychicClient.h"
|
||||
#include "PsychicHttpServer.h"
|
||||
#include <lwip/sockets.h>
|
||||
|
||||
PsychicClient::PsychicClient(httpd_handle_t server, int socket) : _server(server),
|
||||
_socket(socket),
|
||||
_friend(NULL),
|
||||
isNew(false)
|
||||
{
|
||||
}
|
||||
|
||||
PsychicClient::~PsychicClient()
|
||||
{
|
||||
}
|
||||
|
||||
httpd_handle_t PsychicClient::server()
|
||||
{
|
||||
return _server;
|
||||
}
|
||||
|
||||
int PsychicClient::socket()
|
||||
{
|
||||
return _socket;
|
||||
}
|
||||
|
||||
// I'm not sure this is entirely safe to call. I was having issues with race conditions when highly loaded using this.
|
||||
esp_err_t PsychicClient::close()
|
||||
{
|
||||
esp_err_t err = httpd_sess_trigger_close(_server, _socket);
|
||||
// PsychicHttpServer::closeCallback(_server, _socket); // call this immediately so the client is taken off the list.
|
||||
|
||||
return err;
|
||||
}
|
||||
|
||||
IPAddress PsychicClient::localIP()
|
||||
{
|
||||
IPAddress address(0, 0, 0, 0);
|
||||
|
||||
char ipstr[INET6_ADDRSTRLEN];
|
||||
struct sockaddr_in6 addr; // esp_http_server uses IPv6 addressing
|
||||
socklen_t addr_size = sizeof(addr);
|
||||
|
||||
if (getsockname(_socket, (struct sockaddr *)&addr, &addr_size) < 0)
|
||||
{
|
||||
ESP_LOGE(PH_TAG, "Error getting client IP");
|
||||
return address;
|
||||
}
|
||||
|
||||
// Convert to IPv4 string
|
||||
inet_ntop(AF_INET, &addr.sin6_addr.un.u32_addr[3], ipstr, sizeof(ipstr));
|
||||
ESP_LOGI(PH_TAG, "Client Local IP => %s", ipstr);
|
||||
address.fromString(ipstr);
|
||||
|
||||
return address;
|
||||
}
|
||||
|
||||
IPAddress PsychicClient::remoteIP()
|
||||
{
|
||||
IPAddress address(0, 0, 0, 0);
|
||||
|
||||
char ipstr[INET6_ADDRSTRLEN];
|
||||
struct sockaddr_in6 addr; // esp_http_server uses IPv6 addressing
|
||||
socklen_t addr_size = sizeof(addr);
|
||||
|
||||
if (getpeername(_socket, (struct sockaddr *)&addr, &addr_size) < 0)
|
||||
{
|
||||
ESP_LOGE(PH_TAG, "Error getting client IP");
|
||||
return address;
|
||||
}
|
||||
|
||||
// Convert to IPv4 string
|
||||
inet_ntop(AF_INET, &addr.sin6_addr.un.u32_addr[3], ipstr, sizeof(ipstr));
|
||||
ESP_LOGV(PH_TAG, "Client Remote IP => %s", ipstr);
|
||||
address.fromString(ipstr);
|
||||
|
||||
return address;
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
#ifndef PsychicClient_h
|
||||
#define PsychicClient_h
|
||||
|
||||
#include "PsychicCore.h"
|
||||
|
||||
/*
|
||||
* PsychicClient :: Generic wrapper around the ESP-IDF socket
|
||||
*/
|
||||
|
||||
class PsychicClient {
|
||||
protected:
|
||||
httpd_handle_t _server;
|
||||
int _socket;
|
||||
|
||||
public:
|
||||
PsychicClient(httpd_handle_t server, int socket);
|
||||
~PsychicClient();
|
||||
|
||||
//no idea if this is the right way to do it or not, but lets see.
|
||||
//pointer to our derived class (eg. PsychicWebSocketConnection)
|
||||
void *_friend;
|
||||
|
||||
bool isNew = false;
|
||||
|
||||
bool operator==(PsychicClient& rhs) const { return _socket == rhs.socket(); }
|
||||
|
||||
httpd_handle_t server();
|
||||
int socket();
|
||||
esp_err_t close();
|
||||
|
||||
IPAddress localIP();
|
||||
IPAddress remoteIP();
|
||||
};
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,108 @@
|
||||
#ifndef PsychicCore_h
|
||||
#define PsychicCore_h
|
||||
|
||||
#define PH_TAG "psychic"
|
||||
|
||||
//version numbers
|
||||
#define PSYCHIC_HTTP_VERSION_MAJOR 1
|
||||
#define PSYCHIC_HTTP_VERSION_MINOR 1
|
||||
#define PSYCHIC_HTTP_VERSION_PATCH 0
|
||||
|
||||
#ifndef MAX_COOKIE_SIZE
|
||||
#define MAX_COOKIE_SIZE 512
|
||||
#endif
|
||||
|
||||
#ifndef FILE_CHUNK_SIZE
|
||||
#define FILE_CHUNK_SIZE 8*1024
|
||||
#endif
|
||||
|
||||
#ifndef STREAM_CHUNK_SIZE
|
||||
#define STREAM_CHUNK_SIZE 1024
|
||||
#endif
|
||||
|
||||
#ifndef MAX_UPLOAD_SIZE
|
||||
#define MAX_UPLOAD_SIZE (2048*1024) // 2MB
|
||||
#endif
|
||||
|
||||
#ifndef MAX_REQUEST_BODY_SIZE
|
||||
#define MAX_REQUEST_BODY_SIZE (16*1024) //16K
|
||||
#endif
|
||||
|
||||
#ifdef ARDUINO
|
||||
#include <Arduino.h>
|
||||
#include <ArduinoTrace.h>
|
||||
#endif
|
||||
|
||||
#include <esp_http_server.h>
|
||||
#include <map>
|
||||
#include <list>
|
||||
#include <libb64/cencode.h>
|
||||
#include "esp_random.h"
|
||||
#include "MD5Builder.h"
|
||||
#include <UrlEncode.h>
|
||||
#include "FS.h"
|
||||
#include <ArduinoJson.h>
|
||||
|
||||
enum HTTPAuthMethod { BASIC_AUTH, DIGEST_AUTH };
|
||||
|
||||
String urlDecode(const char* encoded);
|
||||
|
||||
class PsychicHttpServer;
|
||||
class PsychicRequest;
|
||||
class PsychicWebSocketRequest;
|
||||
class PsychicClient;
|
||||
|
||||
//filter function definition
|
||||
typedef std::function<bool(PsychicRequest *request)> PsychicRequestFilterFunction;
|
||||
|
||||
//client connect callback
|
||||
typedef std::function<void(PsychicClient *client)> PsychicClientCallback;
|
||||
|
||||
//callback definitions
|
||||
typedef std::function<esp_err_t(PsychicRequest *request)> PsychicHttpRequestCallback;
|
||||
typedef std::function<esp_err_t(PsychicRequest *request, JsonVariant &json)> PsychicJsonRequestCallback;
|
||||
|
||||
struct HTTPHeader {
|
||||
char * field;
|
||||
char * value;
|
||||
};
|
||||
|
||||
class DefaultHeaders {
|
||||
std::list<HTTPHeader> _headers;
|
||||
|
||||
public:
|
||||
DefaultHeaders() {}
|
||||
|
||||
void addHeader(const String& field, const String& value)
|
||||
{
|
||||
addHeader(field.c_str(), value.c_str());
|
||||
}
|
||||
|
||||
void addHeader(const char * field, const char * value)
|
||||
{
|
||||
HTTPHeader header;
|
||||
|
||||
//these are just going to stick around forever.
|
||||
header.field =(char *)malloc(strlen(field)+1);
|
||||
header.value = (char *)malloc(strlen(value)+1);
|
||||
|
||||
strlcpy(header.field, field, strlen(field)+1);
|
||||
strlcpy(header.value, value, strlen(value)+1);
|
||||
|
||||
_headers.push_back(header);
|
||||
}
|
||||
|
||||
const std::list<HTTPHeader>& getHeaders() { return _headers; }
|
||||
|
||||
//delete the copy constructor, singleton class
|
||||
DefaultHeaders(DefaultHeaders const &) = delete;
|
||||
DefaultHeaders &operator=(DefaultHeaders const &) = delete;
|
||||
|
||||
//single static class interface
|
||||
static DefaultHeaders &Instance() {
|
||||
static DefaultHeaders instance;
|
||||
return instance;
|
||||
}
|
||||
};
|
||||
|
||||
#endif //PsychicCore_h
|
||||
@@ -0,0 +1,90 @@
|
||||
#include "PsychicEndpoint.h"
|
||||
#include "PsychicHttpServer.h"
|
||||
|
||||
PsychicEndpoint::PsychicEndpoint() :
|
||||
_server(NULL),
|
||||
_uri(""),
|
||||
_method(HTTP_GET),
|
||||
_handler(NULL)
|
||||
{
|
||||
}
|
||||
|
||||
PsychicEndpoint::PsychicEndpoint(PsychicHttpServer *server, http_method method, const char * uri) :
|
||||
_server(server),
|
||||
_uri(uri),
|
||||
_method(method),
|
||||
_handler(NULL)
|
||||
{
|
||||
}
|
||||
|
||||
PsychicEndpoint * PsychicEndpoint::setHandler(PsychicHandler *handler)
|
||||
{
|
||||
//clean up old / default handler
|
||||
if (_handler != NULL)
|
||||
delete _handler;
|
||||
|
||||
//get our new pointer
|
||||
_handler = handler;
|
||||
|
||||
//keep a pointer to the server
|
||||
_handler->_server = _server;
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
PsychicHandler * PsychicEndpoint::handler()
|
||||
{
|
||||
return _handler;
|
||||
}
|
||||
|
||||
String PsychicEndpoint::uri() {
|
||||
return _uri;
|
||||
}
|
||||
|
||||
esp_err_t PsychicEndpoint::requestCallback(httpd_req_t *req)
|
||||
{
|
||||
#ifdef ENABLE_ASYNC
|
||||
if (is_on_async_worker_thread() == false) {
|
||||
if (submit_async_req(req, PsychicEndpoint::requestCallback) == ESP_OK) {
|
||||
return ESP_OK;
|
||||
} else {
|
||||
httpd_resp_set_status(req, "503 Busy");
|
||||
httpd_resp_sendstr(req, "No workers available. Server busy.</div>");
|
||||
return ESP_OK;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
PsychicEndpoint *self = (PsychicEndpoint *)req->user_ctx;
|
||||
PsychicHandler *handler = self->handler();
|
||||
PsychicRequest request(self->_server, req);
|
||||
|
||||
//make sure we have a handler
|
||||
if (handler != NULL)
|
||||
{
|
||||
if (handler->filter(&request) && handler->canHandle(&request))
|
||||
{
|
||||
//check our credentials
|
||||
if (handler->needsAuthentication(&request))
|
||||
return handler->authenticate(&request);
|
||||
|
||||
//pass it to our handler
|
||||
return handler->handleRequest(&request);
|
||||
}
|
||||
//pass it to our generic handlers
|
||||
else
|
||||
return PsychicHttpServer::notFoundHandler(req, HTTPD_500_INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
else
|
||||
return request.reply(500, "text/html", "No handler registered.");
|
||||
}
|
||||
|
||||
PsychicEndpoint* PsychicEndpoint::setFilter(PsychicRequestFilterFunction fn) {
|
||||
_handler->setFilter(fn);
|
||||
return this;
|
||||
}
|
||||
|
||||
PsychicEndpoint* PsychicEndpoint::setAuthentication(const char *username, const char *password, HTTPAuthMethod method, const char *realm, const char *authFailMsg) {
|
||||
_handler->setAuthentication(username, password, method, realm, authFailMsg);
|
||||
return this;
|
||||
};
|
||||
@@ -0,0 +1,37 @@
|
||||
#ifndef PsychicEndpoint_h
|
||||
#define PsychicEndpoint_h
|
||||
|
||||
#include "PsychicCore.h"
|
||||
|
||||
class PsychicHandler;
|
||||
|
||||
#ifdef ENABLE_ASYNC
|
||||
#include "async_worker.h"
|
||||
#endif
|
||||
|
||||
class PsychicEndpoint
|
||||
{
|
||||
friend PsychicHttpServer;
|
||||
|
||||
private:
|
||||
PsychicHttpServer *_server;
|
||||
String _uri;
|
||||
http_method _method;
|
||||
PsychicHandler *_handler;
|
||||
|
||||
public:
|
||||
PsychicEndpoint();
|
||||
PsychicEndpoint(PsychicHttpServer *server, http_method method, const char * uri);
|
||||
|
||||
PsychicEndpoint *setHandler(PsychicHandler *handler);
|
||||
PsychicHandler *handler();
|
||||
|
||||
PsychicEndpoint* setFilter(PsychicRequestFilterFunction fn);
|
||||
PsychicEndpoint* setAuthentication(const char *username, const char *password, HTTPAuthMethod method = BASIC_AUTH, const char *realm = "", const char *authFailMsg = "");
|
||||
|
||||
String uri();
|
||||
|
||||
static esp_err_t requestCallback(httpd_req_t *req);
|
||||
};
|
||||
|
||||
#endif // PsychicEndpoint_h
|
||||
@@ -0,0 +1,227 @@
|
||||
/*
|
||||
Asynchronous WebServer library for Espressif MCUs
|
||||
|
||||
Copyright (c) 2016 Hristo Gochkov. All rights reserved.
|
||||
|
||||
This library is free software; you can redistribute it and/or
|
||||
modify it under the terms of the GNU Lesser General Public
|
||||
License as published by the Free Software Foundation; either
|
||||
version 2.1 of the License, or (at your option) any later version.
|
||||
|
||||
This library is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
Lesser General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Lesser General Public
|
||||
License along with this library; if not, write to the Free Software
|
||||
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
*/
|
||||
|
||||
#include "PsychicEventSource.h"
|
||||
|
||||
/*****************************************/
|
||||
// PsychicEventSource - Handler
|
||||
/*****************************************/
|
||||
|
||||
PsychicEventSource::PsychicEventSource() :
|
||||
PsychicHandler(),
|
||||
_onOpen(NULL),
|
||||
_onClose(NULL)
|
||||
{}
|
||||
|
||||
PsychicEventSource::~PsychicEventSource() {
|
||||
}
|
||||
|
||||
PsychicEventSourceClient * PsychicEventSource::getClient(int socket)
|
||||
{
|
||||
PsychicClient *client = PsychicHandler::getClient(socket);
|
||||
|
||||
if (client == NULL)
|
||||
return NULL;
|
||||
|
||||
return (PsychicEventSourceClient *)client->_friend;
|
||||
}
|
||||
|
||||
PsychicEventSourceClient * PsychicEventSource::getClient(PsychicClient *client) {
|
||||
return getClient(client->socket());
|
||||
}
|
||||
|
||||
esp_err_t PsychicEventSource::handleRequest(PsychicRequest *request)
|
||||
{
|
||||
//start our open ended HTTP response
|
||||
PsychicEventSourceResponse response(request);
|
||||
esp_err_t err = response.send();
|
||||
|
||||
//lookup our client
|
||||
PsychicClient *client = checkForNewClient(request->client());
|
||||
if (client->isNew)
|
||||
{
|
||||
//did we get our last id?
|
||||
if(request->hasHeader("Last-Event-ID"))
|
||||
{
|
||||
PsychicEventSourceClient *buddy = getClient(client);
|
||||
buddy->_lastId = atoi(request->header("Last-Event-ID").c_str());
|
||||
}
|
||||
|
||||
//let our handler know.
|
||||
openCallback(client);
|
||||
}
|
||||
|
||||
return err;
|
||||
}
|
||||
|
||||
PsychicEventSource * PsychicEventSource::onOpen(PsychicEventSourceClientCallback fn) {
|
||||
_onOpen = fn;
|
||||
return this;
|
||||
}
|
||||
|
||||
PsychicEventSource * PsychicEventSource::onClose(PsychicEventSourceClientCallback fn) {
|
||||
_onClose = fn;
|
||||
return this;
|
||||
}
|
||||
|
||||
void PsychicEventSource::addClient(PsychicClient *client) {
|
||||
client->_friend = new PsychicEventSourceClient(client);
|
||||
PsychicHandler::addClient(client);
|
||||
}
|
||||
|
||||
void PsychicEventSource::removeClient(PsychicClient *client) {
|
||||
PsychicHandler::removeClient(client);
|
||||
delete (PsychicEventSourceClient*)client->_friend;
|
||||
client->_friend = NULL;
|
||||
}
|
||||
|
||||
void PsychicEventSource::openCallback(PsychicClient *client) {
|
||||
PsychicEventSourceClient *buddy = getClient(client);
|
||||
if (buddy == NULL)
|
||||
{
|
||||
TRACE();
|
||||
return;
|
||||
}
|
||||
|
||||
if (_onOpen != NULL)
|
||||
_onOpen(buddy);
|
||||
}
|
||||
|
||||
void PsychicEventSource::closeCallback(PsychicClient *client) {
|
||||
PsychicEventSourceClient *buddy = getClient(client);
|
||||
if (buddy == NULL)
|
||||
{
|
||||
TRACE();
|
||||
return;
|
||||
}
|
||||
|
||||
if (_onClose != NULL)
|
||||
_onClose(getClient(buddy));
|
||||
}
|
||||
|
||||
void PsychicEventSource::send(const char *message, const char *event, uint32_t id, uint32_t reconnect)
|
||||
{
|
||||
String ev = generateEventMessage(message, event, id, reconnect);
|
||||
for(PsychicClient *c : _clients) {
|
||||
((PsychicEventSourceClient*)c->_friend)->sendEvent(ev.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
/*****************************************/
|
||||
// PsychicEventSourceClient
|
||||
/*****************************************/
|
||||
|
||||
PsychicEventSourceClient::PsychicEventSourceClient(PsychicClient *client) :
|
||||
PsychicClient(client->server(), client->socket()),
|
||||
_lastId(0)
|
||||
{
|
||||
}
|
||||
|
||||
PsychicEventSourceClient::~PsychicEventSourceClient(){
|
||||
}
|
||||
|
||||
void PsychicEventSourceClient::send(const char *message, const char *event, uint32_t id, uint32_t reconnect){
|
||||
String ev = generateEventMessage(message, event, id, reconnect);
|
||||
sendEvent(ev.c_str());
|
||||
}
|
||||
|
||||
void PsychicEventSourceClient::sendEvent(const char *event) {
|
||||
int result;
|
||||
do {
|
||||
result = httpd_socket_send(this->server(), this->socket(), event, strlen(event), 0);
|
||||
} while (result == HTTPD_SOCK_ERR_TIMEOUT);
|
||||
|
||||
//if (result < 0)
|
||||
//error log here
|
||||
}
|
||||
|
||||
/*****************************************/
|
||||
// PsychicEventSourceResponse
|
||||
/*****************************************/
|
||||
|
||||
PsychicEventSourceResponse::PsychicEventSourceResponse(PsychicRequest *request)
|
||||
: PsychicResponse(request)
|
||||
{
|
||||
}
|
||||
|
||||
esp_err_t PsychicEventSourceResponse::send() {
|
||||
|
||||
//build our main header
|
||||
String out = String();
|
||||
out.concat("HTTP/1.1 200 OK\r\n");
|
||||
out.concat("Content-Type: text/event-stream\r\n");
|
||||
out.concat("Cache-Control: no-cache\r\n");
|
||||
out.concat("Connection: keep-alive\r\n");
|
||||
|
||||
//get our global headers out of the way first
|
||||
for (HTTPHeader header : DefaultHeaders::Instance().getHeaders())
|
||||
out.concat(String(header.field) + ": " + String(header.value) + "\r\n");
|
||||
|
||||
//separator
|
||||
out.concat("\r\n");
|
||||
|
||||
int result;
|
||||
do {
|
||||
result = httpd_send(_request->request(), out.c_str(), out.length());
|
||||
} while (result == HTTPD_SOCK_ERR_TIMEOUT);
|
||||
|
||||
if (result < 0)
|
||||
ESP_LOGE(PH_TAG, "EventSource send failed with %s", esp_err_to_name(result));
|
||||
|
||||
if (result > 0)
|
||||
return ESP_OK;
|
||||
else
|
||||
return ESP_ERR_HTTPD_RESP_SEND;
|
||||
}
|
||||
|
||||
/*****************************************/
|
||||
// Event Message Generator
|
||||
/*****************************************/
|
||||
|
||||
String generateEventMessage(const char *message, const char *event, uint32_t id, uint32_t reconnect) {
|
||||
String ev = "";
|
||||
|
||||
if(reconnect){
|
||||
ev += "retry: ";
|
||||
ev += String(reconnect);
|
||||
ev += "\r\n";
|
||||
}
|
||||
|
||||
if(id){
|
||||
ev += "id: ";
|
||||
ev += String(id);
|
||||
ev += "\r\n";
|
||||
}
|
||||
|
||||
if(event != NULL){
|
||||
ev += "event: ";
|
||||
ev += String(event);
|
||||
ev += "\r\n";
|
||||
}
|
||||
|
||||
if(message != NULL){
|
||||
ev += "data: ";
|
||||
ev += String(message);
|
||||
ev += "\r\n";
|
||||
}
|
||||
ev += "\r\n";
|
||||
|
||||
return ev;
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
/*
|
||||
Asynchronous WebServer library for Espressif MCUs
|
||||
|
||||
Copyright (c) 2016 Hristo Gochkov. All rights reserved.
|
||||
|
||||
This library is free software; you can redistribute it and/or
|
||||
modify it under the terms of the GNU Lesser General Public
|
||||
License as published by the Free Software Foundation; either
|
||||
version 2.1 of the License, or (at your option) any later version.
|
||||
|
||||
This library is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
Lesser General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Lesser General Public
|
||||
License along with this library; if not, write to the Free Software
|
||||
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
*/
|
||||
#ifndef PsychicEventSource_H_
|
||||
#define PsychicEventSource_H_
|
||||
|
||||
#include "PsychicCore.h"
|
||||
#include "PsychicHandler.h"
|
||||
#include "PsychicClient.h"
|
||||
#include "PsychicResponse.h"
|
||||
|
||||
class PsychicEventSource;
|
||||
class PsychicEventSourceResponse;
|
||||
class PsychicEventSourceClient;
|
||||
class PsychicResponse;
|
||||
|
||||
typedef std::function<void(PsychicEventSourceClient *client)> PsychicEventSourceClientCallback;
|
||||
|
||||
class PsychicEventSourceClient : public PsychicClient {
|
||||
friend PsychicEventSource;
|
||||
|
||||
protected:
|
||||
uint32_t _lastId;
|
||||
|
||||
public:
|
||||
PsychicEventSourceClient(PsychicClient *client);
|
||||
~PsychicEventSourceClient();
|
||||
|
||||
uint32_t lastId() const { return _lastId; }
|
||||
void send(const char *message, const char *event=NULL, uint32_t id=0, uint32_t reconnect=0);
|
||||
void sendEvent(const char *event);
|
||||
};
|
||||
|
||||
class PsychicEventSource : public PsychicHandler {
|
||||
private:
|
||||
PsychicEventSourceClientCallback _onOpen;
|
||||
PsychicEventSourceClientCallback _onClose;
|
||||
|
||||
public:
|
||||
PsychicEventSource();
|
||||
~PsychicEventSource();
|
||||
|
||||
PsychicEventSourceClient * getClient(int socket) override;
|
||||
PsychicEventSourceClient * getClient(PsychicClient *client) override;
|
||||
void addClient(PsychicClient *client) override;
|
||||
void removeClient(PsychicClient *client) override;
|
||||
void openCallback(PsychicClient *client) override;
|
||||
void closeCallback(PsychicClient *client) override;
|
||||
|
||||
PsychicEventSource *onOpen(PsychicEventSourceClientCallback fn);
|
||||
PsychicEventSource *onClose(PsychicEventSourceClientCallback fn);
|
||||
|
||||
esp_err_t handleRequest(PsychicRequest *request) override final;
|
||||
|
||||
void send(const char *message, const char *event=NULL, uint32_t id=0, uint32_t reconnect=0);
|
||||
};
|
||||
|
||||
class PsychicEventSourceResponse: public PsychicResponse {
|
||||
public:
|
||||
PsychicEventSourceResponse(PsychicRequest *request);
|
||||
virtual esp_err_t send() override;
|
||||
};
|
||||
|
||||
String generateEventMessage(const char *message, const char *event, uint32_t id, uint32_t reconnect);
|
||||
|
||||
#endif /* PsychicEventSource_H_ */
|
||||
@@ -0,0 +1,152 @@
|
||||
#include "PsychicFileResponse.h"
|
||||
#include "PsychicResponse.h"
|
||||
#include "PsychicRequest.h"
|
||||
|
||||
|
||||
PsychicFileResponse::PsychicFileResponse(PsychicRequest *request, FS &fs, const String& path, const String& contentType, bool download)
|
||||
: PsychicResponse(request) {
|
||||
//_code = 200;
|
||||
String _path(path);
|
||||
|
||||
if(!download && !fs.exists(_path) && fs.exists(_path+".gz")){
|
||||
_path = _path+".gz";
|
||||
addHeader("Content-Encoding", "gzip");
|
||||
}
|
||||
|
||||
_content = fs.open(_path, "r");
|
||||
_contentLength = _content.size();
|
||||
|
||||
if(contentType == "")
|
||||
_setContentType(path);
|
||||
else
|
||||
setContentType(contentType.c_str());
|
||||
|
||||
int filenameStart = path.lastIndexOf('/') + 1;
|
||||
char buf[26+path.length()-filenameStart];
|
||||
char* filename = (char*)path.c_str() + filenameStart;
|
||||
|
||||
if(download) {
|
||||
// set filename and force download
|
||||
snprintf(buf, sizeof (buf), "attachment; filename=\"%s\"", filename);
|
||||
} else {
|
||||
// set filename and force rendering
|
||||
snprintf(buf, sizeof (buf), "inline; filename=\"%s\"", filename);
|
||||
}
|
||||
addHeader("Content-Disposition", buf);
|
||||
}
|
||||
|
||||
PsychicFileResponse::PsychicFileResponse(PsychicRequest *request, File content, const String& path, const String& contentType, bool download)
|
||||
: PsychicResponse(request) {
|
||||
String _path(path);
|
||||
|
||||
if(!download && String(content.name()).endsWith(".gz") && !path.endsWith(".gz")){
|
||||
addHeader("Content-Encoding", "gzip");
|
||||
}
|
||||
|
||||
_content = content;
|
||||
_contentLength = _content.size();
|
||||
|
||||
if(contentType == "")
|
||||
_setContentType(path);
|
||||
else
|
||||
setContentType(contentType.c_str());
|
||||
|
||||
int filenameStart = path.lastIndexOf('/') + 1;
|
||||
char buf[26+path.length()-filenameStart];
|
||||
char* filename = (char*)path.c_str() + filenameStart;
|
||||
|
||||
if(download) {
|
||||
snprintf(buf, sizeof (buf), "attachment; filename=\"%s\"", filename);
|
||||
} else {
|
||||
snprintf(buf, sizeof (buf), "inline; filename=\"%s\"", filename);
|
||||
}
|
||||
addHeader("Content-Disposition", buf);
|
||||
}
|
||||
|
||||
PsychicFileResponse::~PsychicFileResponse()
|
||||
{
|
||||
if(_content)
|
||||
_content.close();
|
||||
}
|
||||
|
||||
void PsychicFileResponse::_setContentType(const String& path){
|
||||
const char *_contentType;
|
||||
|
||||
if (path.endsWith(".html")) _contentType = "text/html";
|
||||
else if (path.endsWith(".htm")) _contentType = "text/html";
|
||||
else if (path.endsWith(".css")) _contentType = "text/css";
|
||||
else if (path.endsWith(".json")) _contentType = "application/json";
|
||||
else if (path.endsWith(".js")) _contentType = "application/javascript";
|
||||
else if (path.endsWith(".png")) _contentType = "image/png";
|
||||
else if (path.endsWith(".gif")) _contentType = "image/gif";
|
||||
else if (path.endsWith(".jpg")) _contentType = "image/jpeg";
|
||||
else if (path.endsWith(".ico")) _contentType = "image/x-icon";
|
||||
else if (path.endsWith(".svg")) _contentType = "image/svg+xml";
|
||||
else if (path.endsWith(".eot")) _contentType = "font/eot";
|
||||
else if (path.endsWith(".woff")) _contentType = "font/woff";
|
||||
else if (path.endsWith(".woff2")) _contentType = "font/woff2";
|
||||
else if (path.endsWith(".ttf")) _contentType = "font/ttf";
|
||||
else if (path.endsWith(".xml")) _contentType = "text/xml";
|
||||
else if (path.endsWith(".pdf")) _contentType = "application/pdf";
|
||||
else if (path.endsWith(".zip")) _contentType = "application/zip";
|
||||
else if(path.endsWith(".gz")) _contentType = "application/x-gzip";
|
||||
else _contentType = "text/plain";
|
||||
|
||||
setContentType(_contentType);
|
||||
}
|
||||
|
||||
esp_err_t PsychicFileResponse::send()
|
||||
{
|
||||
esp_err_t err = ESP_OK;
|
||||
|
||||
//just send small files directly
|
||||
size_t size = getContentLength();
|
||||
if (size < FILE_CHUNK_SIZE)
|
||||
{
|
||||
uint8_t *buffer = (uint8_t *)malloc(size);
|
||||
int readSize = _content.readBytes((char *)buffer, size);
|
||||
|
||||
this->setContent(buffer, size);
|
||||
err = PsychicResponse::send();
|
||||
|
||||
free(buffer);
|
||||
}
|
||||
else
|
||||
{
|
||||
/* Retrieve the pointer to scratch buffer for temporary storage */
|
||||
char *chunk = (char *)malloc(FILE_CHUNK_SIZE);
|
||||
if (chunk == NULL)
|
||||
{
|
||||
/* Respond with 500 Internal Server Error */
|
||||
httpd_resp_send_err(this->_request->request(), HTTPD_500_INTERNAL_SERVER_ERROR, "Unable to allocate memory.");
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
this->sendHeaders();
|
||||
|
||||
size_t chunksize;
|
||||
do {
|
||||
/* Read file in chunks into the scratch buffer */
|
||||
chunksize = _content.readBytes(chunk, FILE_CHUNK_SIZE);
|
||||
if (chunksize > 0)
|
||||
{
|
||||
err = this->sendChunk((uint8_t *)chunk, chunksize);
|
||||
if (err != ESP_OK)
|
||||
break;
|
||||
}
|
||||
|
||||
/* Keep looping till the whole file is sent */
|
||||
} while (chunksize != 0);
|
||||
|
||||
//keep track of our memory
|
||||
free(chunk);
|
||||
|
||||
if (err == ESP_OK)
|
||||
{
|
||||
ESP_LOGI(PH_TAG, "File sending complete");
|
||||
this->finishChunking();
|
||||
}
|
||||
}
|
||||
|
||||
return err;
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
#ifndef PsychicFileResponse_h
|
||||
#define PsychicFileResponse_h
|
||||
|
||||
#include "PsychicCore.h"
|
||||
#include "PsychicResponse.h"
|
||||
|
||||
class PsychicRequest;
|
||||
|
||||
class PsychicFileResponse: public PsychicResponse
|
||||
{
|
||||
using File = fs::File;
|
||||
using FS = fs::FS;
|
||||
private:
|
||||
File _content;
|
||||
void _setContentType(const String& path);
|
||||
public:
|
||||
PsychicFileResponse(PsychicRequest *request, FS &fs, const String& path, const String& contentType=String(), bool download=false);
|
||||
PsychicFileResponse(PsychicRequest *request, File content, const String& path, const String& contentType=String(), bool download=false);
|
||||
~PsychicFileResponse();
|
||||
esp_err_t send();
|
||||
};
|
||||
|
||||
#endif // PsychicFileResponse_h
|
||||
@@ -0,0 +1,103 @@
|
||||
#include "PsychicHandler.h"
|
||||
|
||||
PsychicHandler::PsychicHandler() :
|
||||
_filter(NULL),
|
||||
_server(NULL),
|
||||
_username(""),
|
||||
_password(""),
|
||||
_method(DIGEST_AUTH),
|
||||
_realm(""),
|
||||
_authFailMsg("")
|
||||
{}
|
||||
|
||||
PsychicHandler::~PsychicHandler() {
|
||||
// actual PsychicClient deletion handled by PsychicServer
|
||||
// for (PsychicClient *client : _clients)
|
||||
// delete(client);
|
||||
_clients.clear();
|
||||
}
|
||||
|
||||
PsychicHandler* PsychicHandler::setFilter(PsychicRequestFilterFunction fn) {
|
||||
_filter = fn;
|
||||
return this;
|
||||
}
|
||||
|
||||
bool PsychicHandler::filter(PsychicRequest *request){
|
||||
return _filter == NULL || _filter(request);
|
||||
}
|
||||
|
||||
PsychicHandler* PsychicHandler::setAuthentication(const char *username, const char *password, HTTPAuthMethod method, const char *realm, const char *authFailMsg) {
|
||||
_username = String(username);
|
||||
_password = String(password);
|
||||
_method = method;
|
||||
_realm = String(realm);
|
||||
_authFailMsg = String(authFailMsg);
|
||||
return this;
|
||||
};
|
||||
|
||||
bool PsychicHandler::needsAuthentication(PsychicRequest *request) {
|
||||
return (_username != "" && _password != "") && !request->authenticate(_username.c_str(), _password.c_str());
|
||||
}
|
||||
|
||||
esp_err_t PsychicHandler::authenticate(PsychicRequest *request) {
|
||||
return request->requestAuthentication(_method, _realm.c_str(), _authFailMsg.c_str());
|
||||
}
|
||||
|
||||
PsychicClient * PsychicHandler::checkForNewClient(PsychicClient *client)
|
||||
{
|
||||
PsychicClient *c = PsychicHandler::getClient(client);
|
||||
if (c == NULL)
|
||||
{
|
||||
c = client;
|
||||
addClient(c);
|
||||
c->isNew = true;
|
||||
}
|
||||
else
|
||||
c->isNew = false;
|
||||
|
||||
return c;
|
||||
}
|
||||
|
||||
void PsychicHandler::checkForClosedClient(PsychicClient *client)
|
||||
{
|
||||
if (hasClient(client))
|
||||
{
|
||||
closeCallback(client);
|
||||
removeClient(client);
|
||||
}
|
||||
}
|
||||
|
||||
void PsychicHandler::addClient(PsychicClient *client) {
|
||||
_clients.push_back(client);
|
||||
}
|
||||
|
||||
void PsychicHandler::removeClient(PsychicClient *client) {
|
||||
_clients.remove(client);
|
||||
}
|
||||
|
||||
PsychicClient * PsychicHandler::getClient(int socket)
|
||||
{
|
||||
//make sure the server has it too.
|
||||
if (!_server->hasClient(socket))
|
||||
return NULL;
|
||||
|
||||
//what about us?
|
||||
for (PsychicClient *client : _clients)
|
||||
if (client->socket() == socket)
|
||||
return client;
|
||||
|
||||
//nothing found.
|
||||
return NULL;
|
||||
}
|
||||
|
||||
PsychicClient * PsychicHandler::getClient(PsychicClient *client) {
|
||||
return PsychicHandler::getClient(client->socket());
|
||||
}
|
||||
|
||||
bool PsychicHandler::hasClient(PsychicClient *socket) {
|
||||
return PsychicHandler::getClient(socket) != NULL;
|
||||
}
|
||||
|
||||
const std::list<PsychicClient*>& PsychicHandler::getClientList() {
|
||||
return _clients;
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
#ifndef PsychicHandler_h
|
||||
#define PsychicHandler_h
|
||||
|
||||
#include "PsychicCore.h"
|
||||
#include "PsychicRequest.h"
|
||||
|
||||
class PsychicEndpoint;
|
||||
class PsychicHttpServer;
|
||||
|
||||
/*
|
||||
* HANDLER :: Can be attached to any endpoint or as a generic request handler.
|
||||
*/
|
||||
|
||||
class PsychicHandler {
|
||||
friend PsychicEndpoint;
|
||||
|
||||
protected:
|
||||
PsychicRequestFilterFunction _filter;
|
||||
PsychicHttpServer *_server;
|
||||
|
||||
String _username;
|
||||
String _password;
|
||||
HTTPAuthMethod _method;
|
||||
String _realm;
|
||||
String _authFailMsg;
|
||||
|
||||
std::list<PsychicClient*> _clients;
|
||||
|
||||
public:
|
||||
PsychicHandler();
|
||||
~PsychicHandler();
|
||||
|
||||
PsychicHandler* setFilter(PsychicRequestFilterFunction fn);
|
||||
bool filter(PsychicRequest *request);
|
||||
|
||||
PsychicHandler* setAuthentication(const char *username, const char *password, HTTPAuthMethod method = BASIC_AUTH, const char *realm = "", const char *authFailMsg = "");
|
||||
bool needsAuthentication(PsychicRequest *request);
|
||||
esp_err_t authenticate(PsychicRequest *request);
|
||||
|
||||
virtual bool isWebSocket() { return false; };
|
||||
|
||||
PsychicClient * checkForNewClient(PsychicClient *client);
|
||||
void checkForClosedClient(PsychicClient *client);
|
||||
|
||||
virtual void addClient(PsychicClient *client);
|
||||
virtual void removeClient(PsychicClient *client);
|
||||
virtual PsychicClient * getClient(int socket);
|
||||
virtual PsychicClient * getClient(PsychicClient *client);
|
||||
virtual void openCallback(PsychicClient *client) {};
|
||||
virtual void closeCallback(PsychicClient *client) {};
|
||||
|
||||
bool hasClient(PsychicClient *client);
|
||||
int count() { return _clients.size(); };
|
||||
const std::list<PsychicClient*>& getClientList();
|
||||
|
||||
//derived classes must implement these functions
|
||||
virtual bool canHandle(PsychicRequest *request) { return true; };
|
||||
virtual esp_err_t handleRequest(PsychicRequest *request) = 0;
|
||||
};
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,24 @@
|
||||
#ifndef PsychicHttp_h
|
||||
#define PsychicHttp_h
|
||||
|
||||
//#define ENABLE_ASYNC // This is something added in ESP-IDF 5.1.x where each request can be handled in its own thread
|
||||
|
||||
#include <http_status.h>
|
||||
#include "PsychicHttpServer.h"
|
||||
#include "PsychicRequest.h"
|
||||
#include "PsychicResponse.h"
|
||||
#include "PsychicEndpoint.h"
|
||||
#include "PsychicHandler.h"
|
||||
#include "PsychicStaticFileHandler.h"
|
||||
#include "PsychicFileResponse.h"
|
||||
#include "PsychicStreamResponse.h"
|
||||
#include "PsychicUploadHandler.h"
|
||||
#include "PsychicWebSocket.h"
|
||||
#include "PsychicEventSource.h"
|
||||
#include "PsychicJson.h"
|
||||
|
||||
#ifdef ENABLE_ASYNC
|
||||
#include "async_worker.h"
|
||||
#endif
|
||||
|
||||
#endif /* PsychicHttp_h */
|
||||
@@ -0,0 +1,366 @@
|
||||
#include "PsychicHttpServer.h"
|
||||
#include "PsychicEndpoint.h"
|
||||
#include "PsychicHandler.h"
|
||||
#include "PsychicWebHandler.h"
|
||||
#include "PsychicStaticFileHandler.h"
|
||||
#include "PsychicWebSocket.h"
|
||||
#include "PsychicJson.h"
|
||||
#include "WiFi.h"
|
||||
|
||||
PsychicHttpServer::PsychicHttpServer() :
|
||||
_onOpen(NULL),
|
||||
_onClose(NULL)
|
||||
{
|
||||
maxRequestBodySize = MAX_REQUEST_BODY_SIZE;
|
||||
maxUploadSize = MAX_UPLOAD_SIZE;
|
||||
|
||||
defaultEndpoint = new PsychicEndpoint(this, HTTP_GET, "");
|
||||
onNotFound(PsychicHttpServer::defaultNotFoundHandler);
|
||||
|
||||
//for a regular server
|
||||
config = HTTPD_DEFAULT_CONFIG();
|
||||
config.open_fn = PsychicHttpServer::openCallback;
|
||||
config.close_fn = PsychicHttpServer::closeCallback;
|
||||
config.uri_match_fn = httpd_uri_match_wildcard;
|
||||
config.global_user_ctx = this;
|
||||
config.global_user_ctx_free_fn = destroy;
|
||||
config.max_uri_handlers = 20;
|
||||
|
||||
#ifdef ENABLE_ASYNC
|
||||
// It is advisable that httpd_config_t->max_open_sockets > MAX_ASYNC_REQUESTS
|
||||
// Why? This leaves at least one socket still available to handle
|
||||
// quick synchronous requests. Otherwise, all the sockets will
|
||||
// get taken by the long async handlers, and your server will no
|
||||
// longer be responsive.
|
||||
config.max_open_sockets = ASYNC_WORKER_COUNT + 1;
|
||||
config.lru_purge_enable = true;
|
||||
#endif
|
||||
}
|
||||
|
||||
PsychicHttpServer::~PsychicHttpServer()
|
||||
{
|
||||
for (auto *client : _clients)
|
||||
delete(client);
|
||||
_clients.clear();
|
||||
|
||||
for (auto *endpoint : _endpoints)
|
||||
delete(endpoint);
|
||||
_endpoints.clear();
|
||||
|
||||
for (auto *handler : _handlers)
|
||||
delete(handler);
|
||||
_handlers.clear();
|
||||
|
||||
delete defaultEndpoint;
|
||||
}
|
||||
|
||||
void PsychicHttpServer::destroy(void *ctx)
|
||||
{
|
||||
PsychicHttpServer *temp = (PsychicHttpServer *)ctx;
|
||||
delete temp;
|
||||
}
|
||||
|
||||
esp_err_t PsychicHttpServer::listen(uint16_t port)
|
||||
{
|
||||
this->_use_ssl = false;
|
||||
this->config.server_port = port;
|
||||
|
||||
return this->_start();
|
||||
}
|
||||
|
||||
esp_err_t PsychicHttpServer::_start()
|
||||
{
|
||||
esp_err_t ret;
|
||||
|
||||
#ifdef ENABLE_ASYNC
|
||||
// start workers
|
||||
start_async_req_workers();
|
||||
#endif
|
||||
|
||||
//fire it up.
|
||||
ret = _startServer();
|
||||
if (ret != ESP_OK)
|
||||
{
|
||||
ESP_LOGE(PH_TAG, "Server start failed (%s)", esp_err_to_name(ret));
|
||||
return ret;
|
||||
}
|
||||
|
||||
// Register handler
|
||||
ret = httpd_register_err_handler(server, HTTPD_404_NOT_FOUND, PsychicHttpServer::notFoundHandler);
|
||||
if (ret != ESP_OK)
|
||||
ESP_LOGE(PH_TAG, "Add 404 handler failed (%s)", esp_err_to_name(ret));
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
esp_err_t PsychicHttpServer::_startServer() {
|
||||
return httpd_start(&this->server, &this->config);
|
||||
}
|
||||
|
||||
void PsychicHttpServer::stop()
|
||||
{
|
||||
httpd_stop(this->server);
|
||||
}
|
||||
|
||||
PsychicHandler& PsychicHttpServer::addHandler(PsychicHandler* handler){
|
||||
_handlers.push_back(handler);
|
||||
return *handler;
|
||||
}
|
||||
|
||||
void PsychicHttpServer::removeHandler(PsychicHandler *handler){
|
||||
_handlers.remove(handler);
|
||||
}
|
||||
|
||||
PsychicEndpoint* PsychicHttpServer::on(const char* uri) {
|
||||
return on(uri, HTTP_GET);
|
||||
}
|
||||
|
||||
PsychicEndpoint* PsychicHttpServer::on(const char* uri, http_method method)
|
||||
{
|
||||
PsychicWebHandler *handler = new PsychicWebHandler();
|
||||
|
||||
return on(uri, method, handler);
|
||||
}
|
||||
|
||||
PsychicEndpoint* PsychicHttpServer::on(const char* uri, PsychicHandler *handler)
|
||||
{
|
||||
return on(uri, HTTP_GET, handler);
|
||||
}
|
||||
|
||||
PsychicEndpoint* PsychicHttpServer::on(const char* uri, http_method method, PsychicHandler *handler)
|
||||
{
|
||||
//make our endpoint
|
||||
PsychicEndpoint *endpoint = new PsychicEndpoint(this, method, uri);
|
||||
|
||||
//set our handler
|
||||
endpoint->setHandler(handler);
|
||||
|
||||
// URI handler structure
|
||||
httpd_uri_t my_uri {
|
||||
.uri = uri,
|
||||
.method = method,
|
||||
.handler = PsychicEndpoint::requestCallback,
|
||||
.user_ctx = endpoint,
|
||||
.is_websocket = handler->isWebSocket()
|
||||
};
|
||||
|
||||
// Register endpoint with ESP-IDF server
|
||||
esp_err_t ret = httpd_register_uri_handler(this->server, &my_uri);
|
||||
if (ret != ESP_OK)
|
||||
ESP_LOGE(PH_TAG, "Add endpoint failed (%s)", esp_err_to_name(ret));
|
||||
|
||||
//save it for later
|
||||
_endpoints.push_back(endpoint);
|
||||
|
||||
return endpoint;
|
||||
}
|
||||
|
||||
PsychicEndpoint* PsychicHttpServer::on(const char* uri, PsychicHttpRequestCallback fn)
|
||||
{
|
||||
return on(uri, HTTP_GET, fn);
|
||||
}
|
||||
|
||||
PsychicEndpoint* PsychicHttpServer::on(const char* uri, http_method method, PsychicHttpRequestCallback fn)
|
||||
{
|
||||
//these basic requests need a basic web handler
|
||||
PsychicWebHandler *handler = new PsychicWebHandler();
|
||||
handler->onRequest(fn);
|
||||
|
||||
return on(uri, method, handler);
|
||||
}
|
||||
|
||||
PsychicEndpoint* PsychicHttpServer::on(const char* uri, PsychicJsonRequestCallback fn)
|
||||
{
|
||||
return on(uri, HTTP_GET, fn);
|
||||
}
|
||||
|
||||
PsychicEndpoint* PsychicHttpServer::on(const char* uri, http_method method, PsychicJsonRequestCallback fn)
|
||||
{
|
||||
//these basic requests need a basic web handler
|
||||
PsychicJsonHandler *handler = new PsychicJsonHandler();
|
||||
handler->onRequest(fn);
|
||||
|
||||
return on(uri, method, handler);
|
||||
}
|
||||
|
||||
void PsychicHttpServer::onNotFound(PsychicHttpRequestCallback fn)
|
||||
{
|
||||
PsychicWebHandler *handler = new PsychicWebHandler();
|
||||
handler->onRequest(fn);
|
||||
|
||||
this->defaultEndpoint->setHandler(handler);
|
||||
}
|
||||
|
||||
esp_err_t PsychicHttpServer::notFoundHandler(httpd_req_t *req, httpd_err_code_t err)
|
||||
{
|
||||
PsychicHttpServer *server = (PsychicHttpServer*)httpd_get_global_user_ctx(req->handle);
|
||||
PsychicRequest request(server, req);
|
||||
|
||||
//loop through our global handlers and see if anyone wants it
|
||||
for(auto *handler: server->_handlers)
|
||||
{
|
||||
//are we capable of handling this?
|
||||
if (handler->filter(&request) && handler->canHandle(&request))
|
||||
{
|
||||
//check our credentials
|
||||
if (handler->needsAuthentication(&request))
|
||||
return handler->authenticate(&request);
|
||||
else
|
||||
return handler->handleRequest(&request);
|
||||
}
|
||||
}
|
||||
|
||||
//nothing found, give it to our defaultEndpoint
|
||||
PsychicHandler *handler = server->defaultEndpoint->handler();
|
||||
if (handler->filter(&request) && handler->canHandle(&request))
|
||||
return handler->handleRequest(&request);
|
||||
|
||||
//not sure how we got this far.
|
||||
return ESP_ERR_HTTPD_INVALID_REQ;
|
||||
}
|
||||
|
||||
esp_err_t PsychicHttpServer::defaultNotFoundHandler(PsychicRequest *request)
|
||||
{
|
||||
request->reply(404, "text/html", "That URI does not exist.");
|
||||
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
void PsychicHttpServer::onOpen(PsychicClientCallback handler) {
|
||||
this->_onOpen = handler;
|
||||
}
|
||||
|
||||
esp_err_t PsychicHttpServer::openCallback(httpd_handle_t hd, int sockfd)
|
||||
{
|
||||
ESP_LOGI(PH_TAG, "New client connected %d", sockfd);
|
||||
|
||||
//get our global server reference
|
||||
PsychicHttpServer *server = (PsychicHttpServer*)httpd_get_global_user_ctx(hd);
|
||||
|
||||
//lookup our client
|
||||
PsychicClient *client = server->getClient(sockfd);
|
||||
if (client == NULL)
|
||||
{
|
||||
client = new PsychicClient(hd, sockfd);
|
||||
server->addClient(client);
|
||||
}
|
||||
|
||||
//user callback
|
||||
if (server->_onOpen != NULL)
|
||||
server->_onOpen(client);
|
||||
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
void PsychicHttpServer::onClose(PsychicClientCallback handler) {
|
||||
this->_onClose = handler;
|
||||
}
|
||||
|
||||
void PsychicHttpServer::closeCallback(httpd_handle_t hd, int sockfd)
|
||||
{
|
||||
ESP_LOGI(PH_TAG, "Client disconnected %d", sockfd);
|
||||
|
||||
PsychicHttpServer *server = (PsychicHttpServer*)httpd_get_global_user_ctx(hd);
|
||||
|
||||
//lookup our client
|
||||
PsychicClient *client = server->getClient(sockfd);
|
||||
if (client != NULL)
|
||||
{
|
||||
//give our handlers a chance to handle a disconnect first
|
||||
for (PsychicEndpoint * endpoint : server->_endpoints)
|
||||
{
|
||||
PsychicHandler *handler = endpoint->handler();
|
||||
handler->checkForClosedClient(client);
|
||||
}
|
||||
|
||||
//do we have a callback attached?
|
||||
if (server->_onClose != NULL)
|
||||
server->_onClose(client);
|
||||
|
||||
//remove it from our list
|
||||
server->removeClient(client);
|
||||
}
|
||||
else
|
||||
ESP_LOGE(PH_TAG, "No client record %d", sockfd);
|
||||
|
||||
//finally close it out.
|
||||
close(sockfd);
|
||||
}
|
||||
|
||||
PsychicStaticFileHandler* PsychicHttpServer::serveStatic(const char* uri, fs::FS& fs, const char* path, const char* cache_control)
|
||||
{
|
||||
PsychicStaticFileHandler* handler = new PsychicStaticFileHandler(uri, fs, path, cache_control);
|
||||
this->addHandler(handler);
|
||||
|
||||
return handler;
|
||||
}
|
||||
|
||||
void PsychicHttpServer::addClient(PsychicClient *client) {
|
||||
_clients.push_back(client);
|
||||
}
|
||||
|
||||
void PsychicHttpServer::removeClient(PsychicClient *client) {
|
||||
_clients.remove(client);
|
||||
delete client;
|
||||
}
|
||||
|
||||
PsychicClient * PsychicHttpServer::getClient(int socket) {
|
||||
for (PsychicClient * client : _clients)
|
||||
if (client->socket() == socket)
|
||||
return client;
|
||||
|
||||
return NULL;
|
||||
}
|
||||
|
||||
PsychicClient * PsychicHttpServer::getClient(httpd_req_t *req) {
|
||||
return getClient(httpd_req_to_sockfd(req));
|
||||
}
|
||||
|
||||
bool PsychicHttpServer::hasClient(int socket) {
|
||||
return getClient(socket) != NULL;
|
||||
}
|
||||
|
||||
const std::list<PsychicClient*>& PsychicHttpServer::getClientList() {
|
||||
return _clients;
|
||||
}
|
||||
|
||||
bool ON_STA_FILTER(PsychicRequest *request) {
|
||||
return WiFi.localIP() == request->client()->localIP();
|
||||
}
|
||||
|
||||
bool ON_AP_FILTER(PsychicRequest *request) {
|
||||
return WiFi.softAPIP() == request->client()->localIP();
|
||||
}
|
||||
|
||||
String urlDecode(const char* encoded)
|
||||
{
|
||||
size_t length = strlen(encoded);
|
||||
char* decoded = (char*)malloc(length + 1);
|
||||
if (!decoded) {
|
||||
return "";
|
||||
}
|
||||
|
||||
size_t i, j = 0;
|
||||
for (i = 0; i < length; ++i) {
|
||||
if (encoded[i] == '%' && isxdigit(encoded[i + 1]) && isxdigit(encoded[i + 2])) {
|
||||
// Valid percent-encoded sequence
|
||||
int hex;
|
||||
sscanf(encoded + i + 1, "%2x", &hex);
|
||||
decoded[j++] = (char)hex;
|
||||
i += 2; // Skip the two hexadecimal characters
|
||||
} else if (encoded[i] == '+') {
|
||||
// Convert '+' to space
|
||||
decoded[j++] = ' ';
|
||||
} else {
|
||||
// Copy other characters as they are
|
||||
decoded[j++] = encoded[i];
|
||||
}
|
||||
}
|
||||
|
||||
decoded[j] = '\0'; // Null-terminate the decoded string
|
||||
|
||||
String output(decoded);
|
||||
free(decoded);
|
||||
|
||||
return output;
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
#ifndef PsychicHttpServer_h
|
||||
#define PsychicHttpServer_h
|
||||
|
||||
#include "PsychicCore.h"
|
||||
#include "PsychicClient.h"
|
||||
#include "PsychicHandler.h"
|
||||
|
||||
class PsychicEndpoint;
|
||||
class PsychicHandler;
|
||||
class PsychicStaticFileHandler;
|
||||
|
||||
class PsychicHttpServer
|
||||
{
|
||||
protected:
|
||||
bool _use_ssl = false;
|
||||
std::list<PsychicEndpoint*> _endpoints;
|
||||
std::list<PsychicHandler*> _handlers;
|
||||
std::list<PsychicClient*> _clients;
|
||||
|
||||
PsychicClientCallback _onOpen;
|
||||
PsychicClientCallback _onClose;
|
||||
|
||||
esp_err_t _start();
|
||||
virtual esp_err_t _startServer();
|
||||
|
||||
public:
|
||||
PsychicHttpServer();
|
||||
~PsychicHttpServer();
|
||||
|
||||
//esp-idf specific stuff
|
||||
httpd_handle_t server;
|
||||
httpd_config_t config;
|
||||
|
||||
//some limits on what we will accept
|
||||
unsigned long maxUploadSize;
|
||||
unsigned long maxRequestBodySize;
|
||||
|
||||
PsychicEndpoint *defaultEndpoint;
|
||||
|
||||
static void destroy(void *ctx);
|
||||
|
||||
esp_err_t listen(uint16_t port);
|
||||
|
||||
virtual void stop();
|
||||
|
||||
PsychicHandler& addHandler(PsychicHandler* handler);
|
||||
void removeHandler(PsychicHandler* handler);
|
||||
|
||||
void addClient(PsychicClient *client);
|
||||
void removeClient(PsychicClient *client);
|
||||
PsychicClient* getClient(int socket);
|
||||
PsychicClient* getClient(httpd_req_t *req);
|
||||
bool hasClient(int socket);
|
||||
int count() { return _clients.size(); };
|
||||
const std::list<PsychicClient*>& getClientList();
|
||||
|
||||
PsychicEndpoint* on(const char* uri);
|
||||
PsychicEndpoint* on(const char* uri, http_method method);
|
||||
PsychicEndpoint* on(const char* uri, PsychicHandler *handler);
|
||||
PsychicEndpoint* on(const char* uri, http_method method, PsychicHandler *handler);
|
||||
PsychicEndpoint* on(const char* uri, PsychicHttpRequestCallback onRequest);
|
||||
PsychicEndpoint* on(const char* uri, http_method method, PsychicHttpRequestCallback onRequest);
|
||||
PsychicEndpoint* on(const char* uri, PsychicJsonRequestCallback onRequest);
|
||||
PsychicEndpoint* on(const char* uri, http_method method, PsychicJsonRequestCallback onRequest);
|
||||
|
||||
static esp_err_t notFoundHandler(httpd_req_t *req, httpd_err_code_t err);
|
||||
static esp_err_t defaultNotFoundHandler(PsychicRequest *request);
|
||||
void onNotFound(PsychicHttpRequestCallback fn);
|
||||
|
||||
void onOpen(PsychicClientCallback handler);
|
||||
void onClose(PsychicClientCallback handler);
|
||||
static esp_err_t openCallback(httpd_handle_t hd, int sockfd);
|
||||
static void closeCallback(httpd_handle_t hd, int sockfd);
|
||||
|
||||
PsychicStaticFileHandler* serveStatic(const char* uri, fs::FS& fs, const char* path, const char* cache_control = NULL);
|
||||
};
|
||||
|
||||
bool ON_STA_FILTER(PsychicRequest *request);
|
||||
bool ON_AP_FILTER(PsychicRequest *request);
|
||||
|
||||
#endif // PsychicHttpServer_h
|
||||
@@ -0,0 +1,50 @@
|
||||
#include "PsychicHttpsServer.h"
|
||||
|
||||
PsychicHttpsServer::PsychicHttpsServer() : PsychicHttpServer()
|
||||
{
|
||||
//for a SSL server
|
||||
ssl_config = HTTPD_SSL_CONFIG_DEFAULT();
|
||||
ssl_config.httpd.open_fn = PsychicHttpServer::openCallback;
|
||||
ssl_config.httpd.close_fn = PsychicHttpServer::closeCallback;
|
||||
ssl_config.httpd.uri_match_fn = httpd_uri_match_wildcard;
|
||||
ssl_config.httpd.global_user_ctx = this;
|
||||
ssl_config.httpd.global_user_ctx_free_fn = destroy;
|
||||
ssl_config.httpd.max_uri_handlers = 20;
|
||||
|
||||
// each SSL connection takes about 45kb of heap
|
||||
// a barebones sketch with PsychicHttp has ~150kb of heap available
|
||||
// if we set it higher than 2 and use all the connections, we get lots of memory errors.
|
||||
// not to mention there is no heap left over for the program itself.
|
||||
ssl_config.httpd.max_open_sockets = 2;
|
||||
}
|
||||
|
||||
PsychicHttpsServer::~PsychicHttpsServer() {}
|
||||
|
||||
esp_err_t PsychicHttpsServer::listen(uint16_t port, const char *cert, const char *private_key)
|
||||
{
|
||||
this->_use_ssl = true;
|
||||
|
||||
this->ssl_config.port_secure = port;
|
||||
this->ssl_config.cacert_pem = (uint8_t *)cert;
|
||||
this->ssl_config.cacert_len = strlen(cert)+1;
|
||||
this->ssl_config.prvtkey_pem = (uint8_t *)private_key;
|
||||
this->ssl_config.prvtkey_len = strlen(private_key)+1;
|
||||
|
||||
return this->_start();
|
||||
}
|
||||
|
||||
esp_err_t PsychicHttpsServer::_startServer()
|
||||
{
|
||||
if (this->_use_ssl)
|
||||
return httpd_ssl_start(&this->server, &this->ssl_config);
|
||||
else
|
||||
return httpd_start(&this->server, &this->config);
|
||||
}
|
||||
|
||||
void PsychicHttpsServer::stop()
|
||||
{
|
||||
if (this->_use_ssl)
|
||||
httpd_ssl_stop(this->server);
|
||||
else
|
||||
httpd_stop(this->server);
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
#ifndef PsychicHttpsServer_h
|
||||
#define PsychicHttpsServer_h
|
||||
|
||||
#include "PsychicCore.h"
|
||||
#include "PsychicHttpServer.h"
|
||||
#include <esp_https_server.h>
|
||||
|
||||
#if !CONFIG_HTTPD_WS_SUPPORT
|
||||
#error PsychicHttpsServer cannot be used unless HTTPD_WS_SUPPORT is enabled in esp-http-server component configuration
|
||||
#endif
|
||||
|
||||
#define PSY_ENABLE_SSL //you can use this define in your code to enable/disable these features
|
||||
|
||||
class PsychicHttpsServer : public PsychicHttpServer
|
||||
{
|
||||
protected:
|
||||
bool _use_ssl = false;
|
||||
|
||||
public:
|
||||
PsychicHttpsServer();
|
||||
~PsychicHttpsServer();
|
||||
|
||||
httpd_ssl_config_t ssl_config;
|
||||
|
||||
using PsychicHttpServer::listen; //keep the regular version
|
||||
esp_err_t listen(uint16_t port, const char *cert, const char *private_key);
|
||||
|
||||
virtual esp_err_t _startServer() override final;
|
||||
virtual void stop() override final;
|
||||
};
|
||||
|
||||
#endif // PsychicHttpsServer_h
|
||||
@@ -0,0 +1,135 @@
|
||||
#include "PsychicJson.h"
|
||||
|
||||
#ifdef ARDUINOJSON_6_COMPATIBILITY
|
||||
PsychicJsonResponse::PsychicJsonResponse(PsychicRequest *request, bool isArray, size_t maxJsonBufferSize) : PsychicResponse(request),
|
||||
_jsonBuffer(maxJsonBufferSize)
|
||||
{
|
||||
setContentType(JSON_MIMETYPE);
|
||||
if (isArray)
|
||||
_root = _jsonBuffer.createNestedArray();
|
||||
else
|
||||
_root = _jsonBuffer.createNestedObject();
|
||||
}
|
||||
#else
|
||||
PsychicJsonResponse::PsychicJsonResponse(PsychicRequest *request, bool isArray) : PsychicResponse(request)
|
||||
{
|
||||
setContentType(JSON_MIMETYPE);
|
||||
if (isArray)
|
||||
_root = _jsonBuffer.add<JsonArray>();
|
||||
else
|
||||
_root = _jsonBuffer.add<JsonObject>();
|
||||
}
|
||||
#endif
|
||||
|
||||
JsonVariant &PsychicJsonResponse::getRoot() { return _root; }
|
||||
|
||||
size_t PsychicJsonResponse::getLength()
|
||||
{
|
||||
return measureJson(_root);
|
||||
}
|
||||
|
||||
esp_err_t PsychicJsonResponse::send()
|
||||
{
|
||||
esp_err_t err = ESP_OK;
|
||||
size_t length = getLength();
|
||||
size_t buffer_size;
|
||||
char *buffer;
|
||||
|
||||
// DUMP(length);
|
||||
|
||||
// how big of a buffer do we want?
|
||||
if (length < JSON_BUFFER_SIZE)
|
||||
buffer_size = length + 1;
|
||||
else
|
||||
buffer_size = JSON_BUFFER_SIZE;
|
||||
|
||||
// DUMP(buffer_size);
|
||||
|
||||
buffer = (char *)malloc(buffer_size);
|
||||
if (buffer == NULL)
|
||||
{
|
||||
httpd_resp_send_err(this->_request->request(), HTTPD_500_INTERNAL_SERVER_ERROR, "Unable to allocate memory.");
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
// send it in one shot or no?
|
||||
if (length < JSON_BUFFER_SIZE)
|
||||
{
|
||||
// TRACE();
|
||||
|
||||
serializeJson(_root, buffer, buffer_size);
|
||||
|
||||
this->setContent((uint8_t *)buffer, length);
|
||||
this->setContentType(JSON_MIMETYPE);
|
||||
|
||||
err = PsychicResponse::send();
|
||||
}
|
||||
else
|
||||
{
|
||||
// helper class that acts as a stream to print chunked responses
|
||||
ChunkPrinter dest(this, (uint8_t *)buffer, buffer_size);
|
||||
|
||||
// keep our headers
|
||||
this->sendHeaders();
|
||||
|
||||
serializeJson(_root, dest);
|
||||
|
||||
// send the last bits
|
||||
dest.flush();
|
||||
|
||||
// done with our chunked response too
|
||||
err = this->finishChunking();
|
||||
}
|
||||
|
||||
// let the buffer go
|
||||
free(buffer);
|
||||
|
||||
return err;
|
||||
}
|
||||
|
||||
#ifdef ARDUINOJSON_6_COMPATIBILITY
|
||||
PsychicJsonHandler::PsychicJsonHandler(size_t maxJsonBufferSize) : _onRequest(NULL),
|
||||
_maxJsonBufferSize(maxJsonBufferSize){};
|
||||
|
||||
PsychicJsonHandler::PsychicJsonHandler(PsychicJsonRequestCallback onRequest, size_t maxJsonBufferSize) : _onRequest(onRequest),
|
||||
_maxJsonBufferSize(maxJsonBufferSize)
|
||||
{
|
||||
}
|
||||
#else
|
||||
PsychicJsonHandler::PsychicJsonHandler() : _onRequest(NULL){};
|
||||
|
||||
PsychicJsonHandler::PsychicJsonHandler(PsychicJsonRequestCallback onRequest) : _onRequest(onRequest)
|
||||
{
|
||||
}
|
||||
#endif
|
||||
|
||||
void PsychicJsonHandler::onRequest(PsychicJsonRequestCallback fn) { _onRequest = fn; }
|
||||
|
||||
esp_err_t PsychicJsonHandler::handleRequest(PsychicRequest *request)
|
||||
{
|
||||
// process basic stuff
|
||||
PsychicWebHandler::handleRequest(request);
|
||||
|
||||
if (_onRequest)
|
||||
{
|
||||
#ifdef ARDUINOJSON_6_COMPATIBILITY
|
||||
DynamicJsonDocument jsonBuffer(this->_maxJsonBufferSize);
|
||||
DeserializationError error = deserializeJson(jsonBuffer, request->body());
|
||||
if (error)
|
||||
return request->reply(400);
|
||||
|
||||
JsonVariant json = jsonBuffer.as<JsonVariant>();
|
||||
#else
|
||||
JsonDocument jsonBuffer;
|
||||
DeserializationError error = deserializeJson(jsonBuffer, request->body());
|
||||
if (error)
|
||||
return request->reply(400);
|
||||
|
||||
JsonVariant json = jsonBuffer.as<JsonVariant>();
|
||||
#endif
|
||||
|
||||
return _onRequest(request, json);
|
||||
}
|
||||
else
|
||||
return request->reply(500);
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
// PsychicJson.h
|
||||
/*
|
||||
Async Response to use with ArduinoJson and AsyncWebServer
|
||||
Written by Andrew Melvin (SticilFace) with help from me-no-dev and BBlanchon.
|
||||
Ported to PsychicHttp by Zach Hoeken
|
||||
|
||||
*/
|
||||
#ifndef PSYCHIC_JSON_H_
|
||||
#define PSYCHIC_JSON_H_
|
||||
|
||||
#include "PsychicRequest.h"
|
||||
#include "PsychicWebHandler.h"
|
||||
#include "ChunkPrinter.h"
|
||||
#include <ArduinoJson.h>
|
||||
|
||||
#if ARDUINOJSON_VERSION_MAJOR == 6
|
||||
#define ARDUINOJSON_6_COMPATIBILITY
|
||||
#ifndef DYNAMIC_JSON_DOCUMENT_SIZE
|
||||
#define DYNAMIC_JSON_DOCUMENT_SIZE 4096
|
||||
#endif
|
||||
#endif
|
||||
|
||||
|
||||
#ifndef JSON_BUFFER_SIZE
|
||||
#define JSON_BUFFER_SIZE 4*1024
|
||||
#endif
|
||||
|
||||
constexpr const char *JSON_MIMETYPE = "application/json";
|
||||
|
||||
/*
|
||||
* Json Response
|
||||
* */
|
||||
|
||||
class PsychicJsonResponse : public PsychicResponse
|
||||
{
|
||||
protected:
|
||||
#ifdef ARDUINOJSON_5_COMPATIBILITY
|
||||
DynamicJsonBuffer _jsonBuffer;
|
||||
#elif ARDUINOJSON_VERSION_MAJOR == 6
|
||||
DynamicJsonDocument _jsonBuffer;
|
||||
#else
|
||||
JsonDocument _jsonBuffer;
|
||||
#endif
|
||||
|
||||
JsonVariant _root;
|
||||
size_t _contentLength;
|
||||
|
||||
public:
|
||||
#ifdef ARDUINOJSON_5_COMPATIBILITY
|
||||
PsychicJsonResponse(PsychicRequest *request, bool isArray = false);
|
||||
#elif ARDUINOJSON_VERSION_MAJOR == 6
|
||||
PsychicJsonResponse(PsychicRequest *request, bool isArray = false, size_t maxJsonBufferSize = DYNAMIC_JSON_DOCUMENT_SIZE);
|
||||
#else
|
||||
PsychicJsonResponse(PsychicRequest *request, bool isArray = false);
|
||||
#endif
|
||||
|
||||
~PsychicJsonResponse() {}
|
||||
|
||||
JsonVariant &getRoot();
|
||||
size_t getLength();
|
||||
|
||||
virtual esp_err_t send() override;
|
||||
};
|
||||
|
||||
class PsychicJsonHandler : public PsychicWebHandler
|
||||
{
|
||||
protected:
|
||||
PsychicJsonRequestCallback _onRequest;
|
||||
#if ARDUINOJSON_VERSION_MAJOR == 6
|
||||
const size_t _maxJsonBufferSize = DYNAMIC_JSON_DOCUMENT_SIZE;
|
||||
#endif
|
||||
|
||||
public:
|
||||
#ifdef ARDUINOJSON_5_COMPATIBILITY
|
||||
PsychicJsonHandler();
|
||||
PsychicJsonHandler(PsychicJsonRequestCallback onRequest);
|
||||
#elif ARDUINOJSON_VERSION_MAJOR == 6
|
||||
PsychicJsonHandler(size_t maxJsonBufferSize = DYNAMIC_JSON_DOCUMENT_SIZE);
|
||||
PsychicJsonHandler(PsychicJsonRequestCallback onRequest, size_t maxJsonBufferSize = DYNAMIC_JSON_DOCUMENT_SIZE);
|
||||
#else
|
||||
PsychicJsonHandler();
|
||||
PsychicJsonHandler(PsychicJsonRequestCallback onRequest);
|
||||
#endif
|
||||
|
||||
void onRequest(PsychicJsonRequestCallback fn);
|
||||
virtual esp_err_t handleRequest(PsychicRequest *request) override;
|
||||
};
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,586 @@
|
||||
#include "PsychicRequest.h"
|
||||
#include "http_status.h"
|
||||
#include "PsychicHttpServer.h"
|
||||
|
||||
PsychicRequest::PsychicRequest(PsychicHttpServer *server, httpd_req_t *req) : _server(server),
|
||||
_req(req),
|
||||
_method(HTTP_GET),
|
||||
_query(""),
|
||||
_body(""),
|
||||
_tempObject(NULL)
|
||||
{
|
||||
// load up our client.
|
||||
this->_client = server->getClient(req);
|
||||
|
||||
// handle our session data
|
||||
if (req->sess_ctx != NULL)
|
||||
this->_session = (SessionData *)req->sess_ctx;
|
||||
else
|
||||
{
|
||||
this->_session = new SessionData();
|
||||
req->sess_ctx = this->_session;
|
||||
}
|
||||
|
||||
// callback for freeing the session later
|
||||
req->free_ctx = this->freeSession;
|
||||
|
||||
// load up some data
|
||||
this->_uri = String(this->_req->uri);
|
||||
}
|
||||
|
||||
PsychicRequest::~PsychicRequest()
|
||||
{
|
||||
// temorary user object
|
||||
if (_tempObject != NULL)
|
||||
free(_tempObject);
|
||||
|
||||
// our web parameters
|
||||
for (auto *param : _params)
|
||||
delete (param);
|
||||
_params.clear();
|
||||
}
|
||||
|
||||
void PsychicRequest::freeSession(void *ctx)
|
||||
{
|
||||
if (ctx != NULL)
|
||||
{
|
||||
SessionData *session = (SessionData *)ctx;
|
||||
delete session;
|
||||
}
|
||||
}
|
||||
|
||||
PsychicHttpServer *PsychicRequest::server()
|
||||
{
|
||||
return _server;
|
||||
}
|
||||
|
||||
httpd_req_t *PsychicRequest::request()
|
||||
{
|
||||
return _req;
|
||||
}
|
||||
|
||||
PsychicClient *PsychicRequest::client()
|
||||
{
|
||||
return _client;
|
||||
}
|
||||
|
||||
const String PsychicRequest::getFilename()
|
||||
{
|
||||
// parse the content-disposition header
|
||||
if (this->hasHeader("Content-Disposition"))
|
||||
{
|
||||
ContentDisposition cd = this->getContentDisposition();
|
||||
if (cd.filename != "")
|
||||
return cd.filename;
|
||||
}
|
||||
|
||||
// fall back to passed in query string
|
||||
PsychicWebParameter *param = getParam("_filename");
|
||||
if (param != NULL)
|
||||
return param->name();
|
||||
|
||||
// fall back to parsing it from url (useful for wildcard uploads)
|
||||
String uri = this->uri();
|
||||
int filenameStart = uri.lastIndexOf('/') + 1;
|
||||
String filename = uri.substring(filenameStart);
|
||||
if (filename != "")
|
||||
return filename;
|
||||
|
||||
// finally, unknown.
|
||||
ESP_LOGE(PH_TAG, "Did not get a valid filename from the upload.");
|
||||
return "unknown.txt";
|
||||
}
|
||||
|
||||
const ContentDisposition PsychicRequest::getContentDisposition()
|
||||
{
|
||||
ContentDisposition cd;
|
||||
String header = this->header("Content-Disposition");
|
||||
int start;
|
||||
int end;
|
||||
|
||||
if (header.indexOf("form-data") == 0)
|
||||
cd.disposition = FORM_DATA;
|
||||
else if (header.indexOf("attachment") == 0)
|
||||
cd.disposition = ATTACHMENT;
|
||||
else if (header.indexOf("inline") == 0)
|
||||
cd.disposition = INLINE;
|
||||
else
|
||||
cd.disposition = NONE;
|
||||
|
||||
start = header.indexOf("filename=");
|
||||
if (start)
|
||||
{
|
||||
end = header.indexOf('"', start + 10);
|
||||
cd.filename = header.substring(start + 10, end - 1);
|
||||
}
|
||||
|
||||
start = header.indexOf("name=");
|
||||
if (start)
|
||||
{
|
||||
end = header.indexOf('"', start + 6);
|
||||
cd.name = header.substring(start + 6, end - 1);
|
||||
}
|
||||
|
||||
return cd;
|
||||
}
|
||||
|
||||
esp_err_t PsychicRequest::loadBody()
|
||||
{
|
||||
esp_err_t err = ESP_OK;
|
||||
|
||||
this->_body = String();
|
||||
|
||||
size_t remaining = this->_req->content_len;
|
||||
size_t actuallyReceived = 0;
|
||||
char *buf = (char *)malloc(remaining + 1);
|
||||
if (buf == NULL)
|
||||
{
|
||||
ESP_LOGE(PH_TAG, "Failed to allocate memory for body");
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
while (remaining > 0)
|
||||
{
|
||||
int received = httpd_req_recv(this->_req, buf + actuallyReceived, remaining);
|
||||
|
||||
if (received == HTTPD_SOCK_ERR_TIMEOUT)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
else if (received == HTTPD_SOCK_ERR_FAIL)
|
||||
{
|
||||
ESP_LOGE(PH_TAG, "Failed to receive data.");
|
||||
err = ESP_FAIL;
|
||||
break;
|
||||
}
|
||||
|
||||
remaining -= received;
|
||||
actuallyReceived += received;
|
||||
}
|
||||
|
||||
buf[actuallyReceived] = '\0';
|
||||
this->_body = String(buf);
|
||||
free(buf);
|
||||
return err;
|
||||
}
|
||||
|
||||
http_method PsychicRequest::method()
|
||||
{
|
||||
return (http_method)this->_req->method;
|
||||
}
|
||||
|
||||
const String PsychicRequest::methodStr()
|
||||
{
|
||||
return String(http_method_str((http_method)this->_req->method));
|
||||
}
|
||||
|
||||
const String PsychicRequest::path()
|
||||
{
|
||||
int index = _uri.indexOf("?");
|
||||
if (index == -1)
|
||||
return _uri;
|
||||
else
|
||||
return _uri.substring(0, index);
|
||||
}
|
||||
|
||||
const String &PsychicRequest::uri()
|
||||
{
|
||||
return this->_uri;
|
||||
}
|
||||
|
||||
const String &PsychicRequest::query()
|
||||
{
|
||||
return this->_query;
|
||||
}
|
||||
|
||||
// no way to get list of headers yet....
|
||||
// int PsychicRequest::headers()
|
||||
// {
|
||||
// }
|
||||
|
||||
const String PsychicRequest::header(const char *name)
|
||||
{
|
||||
size_t header_len = httpd_req_get_hdr_value_len(this->_req, name);
|
||||
|
||||
// if we've got one, allocated it and load it
|
||||
if (header_len)
|
||||
{
|
||||
char header[header_len + 1];
|
||||
httpd_req_get_hdr_value_str(this->_req, name, header, sizeof(header));
|
||||
return String(header);
|
||||
}
|
||||
else
|
||||
return "";
|
||||
}
|
||||
|
||||
bool PsychicRequest::hasHeader(const char *name)
|
||||
{
|
||||
return httpd_req_get_hdr_value_len(this->_req, name) > 0;
|
||||
}
|
||||
|
||||
const String PsychicRequest::host()
|
||||
{
|
||||
return this->header("Host");
|
||||
}
|
||||
|
||||
const String PsychicRequest::contentType()
|
||||
{
|
||||
return header("Content-Type");
|
||||
}
|
||||
|
||||
size_t PsychicRequest::contentLength()
|
||||
{
|
||||
return this->_req->content_len;
|
||||
}
|
||||
|
||||
const String &PsychicRequest::body()
|
||||
{
|
||||
return this->_body;
|
||||
}
|
||||
|
||||
bool PsychicRequest::isMultipart()
|
||||
{
|
||||
const String &type = this->contentType();
|
||||
|
||||
return (this->contentType().indexOf("multipart/form-data") >= 0);
|
||||
}
|
||||
|
||||
esp_err_t PsychicRequest::redirect(const char *url)
|
||||
{
|
||||
PsychicResponse response(this);
|
||||
response.setCode(301);
|
||||
response.addHeader("Location", url);
|
||||
|
||||
return response.send();
|
||||
}
|
||||
|
||||
bool PsychicRequest::hasCookie(const char *key)
|
||||
{
|
||||
char cookie[MAX_COOKIE_SIZE];
|
||||
size_t cookieSize = MAX_COOKIE_SIZE;
|
||||
esp_err_t err = httpd_req_get_cookie_val(this->_req, key, cookie, &cookieSize);
|
||||
|
||||
// did we get anything?
|
||||
if (err == ESP_OK)
|
||||
return true;
|
||||
else if (err == ESP_ERR_HTTPD_RESULT_TRUNC)
|
||||
ESP_LOGE(PH_TAG, "cookie too large (%d bytes).\n", cookieSize);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
const String PsychicRequest::getCookie(const char *key)
|
||||
{
|
||||
char cookie[MAX_COOKIE_SIZE];
|
||||
size_t cookieSize = MAX_COOKIE_SIZE;
|
||||
esp_err_t err = httpd_req_get_cookie_val(this->_req, key, cookie, &cookieSize);
|
||||
|
||||
// did we get anything?
|
||||
if (err == ESP_OK)
|
||||
return String(cookie);
|
||||
else
|
||||
return "";
|
||||
}
|
||||
|
||||
void PsychicRequest::loadParams()
|
||||
{
|
||||
// did we get a query string?
|
||||
size_t query_len = httpd_req_get_url_query_len(_req);
|
||||
if (query_len)
|
||||
{
|
||||
char query[query_len + 1];
|
||||
httpd_req_get_url_query_str(_req, query, sizeof(query));
|
||||
_query = "";
|
||||
_query.concat(query);
|
||||
|
||||
// parse them.
|
||||
_addParams(_query);
|
||||
}
|
||||
|
||||
// did we get form data as body?
|
||||
if (this->method() == HTTP_POST && this->contentType() == "application/x-www-form-urlencoded")
|
||||
{
|
||||
_addParams(_body);
|
||||
}
|
||||
}
|
||||
|
||||
void PsychicRequest::_addParams(const String ¶ms)
|
||||
{
|
||||
size_t start = 0;
|
||||
while (start < params.length())
|
||||
{
|
||||
int end = params.indexOf('&', start);
|
||||
if (end < 0)
|
||||
end = params.length();
|
||||
int equal = params.indexOf('=', start);
|
||||
if (equal < 0 || equal > end)
|
||||
equal = end;
|
||||
String name = params.substring(start, equal);
|
||||
String value = equal + 1 < end ? params.substring(equal + 1, end) : String();
|
||||
addParam(name, value);
|
||||
start = end + 1;
|
||||
}
|
||||
}
|
||||
|
||||
PsychicWebParameter *PsychicRequest::addParam(const String &name, const String &value, bool decode)
|
||||
{
|
||||
if (decode)
|
||||
return addParam(new PsychicWebParameter(urlDecode(name.c_str()), urlDecode(value.c_str())));
|
||||
else
|
||||
return addParam(new PsychicWebParameter(name, value));
|
||||
}
|
||||
|
||||
PsychicWebParameter *PsychicRequest::addParam(PsychicWebParameter *param)
|
||||
{
|
||||
_params.push_back(param);
|
||||
return param;
|
||||
}
|
||||
|
||||
bool PsychicRequest::hasParam(const char *key)
|
||||
{
|
||||
return getParam(key) != NULL;
|
||||
}
|
||||
|
||||
PsychicWebParameter *PsychicRequest::getParam(const char *key)
|
||||
{
|
||||
for (auto *param : _params)
|
||||
if (param->name().equals(key))
|
||||
return param;
|
||||
|
||||
return NULL;
|
||||
}
|
||||
|
||||
bool PsychicRequest::hasSessionKey(const String &key)
|
||||
{
|
||||
return this->_session->find(key) != this->_session->end();
|
||||
}
|
||||
|
||||
const String PsychicRequest::getSessionKey(const String &key)
|
||||
{
|
||||
auto it = this->_session->find(key);
|
||||
if (it != this->_session->end())
|
||||
return it->second;
|
||||
else
|
||||
return "";
|
||||
}
|
||||
|
||||
void PsychicRequest::setSessionKey(const String &key, const String &value)
|
||||
{
|
||||
this->_session->insert(std::pair<String, String>(key, value));
|
||||
}
|
||||
|
||||
static const String md5str(const String &in)
|
||||
{
|
||||
MD5Builder md5 = MD5Builder();
|
||||
md5.begin();
|
||||
md5.add(in);
|
||||
md5.calculate();
|
||||
return md5.toString();
|
||||
}
|
||||
|
||||
bool PsychicRequest::authenticate(const char *username, const char *password)
|
||||
{
|
||||
if (hasHeader("Authorization"))
|
||||
{
|
||||
String authReq = header("Authorization");
|
||||
if (authReq.startsWith("Basic"))
|
||||
{
|
||||
authReq = authReq.substring(6);
|
||||
authReq.trim();
|
||||
char toencodeLen = strlen(username) + strlen(password) + 1;
|
||||
char *toencode = new char[toencodeLen + 1];
|
||||
if (toencode == NULL)
|
||||
{
|
||||
authReq = "";
|
||||
return false;
|
||||
}
|
||||
char *encoded = new char[base64_encode_expected_len(toencodeLen) + 1];
|
||||
if (encoded == NULL)
|
||||
{
|
||||
authReq = "";
|
||||
delete[] toencode;
|
||||
return false;
|
||||
}
|
||||
sprintf(toencode, "%s:%s", username, password);
|
||||
if (base64_encode_chars(toencode, toencodeLen, encoded) > 0 && authReq.equalsConstantTime(encoded))
|
||||
{
|
||||
authReq = "";
|
||||
delete[] toencode;
|
||||
delete[] encoded;
|
||||
return true;
|
||||
}
|
||||
delete[] toencode;
|
||||
delete[] encoded;
|
||||
}
|
||||
else if (authReq.startsWith(F("Digest")))
|
||||
{
|
||||
authReq = authReq.substring(7);
|
||||
String _username = _extractParam(authReq, F("username=\""), '\"');
|
||||
if (!_username.length() || _username != String(username))
|
||||
{
|
||||
authReq = "";
|
||||
return false;
|
||||
}
|
||||
// extracting required parameters for RFC 2069 simpler Digest
|
||||
String _realm = _extractParam(authReq, F("realm=\""), '\"');
|
||||
String _nonce = _extractParam(authReq, F("nonce=\""), '\"');
|
||||
String _uri = _extractParam(authReq, F("uri=\""), '\"');
|
||||
String _resp = _extractParam(authReq, F("response=\""), '\"');
|
||||
String _opaque = _extractParam(authReq, F("opaque=\""), '\"');
|
||||
|
||||
if ((!_realm.length()) || (!_nonce.length()) || (!_uri.length()) || (!_resp.length()) || (!_opaque.length()))
|
||||
{
|
||||
authReq = "";
|
||||
return false;
|
||||
}
|
||||
if ((_opaque != this->getSessionKey("opaque")) || (_nonce != this->getSessionKey("nonce")) || (_realm != this->getSessionKey("realm")))
|
||||
{
|
||||
// DUMP(_opaque);
|
||||
// DUMP(this->getSessionKey("opaque"));
|
||||
// DUMP(_nonce);
|
||||
// DUMP(this->getSessionKey("nonce"));
|
||||
// DUMP(_realm);
|
||||
// DUMP(this->getSessionKey("realm"));
|
||||
authReq = "";
|
||||
return false;
|
||||
}
|
||||
// parameters for the RFC 2617 newer Digest
|
||||
String _nc, _cnonce;
|
||||
if (authReq.indexOf("qop=auth") != -1 || authReq.indexOf("qop=\"auth\"") != -1)
|
||||
{
|
||||
_nc = _extractParam(authReq, F("nc="), ',');
|
||||
_cnonce = _extractParam(authReq, F("cnonce=\""), '\"');
|
||||
}
|
||||
String _H1 = md5str(String(username) + ':' + _realm + ':' + String(password));
|
||||
ESP_LOGD(PH_TAG, "Hash of user:realm:pass=%s", _H1);
|
||||
String _H2 = "";
|
||||
if (_method == HTTP_GET)
|
||||
{
|
||||
_H2 = md5str(String(F("GET:")) + _uri);
|
||||
}
|
||||
else if (_method == HTTP_POST)
|
||||
{
|
||||
_H2 = md5str(String(F("POST:")) + _uri);
|
||||
}
|
||||
else if (_method == HTTP_PUT)
|
||||
{
|
||||
_H2 = md5str(String(F("PUT:")) + _uri);
|
||||
}
|
||||
else if (_method == HTTP_DELETE)
|
||||
{
|
||||
_H2 = md5str(String(F("DELETE:")) + _uri);
|
||||
}
|
||||
else
|
||||
{
|
||||
_H2 = md5str(String(F("GET:")) + _uri);
|
||||
}
|
||||
ESP_LOGD(PH_TAG, "Hash of GET:uri=%s", _H2);
|
||||
String _responsecheck = "";
|
||||
if (authReq.indexOf("qop=auth") != -1 || authReq.indexOf("qop=\"auth\"") != -1)
|
||||
{
|
||||
_responsecheck = md5str(_H1 + ':' + _nonce + ':' + _nc + ':' + _cnonce + F(":auth:") + _H2);
|
||||
}
|
||||
else
|
||||
{
|
||||
_responsecheck = md5str(_H1 + ':' + _nonce + ':' + _H2);
|
||||
}
|
||||
ESP_LOGD(PH_TAG, "The Proper response=%s", _responsecheck);
|
||||
if (_resp == _responsecheck)
|
||||
{
|
||||
authReq = "";
|
||||
return true;
|
||||
}
|
||||
}
|
||||
authReq = "";
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
const String PsychicRequest::_extractParam(const String &authReq, const String ¶m, const char delimit)
|
||||
{
|
||||
int _begin = authReq.indexOf(param);
|
||||
if (_begin == -1)
|
||||
return "";
|
||||
return authReq.substring(_begin + param.length(), authReq.indexOf(delimit, _begin + param.length()));
|
||||
}
|
||||
|
||||
const String PsychicRequest::_getRandomHexString()
|
||||
{
|
||||
char buffer[33]; // buffer to hold 32 Hex Digit + /0
|
||||
int i;
|
||||
for (i = 0; i < 4; i++)
|
||||
{
|
||||
sprintf(buffer + (i * 8), "%08lx", esp_random());
|
||||
}
|
||||
return String(buffer);
|
||||
}
|
||||
|
||||
esp_err_t PsychicRequest::requestAuthentication(HTTPAuthMethod mode, const char *realm, const char *authFailMsg)
|
||||
{
|
||||
// what is thy realm, sire?
|
||||
if (!strcmp(realm, ""))
|
||||
this->setSessionKey("realm", "Login Required");
|
||||
else
|
||||
this->setSessionKey("realm", realm);
|
||||
|
||||
PsychicResponse response(this);
|
||||
String authStr;
|
||||
|
||||
// what kind of auth?
|
||||
if (mode == BASIC_AUTH)
|
||||
{
|
||||
authStr = "Basic realm=\"" + this->getSessionKey("realm") + "\"";
|
||||
response.addHeader("WWW-Authenticate", authStr.c_str());
|
||||
}
|
||||
else
|
||||
{
|
||||
// only make new ones if we havent sent them yet
|
||||
if (this->getSessionKey("nonce").isEmpty())
|
||||
this->setSessionKey("nonce", _getRandomHexString());
|
||||
if (this->getSessionKey("opaque").isEmpty())
|
||||
this->setSessionKey("opaque", _getRandomHexString());
|
||||
|
||||
authStr = "Digest realm=\"" + this->getSessionKey("realm") + "\", qop=\"auth\", nonce=\"" + this->getSessionKey("nonce") + "\", opaque=\"" + this->getSessionKey("opaque") + "\"";
|
||||
response.addHeader("WWW-Authenticate", authStr.c_str());
|
||||
}
|
||||
|
||||
// DUMP(authStr);
|
||||
|
||||
response.setCode(401);
|
||||
response.setContentType("text/html");
|
||||
response.setContent(authStr.c_str());
|
||||
return response.send();
|
||||
}
|
||||
|
||||
esp_err_t PsychicRequest::reply(int code)
|
||||
{
|
||||
PsychicResponse response(this);
|
||||
|
||||
response.setCode(code);
|
||||
response.setContentType("text/plain");
|
||||
response.setContent(http_status_reason(code));
|
||||
|
||||
return response.send();
|
||||
}
|
||||
|
||||
esp_err_t PsychicRequest::reply(const char *content)
|
||||
{
|
||||
PsychicResponse response(this);
|
||||
|
||||
response.setCode(200);
|
||||
response.setContentType("text/html");
|
||||
response.setContent(content);
|
||||
|
||||
return response.send();
|
||||
}
|
||||
|
||||
esp_err_t PsychicRequest::reply(int code, const char *contentType, const char *content)
|
||||
{
|
||||
PsychicResponse response(this);
|
||||
|
||||
response.setCode(code);
|
||||
response.setContentType(contentType);
|
||||
response.setContent(content);
|
||||
|
||||
return response.send();
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
#ifndef PsychicRequest_h
|
||||
#define PsychicRequest_h
|
||||
|
||||
#include "PsychicCore.h"
|
||||
#include "PsychicHttpServer.h"
|
||||
#include "PsychicClient.h"
|
||||
#include "PsychicWebParameter.h"
|
||||
#include "PsychicResponse.h"
|
||||
|
||||
typedef std::map<String, String> SessionData;
|
||||
|
||||
enum Disposition { NONE, INLINE, ATTACHMENT, FORM_DATA};
|
||||
|
||||
struct ContentDisposition {
|
||||
Disposition disposition;
|
||||
String filename;
|
||||
String name;
|
||||
};
|
||||
|
||||
class PsychicRequest {
|
||||
friend PsychicHttpServer;
|
||||
|
||||
protected:
|
||||
PsychicHttpServer *_server;
|
||||
httpd_req_t *_req;
|
||||
SessionData *_session;
|
||||
PsychicClient *_client;
|
||||
|
||||
http_method _method;
|
||||
String _uri;
|
||||
String _query;
|
||||
String _body;
|
||||
|
||||
std::list<PsychicWebParameter*> _params;
|
||||
|
||||
void _addParams(const String& params);
|
||||
void _parseGETParams();
|
||||
void _parsePOSTParams();
|
||||
|
||||
const String _extractParam(const String& authReq, const String& param, const char delimit);
|
||||
const String _getRandomHexString();
|
||||
|
||||
public:
|
||||
PsychicRequest(PsychicHttpServer *server, httpd_req_t *req);
|
||||
virtual ~PsychicRequest();
|
||||
|
||||
void *_tempObject;
|
||||
|
||||
PsychicHttpServer * server();
|
||||
httpd_req_t * request();
|
||||
virtual PsychicClient * client();
|
||||
|
||||
bool isMultipart();
|
||||
esp_err_t loadBody();
|
||||
|
||||
const String header(const char *name);
|
||||
bool hasHeader(const char *name);
|
||||
|
||||
static void freeSession(void *ctx);
|
||||
bool hasSessionKey(const String& key);
|
||||
const String getSessionKey(const String& key);
|
||||
void setSessionKey(const String& key, const String& value);
|
||||
|
||||
bool hasCookie(const char * key);
|
||||
const String getCookie(const char * key);
|
||||
|
||||
http_method method(); // returns the HTTP method used as enum value (eg. HTTP_GET)
|
||||
const String methodStr(); // returns the HTTP method used as a string (eg. "GET")
|
||||
const String path(); // returns the request path (eg /page?foo=bar returns "/page")
|
||||
const String& uri(); // returns the full request uri (eg /page?foo=bar)
|
||||
const String& query(); // returns the request query data (eg /page?foo=bar returns "foo=bar")
|
||||
const String host(); // returns the requested host (request to http://psychic.local/foo will return "psychic.local")
|
||||
const String contentType(); // returns the Content-Type header value
|
||||
size_t contentLength(); // returns the Content-Length header value
|
||||
const String& body(); // returns the body of the request
|
||||
const ContentDisposition getContentDisposition();
|
||||
|
||||
const String& queryString() { return query(); } //compatability function. same as query()
|
||||
const String& url() { return uri(); } //compatability function. same as uri()
|
||||
|
||||
void loadParams();
|
||||
PsychicWebParameter * addParam(PsychicWebParameter *param);
|
||||
PsychicWebParameter * addParam(const String &name, const String &value, bool decode = true);
|
||||
bool hasParam(const char *key);
|
||||
PsychicWebParameter * getParam(const char *name);
|
||||
|
||||
const String getFilename();
|
||||
|
||||
bool authenticate(const char * username, const char * password);
|
||||
esp_err_t requestAuthentication(HTTPAuthMethod mode, const char* realm, const char* authFailMsg);
|
||||
|
||||
esp_err_t redirect(const char *url);
|
||||
esp_err_t reply(int code);
|
||||
esp_err_t reply(const char *content);
|
||||
esp_err_t reply(int code, const char *contentType, const char *content);
|
||||
};
|
||||
|
||||
#endif // PsychicRequest_h
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user