🚚 Moves firmware to src and include
This commit is contained in:
@@ -0,0 +1,101 @@
|
||||
#include <ap_service.h>
|
||||
|
||||
static const char *TAG = "APService";
|
||||
|
||||
APService::APService()
|
||||
: endpoint(APSettings::read, APSettings::update, this),
|
||||
_persistence(APSettings::read, APSettings::update, this, AP_SETTINGS_FILE) {
|
||||
addUpdateHandler([&](const String &originId) { reconfigureAP(); }, false);
|
||||
}
|
||||
|
||||
APService::~APService() {}
|
||||
|
||||
void APService::begin() { _persistence.readFromFS(); }
|
||||
|
||||
esp_err_t APService::getStatus(PsychicRequest *request) {
|
||||
PsychicJsonResponse response = PsychicJsonResponse(request, false);
|
||||
JsonObject root = response.getRoot();
|
||||
status(root);
|
||||
return response.send();
|
||||
}
|
||||
|
||||
void APService::status(JsonObject &root) {
|
||||
root["status"] = getAPNetworkStatus();
|
||||
root["ip_address"] = WiFi.softAPIP().toString();
|
||||
root["mac_address"] = WiFi.softAPmacAddress();
|
||||
root["station_num"] = WiFi.softAPgetStationNum();
|
||||
}
|
||||
|
||||
APNetworkStatus APService::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;
|
||||
}
|
||||
|
||||
void APService::reconfigureAP() {
|
||||
_lastManaged = millis() - MANAGE_NETWORK_DELAY;
|
||||
_reconfigureAp = true;
|
||||
_recoveryMode = false;
|
||||
}
|
||||
|
||||
void APService::recoveryMode() {
|
||||
ESP_LOGI(TAG, "Recovery Mode needed");
|
||||
_lastManaged = millis() - MANAGE_NETWORK_DELAY;
|
||||
_recoveryMode = true;
|
||||
_reconfigureAp = true;
|
||||
}
|
||||
|
||||
void APService::loop() {
|
||||
EXECUTE_EVERY_N_MS(MANAGE_NETWORK_DELAY, manageAP());
|
||||
handleDNS();
|
||||
}
|
||||
|
||||
void APService::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 APService::startAP() {
|
||||
ESP_LOGI(TAG, "Starting software access point: %s", state().ssid.c_str());
|
||||
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();
|
||||
ESP_LOGI(TAG, "Starting captive portal on %s", apIp.toString().c_str());
|
||||
_dnsServer = new DNSServer;
|
||||
_dnsServer->start(DNS_PORT, "*", apIp);
|
||||
}
|
||||
}
|
||||
|
||||
void APService::stopAP() {
|
||||
if (_dnsServer) {
|
||||
ESP_LOGI(TAG, "Stopping captive portal");
|
||||
_dnsServer->stop();
|
||||
delete _dnsServer;
|
||||
_dnsServer = nullptr;
|
||||
}
|
||||
ESP_LOGI(TAG, "Stopping AP");
|
||||
WiFi.softAPdisconnect(true);
|
||||
}
|
||||
|
||||
void APService::handleDNS() {
|
||||
if (_dnsServer) {
|
||||
_dnsServer->processNextRequest();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,176 @@
|
||||
#include <event_socket.h>
|
||||
|
||||
SemaphoreHandle_t clientSubscriptionsMutex = xSemaphoreCreateMutex();
|
||||
|
||||
EventSocket::EventSocket() {
|
||||
_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));
|
||||
}
|
||||
|
||||
void EventSocket::onWSOpen(PsychicWebSocketClient *client) {
|
||||
ESP_LOGI("EventSocket", "ws[%s][%u] connect", client->remoteIP().toString().c_str(), client->socket());
|
||||
}
|
||||
|
||||
void EventSocket::onWSClose(PsychicWebSocketClient *client) {
|
||||
xSemaphoreTake(clientSubscriptionsMutex, portMAX_DELAY);
|
||||
for (auto &event_subscriptions : client_subscriptions) {
|
||||
event_subscriptions.second.remove(client->socket());
|
||||
}
|
||||
xSemaphoreGive(clientSubscriptionsMutex);
|
||||
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);
|
||||
|
||||
JsonDocument doc;
|
||||
|
||||
#if USE_MSGPACK
|
||||
if (frame->type != HTTPD_WS_TYPE_BINARY) {
|
||||
ESP_LOGE("EventSocket", "Unsupported frame type: %d", frame->type);
|
||||
return ESP_OK;
|
||||
}
|
||||
if (deserializeMsgPack(doc, frame->payload, frame->len)) {
|
||||
ESP_LOGE("EventSocket", "Could not deserialize msgpack");
|
||||
return ESP_OK;
|
||||
};
|
||||
#else
|
||||
if (frame->type != HTTPD_WS_TYPE_TEXT) {
|
||||
ESP_LOGE("EventSocket", "Unsupported frame type: %d", frame->type);
|
||||
return ESP_OK;
|
||||
}
|
||||
if (deserializeJson(doc, frame->payload, frame->len)) {
|
||||
ESP_LOGE("EventSocket", "Could not deserialize json");
|
||||
return ESP_OK;
|
||||
};
|
||||
#endif
|
||||
|
||||
auto msg = doc.as<JsonArray>();
|
||||
|
||||
message_type_t message_type = static_cast<message_type_t>(msg[0].as<uint8_t>());
|
||||
|
||||
if (message_type == PONG) {
|
||||
ESP_LOGV("EventSocket", "Pong");
|
||||
return ESP_OK;
|
||||
} else if (message_type == PING) {
|
||||
ESP_LOGV("EventSocket", "Ping");
|
||||
#if USE_MSGPACK
|
||||
const uint8_t out[] = {0x91, 0x04};
|
||||
send(request->client(), reinterpret_cast<const char *>(out), sizeof(out));
|
||||
#else
|
||||
const char *out = "[4]";
|
||||
send(request->client(), out, strlen(out));
|
||||
#endif
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
const char *event = msg[1].as<const char *>();
|
||||
|
||||
if (!event) {
|
||||
ESP_LOGE("EventSocket", "Invalid event name");
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
if (message_type == CONNECT) {
|
||||
ESP_LOGV("EventSocket", "Connect: %s", event);
|
||||
client_subscriptions[event].push_back(request->client()->socket());
|
||||
handleSubscribeCallbacks(event, String(request->client()->socket()));
|
||||
} else if (message_type == DISCONNECT) {
|
||||
ESP_LOGV("EventSocket", "Disconnect: %s", event);
|
||||
client_subscriptions[event].remove(request->client()->socket());
|
||||
} else if (message_type == EVENT) {
|
||||
JsonVariant payload = msg[2].as<JsonVariant>();
|
||||
handleEventCallbacks(event, payload, request->client()->socket());
|
||||
return ESP_OK;
|
||||
}
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
bool EventSocket::hasSubscribers(const char *event) { return !client_subscriptions[event].empty(); }
|
||||
|
||||
void EventSocket::emit(const char *event, JsonVariant &payload, const char *originId, bool onlyToSameOrigin) {
|
||||
int originSubscriptionId = originId[0] ? atoi(originId) : -1;
|
||||
xSemaphoreTake(clientSubscriptionsMutex, portMAX_DELAY);
|
||||
auto &subscriptions = client_subscriptions[event];
|
||||
if (subscriptions.empty()) {
|
||||
xSemaphoreGive(clientSubscriptionsMutex);
|
||||
return;
|
||||
}
|
||||
|
||||
JsonDocument doc;
|
||||
auto a = doc.to<JsonArray>();
|
||||
a.add(static_cast<uint8_t>(message_type_t::EVENT));
|
||||
a.add(event);
|
||||
a.add(payload);
|
||||
|
||||
#if USE_MSGPACK
|
||||
static char out[512];
|
||||
size_t len = serializeMsgPack(doc, out, sizeof(out));
|
||||
if (len == 0 || len >= sizeof(out)) {
|
||||
xSemaphoreGive(clientSubscriptionsMutex);
|
||||
ESP_LOGE("EventSocket", "Message payload bigger than buffer (%d <= %d)", sizeof(out), len);
|
||||
return;
|
||||
}
|
||||
const char *data = out;
|
||||
#else
|
||||
static char out[1024];
|
||||
size_t len = serializeJson(doc, out, sizeof(out));
|
||||
if (len == 0 || len >= sizeof(out)) {
|
||||
xSemaphoreGive(clientSubscriptionsMutex);
|
||||
ESP_LOGE("EventSocket", "Message payload bigger than buffer (%d <= %d)", sizeof(out), len);
|
||||
return;
|
||||
}
|
||||
const char *data = out;
|
||||
#endif
|
||||
|
||||
auto sendTo = [&](int id) {
|
||||
if (auto *c = _socket.getClient(id)) {
|
||||
send(c, data, len);
|
||||
} else {
|
||||
subscriptions.remove(id);
|
||||
}
|
||||
};
|
||||
|
||||
if (onlyToSameOrigin && originSubscriptionId > 0) {
|
||||
sendTo(originSubscriptionId);
|
||||
} else {
|
||||
for (int id : subscriptions) {
|
||||
if (id != originSubscriptionId) sendTo(id);
|
||||
}
|
||||
}
|
||||
xSemaphoreGive(clientSubscriptionsMutex);
|
||||
}
|
||||
|
||||
void EventSocket::send(PsychicWebSocketClient *client, const char *data, size_t len) {
|
||||
if (!client) return;
|
||||
|
||||
#if USE_MSGPACK
|
||||
client->sendMessage(HTTPD_WS_TYPE_BINARY, data, len);
|
||||
#else
|
||||
client->sendMessage(HTTPD_WS_TYPE_TEXT, data, len);
|
||||
#endif
|
||||
}
|
||||
|
||||
void EventSocket::handleEventCallbacks(String event, JsonVariant &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) {
|
||||
event_callbacks[event].push_back(std::move(callback));
|
||||
}
|
||||
|
||||
void EventSocket::onSubscribe(String event, SubscribeCallback callback) {
|
||||
subscribe_callbacks[event].push_back(std::move(callback));
|
||||
}
|
||||
|
||||
EventSocket socket;
|
||||
@@ -0,0 +1,65 @@
|
||||
#include <features.h>
|
||||
|
||||
namespace feature_service {
|
||||
|
||||
// New function to print all feature flags to log
|
||||
void printFeatureConfiguration() {
|
||||
ESP_LOGI("Features", "====================== FEATURE FLAGS ======================");
|
||||
ESP_LOGI("Features", "Firmware version: %s, name: %s, target: %s", APP_VERSION, APP_NAME, BUILD_TARGET);
|
||||
|
||||
// Core features
|
||||
ESP_LOGI("Features", "USE_UPLOAD_FIRMWARE: %s", USE_UPLOAD_FIRMWARE ? "enabled" : "disabled");
|
||||
ESP_LOGI("Features", "USE_DOWNLOAD_FIRMWARE: %s", USE_DOWNLOAD_FIRMWARE ? "enabled" : "disabled");
|
||||
ESP_LOGI("Features", "USE_SLEEP: %s", USE_SLEEP ? "enabled" : "disabled");
|
||||
ESP_LOGI("Features", "USE_CAMERA: %s", USE_CAMERA ? "enabled" : "disabled");
|
||||
ESP_LOGI("Features", "USE_MOTION: %s", USE_MOTION ? "enabled" : "disabled");
|
||||
|
||||
// Sensors
|
||||
ESP_LOGI("Features", "USE_BNO055: %s", USE_BNO055 ? "enabled" : "disabled");
|
||||
ESP_LOGI("Features", "USE_MPU6050: %s", USE_MPU6050 ? "enabled" : "disabled");
|
||||
ESP_LOGI("Features", "USE_HMC5883: %s", USE_HMC5883 ? "enabled" : "disabled");
|
||||
ESP_LOGI("Features", "USE_BMP180: %s", USE_BMP180 ? "enabled" : "disabled");
|
||||
ESP_LOGI("Features", "USE_USS: %s", USE_USS ? "enabled" : "disabled");
|
||||
ESP_LOGI("Features", "USE_GPS: %s", USE_GPS ? "enabled" : "disabled");
|
||||
|
||||
// Peripherals
|
||||
ESP_LOGI("Features", "USE_PCA9685: %s", USE_PCA9685 ? "enabled" : "disabled");
|
||||
ESP_LOGI("Features", "USE_WS2812: %s", USE_WS2812 ? "enabled" : "disabled");
|
||||
|
||||
// Web services
|
||||
ESP_LOGI("Features", "USE_MDNS: %s", USE_MDNS ? "enabled" : "disabled");
|
||||
ESP_LOGI("Features", "EMBED_WWW: %s", EMBED_WWW ? "enabled" : "disabled");
|
||||
ESP_LOGI("Features", "ENABLE_CORS: %s", ENABLE_CORS ? "enabled" : "disabled");
|
||||
ESP_LOGI("Features", "SERVE_CONFIG_FILES: %s", SERVE_CONFIG_FILES ? "enabled" : "disabled");
|
||||
ESP_LOGI("Features", "==========================================================");
|
||||
}
|
||||
|
||||
void features(JsonObject &root) {
|
||||
root["upload_firmware"] = USE_UPLOAD_FIRMWARE;
|
||||
root["download_firmware"] = USE_DOWNLOAD_FIRMWARE;
|
||||
root["sleep"] = USE_SLEEP;
|
||||
root["camera"] = USE_CAMERA;
|
||||
root["imu"] = USE_MPU6050 || USE_BNO055;
|
||||
root["mag"] = USE_HMC5883 || USE_BNO055;
|
||||
root["bmp"] = USE_BMP180;
|
||||
root["sonar"] = USE_USS;
|
||||
root["motion"] = USE_MOTION;
|
||||
root["servo"] = USE_PCA9685;
|
||||
root["ws2812"] = USE_WS2812;
|
||||
root["mdns"] = USE_MDNS;
|
||||
root["embed_www"] = EMBED_WWW;
|
||||
root["enable_cors"] = ENABLE_CORS;
|
||||
root["serve_config_files"] = SERVE_CONFIG_FILES;
|
||||
root["firmware_version"] = APP_VERSION;
|
||||
root["firmware_name"] = APP_NAME;
|
||||
root["firmware_built_target"] = BUILD_TARGET;
|
||||
}
|
||||
|
||||
esp_err_t getFeatures(PsychicRequest *request) {
|
||||
PsychicJsonResponse response = PsychicJsonResponse(request, false);
|
||||
JsonObject root = response.getRoot();
|
||||
features(root);
|
||||
return response.send();
|
||||
}
|
||||
|
||||
} // namespace feature_service
|
||||
@@ -0,0 +1,112 @@
|
||||
#include <filesystem.h>
|
||||
|
||||
static const char *TAG = "FileService";
|
||||
|
||||
namespace FileSystem {
|
||||
|
||||
PsychicUploadHandler *uploadHandler;
|
||||
|
||||
class Initializer {
|
||||
public:
|
||||
Initializer() {
|
||||
uploadHandler = new PsychicUploadHandler();
|
||||
uploadHandler->onUpload(uploadFile);
|
||||
uploadHandler->onRequest([](PsychicRequest *request) { return request->reply(200); });
|
||||
}
|
||||
};
|
||||
|
||||
static Initializer initializer;
|
||||
|
||||
esp_err_t getFiles(PsychicRequest *request) { return request->reply(200, "application/json", listFiles("/").c_str()); }
|
||||
|
||||
esp_err_t handleDelete(PsychicRequest *request, JsonVariant &json) {
|
||||
if (json.is<JsonObject>()) {
|
||||
const char *filename = json["file"].as<const char *>();
|
||||
ESP_LOGI(TAG, "Deleting file: %s", filename);
|
||||
return deleteFile(filename) ? request->reply(200) : request->reply(500);
|
||||
}
|
||||
return request->reply(400);
|
||||
}
|
||||
|
||||
esp_err_t handleEdit(PsychicRequest *request, JsonVariant &json) {
|
||||
if (json.is<JsonObject>()) {
|
||||
const char *filename = json["file"].as<const char *>();
|
||||
const char *content = json["content"].as<const char *>();
|
||||
ESP_LOGI(TAG, "Editing file: %s", filename);
|
||||
return editFile(filename, content) ? request->reply(200) : request->reply(500);
|
||||
}
|
||||
return request->reply(400);
|
||||
}
|
||||
|
||||
/* Helpers */
|
||||
|
||||
bool deleteFile(const char *filename) { return ESPFS.remove(filename); }
|
||||
|
||||
String listFiles(const String &directory, bool isRoot) {
|
||||
File root = ESPFS.open(directory.startsWith("/") ? directory : "/" + directory);
|
||||
if (!root.isDirectory()) return "{}";
|
||||
|
||||
File file = root.openNextFile();
|
||||
if (!file) {
|
||||
return isRoot ? "{ \"root\": {} }" : "{}";
|
||||
}
|
||||
|
||||
String output = isRoot ? "{ \"root\": {" : "{";
|
||||
|
||||
while (file) {
|
||||
String name = String(file.name());
|
||||
if (file.isDirectory()) {
|
||||
output += "\"" + name + "\": " + listFiles(name, false);
|
||||
} else {
|
||||
output += "\"" + name + "\": " + String(file.size());
|
||||
}
|
||||
|
||||
File next = root.openNextFile();
|
||||
if (next) output += ", ";
|
||||
file = next;
|
||||
}
|
||||
|
||||
output += "}";
|
||||
if (isRoot) output += "}";
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
esp_err_t uploadFile(PsychicRequest *request, const String &filename, uint64_t index, uint8_t *data, size_t len,
|
||||
bool last) {
|
||||
File file;
|
||||
String path = "/www/" + filename;
|
||||
ESP_LOGI(TAG, "Writing %d/%d bytes to: %s\n", (int)index + (int)len, request->contentLength(), path.c_str());
|
||||
|
||||
if (last) ESP_LOGI(TAG, "%s is finished. Total bytes: %d\n", path.c_str(), (int)index + (int)len);
|
||||
|
||||
file = LittleFS.open(path, !index ? FILE_WRITE : FILE_APPEND);
|
||||
if (!file) {
|
||||
ESP_LOGE(TAG, "Failed to open file");
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
if (!file.write(data, len)) {
|
||||
ESP_LOGE(TAG, "Write failed");
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
bool editFile(const char *filename, const char *content) {
|
||||
File file = ESPFS.open(filename, FILE_WRITE);
|
||||
if (!file) return false;
|
||||
|
||||
file.print(content);
|
||||
file.close();
|
||||
return true;
|
||||
}
|
||||
|
||||
esp_err_t mkdir(PsychicRequest *request, JsonVariant &json) {
|
||||
const char *path = json["path"].as<const char *>();
|
||||
ESP_LOGI(TAG, "Creating directory: %s", path);
|
||||
return ESPFS.mkdir(path) ? request->reply(200) : request->reply(500);
|
||||
}
|
||||
|
||||
} // namespace FileSystem
|
||||
@@ -0,0 +1,105 @@
|
||||
/**
|
||||
* 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 <firmware_download_service.h>
|
||||
|
||||
extern const uint8_t rootca_crt_bundle_start[] asm("_binary_src_certs_x509_crt_bundle_bin_start");
|
||||
|
||||
static int previousProgress = 0;
|
||||
JsonVariant obj;
|
||||
|
||||
void update_started() {
|
||||
obj["status"] = "preparing";
|
||||
socket.emit(EVENT_DOWNLOAD_OTA, obj);
|
||||
}
|
||||
|
||||
void update_progress(int currentBytes, int totalBytes) {
|
||||
obj["status"] = "progress";
|
||||
int progress = ((currentBytes * 100) / totalBytes);
|
||||
if (progress > previousProgress) {
|
||||
obj["progress"] = progress;
|
||||
socket.emit(EVENT_DOWNLOAD_OTA, obj);
|
||||
ESP_LOGV("Download OTA", "HTTP update process at %d of %d bytes... (%d %%)", currentBytes, totalBytes,
|
||||
progress);
|
||||
}
|
||||
previousProgress = progress;
|
||||
}
|
||||
|
||||
void update_finished() {
|
||||
obj["status"] = "finished";
|
||||
socket.emit(EVENT_DOWNLOAD_OTA, obj);
|
||||
|
||||
// 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);
|
||||
// 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:
|
||||
obj["status"] = "error";
|
||||
obj["error"] = httpUpdate.getLastErrorString().c_str();
|
||||
socket.emit(EVENT_DOWNLOAD_OTA, obj);
|
||||
|
||||
ESP_LOGE("Download OTA", "HTTP Update failed with error (%d): %s", httpUpdate.getLastError(),
|
||||
httpUpdate.getLastErrorString().c_str());
|
||||
break;
|
||||
case HTTP_UPDATE_NO_UPDATES:
|
||||
|
||||
obj["status"] = "error";
|
||||
obj["error"] = "Update failed, has same firmware version";
|
||||
socket.emit(EVENT_DOWNLOAD_OTA, obj);
|
||||
|
||||
ESP_LOGE("Download OTA", "HTTP Update failed, has same firmware version");
|
||||
break;
|
||||
case HTTP_UPDATE_OK: ESP_LOGI("Download OTA", "HTTP Update successful - Restarting"); break;
|
||||
}
|
||||
vTaskDelete(NULL);
|
||||
}
|
||||
|
||||
DownloadFirmwareService::DownloadFirmwareService() {}
|
||||
|
||||
esp_err_t DownloadFirmwareService::handleDownloadUpdate(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());
|
||||
|
||||
obj["status"] = "preparing";
|
||||
obj["progress"] = 0;
|
||||
obj["error"] = "";
|
||||
socket.emit(EVENT_DOWNLOAD_OTA, obj);
|
||||
|
||||
const BaseType_t taskResult = g_taskManager.createTask(&updateTask, "Firmware download", OTA_TASK_STACK_SIZE,
|
||||
&downloadURL, (configMAX_PRIORITIES - 1), NULL, 1);
|
||||
if (taskResult != pdPASS) {
|
||||
ESP_LOGE("Download OTA", "Couldn't create download OTA task");
|
||||
return request->reply(500);
|
||||
}
|
||||
return request->reply(200);
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
#include <firmware_upload_service.h>
|
||||
#include <esp_app_format.h>
|
||||
#include <esp_ota_ops.h>
|
||||
|
||||
static const char *TAG = "FirmwareUploadService";
|
||||
|
||||
using namespace std::placeholders;
|
||||
|
||||
static char md5[33] = "\0";
|
||||
static size_t fsize = 0;
|
||||
|
||||
static FileType fileType = ft_none;
|
||||
|
||||
FirmwareUploadService::FirmwareUploadService() {}
|
||||
|
||||
void FirmwareUploadService::begin() {
|
||||
uploadHandler.onUpload(std::bind(&FirmwareUploadService::handleUpload, this, _1, _2, _3, _4, _5, _6));
|
||||
uploadHandler.onRequest(std::bind(&FirmwareUploadService::uploadComplete, this, _1));
|
||||
uploadHandler.onClose(std::bind(&FirmwareUploadService::handleEarlyDisconnect, this));
|
||||
}
|
||||
|
||||
esp_err_t FirmwareUploadService::handleUpload(PsychicRequest *request, const String &filename, uint64_t index,
|
||||
uint8_t *data, size_t len, bool final) {
|
||||
// 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);
|
||||
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))) {
|
||||
ESP_LOGI(TAG, "Starting update");
|
||||
if (strlen(md5) == 32) {
|
||||
Update.setMD5(md5);
|
||||
md5[0] = '\0';
|
||||
ESP_LOGI(TAG, "Setting MD5 hash");
|
||||
}
|
||||
} 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);
|
||||
} else {
|
||||
JsonVariant obj;
|
||||
obj["status"] = "progress";
|
||||
obj["progress"] = (float)Update.progress() / (float)fsize * 100.f;
|
||||
socket.emit("otastatus", obj);
|
||||
delay(20);
|
||||
}
|
||||
if (final) {
|
||||
if (!Update.end(true)) {
|
||||
handleError(request, 500);
|
||||
} else {
|
||||
JsonVariant obj;
|
||||
obj["status"] = "finished", obj["progress"] = 100;
|
||||
socket.emit("otastatus", obj);
|
||||
ESP_LOGI(TAG, "Finish writing update");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t FirmwareUploadService::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);
|
||||
JsonObject root = response.getRoot();
|
||||
root["md5"] = md5;
|
||||
return response.send();
|
||||
}
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
// if no error, send the success response
|
||||
if (!request->_tempObject) {
|
||||
ESP_LOGI(TAG, "Finish updating");
|
||||
system_service::restart();
|
||||
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 FirmwareUploadService::handleError(PsychicRequest *request, int code) {
|
||||
JsonVariant obj;
|
||||
obj["status"] = "error", obj["error"] = Update.getError();
|
||||
socket.emit("otastatus", obj);
|
||||
// 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 FirmwareUploadService::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,101 @@
|
||||
#include <mdns_service.h>
|
||||
|
||||
static const char *TAG = "MDNSService";
|
||||
|
||||
MDNSService::MDNSService()
|
||||
: endpoint(MDNSSettings::read, MDNSSettings::update, this),
|
||||
_persistence(MDNSSettings::read, MDNSSettings::update, this, MDNS_SETTINGS_FILE),
|
||||
_started(false) {
|
||||
addUpdateHandler([&](const String &originId) { reconfigureMDNS(); }, false);
|
||||
}
|
||||
|
||||
MDNSService::~MDNSService() {
|
||||
if (_started) {
|
||||
stopMDNS();
|
||||
}
|
||||
}
|
||||
|
||||
void MDNSService::begin() {
|
||||
_persistence.readFromFS();
|
||||
startMDNS();
|
||||
}
|
||||
|
||||
void MDNSService::reconfigureMDNS() {
|
||||
if (_started) {
|
||||
stopMDNS();
|
||||
}
|
||||
startMDNS();
|
||||
}
|
||||
|
||||
void MDNSService::startMDNS() {
|
||||
ESP_LOGV(TAG, "Starting MDNS with hostname: %s", state().hostname.c_str());
|
||||
|
||||
if (MDNS.begin(state().hostname.c_str())) {
|
||||
_started = true;
|
||||
MDNS.setInstanceName(state().instance.c_str());
|
||||
|
||||
addServices();
|
||||
|
||||
ESP_LOGI(TAG, "MDNS started successfully with hostname: %s", state().hostname.c_str());
|
||||
} else {
|
||||
_started = false;
|
||||
ESP_LOGE(TAG, "Failed to start MDNS");
|
||||
}
|
||||
}
|
||||
|
||||
void MDNSService::stopMDNS() {
|
||||
ESP_LOGV(TAG, "Stopping MDNS");
|
||||
MDNS.end();
|
||||
_started = false;
|
||||
}
|
||||
|
||||
void MDNSService::addServices() {
|
||||
for (const auto &service : state().services) {
|
||||
MDNS.addService(service.service.c_str(), service.protocol.c_str(), service.port);
|
||||
|
||||
for (const auto &txt : service.txtRecords) {
|
||||
MDNS.addServiceTxt(service.service.c_str(), service.protocol.c_str(), txt.key.c_str(), txt.value.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
for (const auto &txt : state().globalTxtRecords) {
|
||||
for (const auto &service : state().services) {
|
||||
MDNS.addServiceTxt(service.service.c_str(), service.protocol.c_str(), txt.key.c_str(), txt.value.c_str());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
esp_err_t MDNSService::getStatus(PsychicRequest *request) {
|
||||
PsychicJsonResponse response = PsychicJsonResponse(request, false);
|
||||
JsonVariant root = response.getRoot();
|
||||
getStatus(root);
|
||||
return response.send();
|
||||
}
|
||||
|
||||
void MDNSService::getStatus(JsonVariant &root) {
|
||||
state().read(state(), root);
|
||||
root["started"] = _started;
|
||||
}
|
||||
|
||||
esp_err_t MDNSService::queryServices(PsychicRequest *request, JsonVariant &json) {
|
||||
String service = json["service"].as<String>();
|
||||
String proto = json["protocol"].as<String>();
|
||||
|
||||
PsychicJsonResponse response = PsychicJsonResponse(request, false);
|
||||
JsonVariant root = response.getRoot();
|
||||
|
||||
ESP_LOGI(TAG, "Querying for service: %s, protocol: %s", service.c_str(), proto.c_str());
|
||||
|
||||
int n = MDNS.queryService(service.c_str(), proto.c_str());
|
||||
ESP_LOGI(TAG, "Found %d services", n);
|
||||
|
||||
JsonArray servicesArray = root["services"].to<JsonArray>();
|
||||
for (int i = 0; i < n; i++) {
|
||||
JsonVariant serviceObj = servicesArray.add<JsonVariant>();
|
||||
serviceObj["name"] = MDNS.hostname(i);
|
||||
serviceObj["ip"] = MDNS.IP(i);
|
||||
serviceObj["port"] = MDNS.port(i);
|
||||
}
|
||||
|
||||
return response.send();
|
||||
}
|
||||
@@ -0,0 +1,198 @@
|
||||
#include <peripherals/camera_service.h>
|
||||
|
||||
namespace Camera {
|
||||
|
||||
static const char *const TAG = "CameraService";
|
||||
|
||||
static const char *_STREAM_CONTENT_TYPE = "multipart/x-mixed-replace;boundary=" PART_BOUNDARY;
|
||||
static const char *_STREAM_BOUNDARY = "\r\n--" PART_BOUNDARY "\r\n";
|
||||
static const char *_STREAM_PART = "Content-Type: image/jpeg\r\nContent-Length: %u\r\n\r\n";
|
||||
|
||||
SemaphoreHandle_t cameraMutex = xSemaphoreCreateMutex();
|
||||
|
||||
camera_fb_t *safe_camera_fb_get() {
|
||||
camera_fb_t *fb = NULL;
|
||||
if (xSemaphoreTakeRecursive(cameraMutex, portMAX_DELAY) == pdTRUE) {
|
||||
fb = esp_camera_fb_get();
|
||||
xSemaphoreGiveRecursive(cameraMutex);
|
||||
}
|
||||
return fb;
|
||||
}
|
||||
|
||||
sensor_t *safe_sensor_get() {
|
||||
sensor_t *s = NULL;
|
||||
if (xSemaphoreTakeRecursive(cameraMutex, portMAX_DELAY) == pdTRUE) {
|
||||
s = esp_camera_sensor_get();
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
void safe_sensor_return() { xSemaphoreGiveRecursive(cameraMutex); }
|
||||
|
||||
CameraService::CameraService()
|
||||
: endpoint(CameraSettings::read, CameraSettings::update, this),
|
||||
_eventEndpoint(CameraSettings::read, CameraSettings::update, this, EVENT_CAMERA_SETTINGS),
|
||||
_persistence(CameraSettings::read, CameraSettings::update, this, CAMERA_SETTINGS_FILE) {
|
||||
addUpdateHandler([&](const String &originId) { updateCamera(); }, false);
|
||||
}
|
||||
|
||||
esp_err_t CameraService::begin() {
|
||||
_eventEndpoint.begin();
|
||||
_persistence.readFromFS();
|
||||
camera_config_t camera_config;
|
||||
camera_config.ledc_channel = LEDC_CHANNEL_0;
|
||||
camera_config.ledc_timer = LEDC_TIMER_0;
|
||||
#if FT_ENABLED(USE_CAMERA)
|
||||
camera_config.pin_d0 = Y2_GPIO_NUM;
|
||||
camera_config.pin_d1 = Y3_GPIO_NUM;
|
||||
camera_config.pin_d2 = Y4_GPIO_NUM;
|
||||
camera_config.pin_d3 = Y5_GPIO_NUM;
|
||||
camera_config.pin_d4 = Y6_GPIO_NUM;
|
||||
camera_config.pin_d5 = Y7_GPIO_NUM;
|
||||
camera_config.pin_d6 = Y8_GPIO_NUM;
|
||||
camera_config.pin_d7 = Y9_GPIO_NUM;
|
||||
camera_config.pin_xclk = XCLK_GPIO_NUM;
|
||||
camera_config.pin_pclk = PCLK_GPIO_NUM;
|
||||
camera_config.pin_vsync = VSYNC_GPIO_NUM;
|
||||
camera_config.pin_href = HREF_GPIO_NUM;
|
||||
camera_config.pin_sccb_sda = SIOD_GPIO_NUM;
|
||||
camera_config.pin_sccb_scl = SIOC_GPIO_NUM;
|
||||
camera_config.pin_pwdn = PWDN_GPIO_NUM;
|
||||
camera_config.pin_reset = RESET_GPIO_NUM;
|
||||
#endif
|
||||
camera_config.xclk_freq_hz = 20000000;
|
||||
camera_config.pixel_format = PIXFORMAT_JPEG;
|
||||
|
||||
if (psramFound()) {
|
||||
camera_config.frame_size = FRAMESIZE_SVGA;
|
||||
camera_config.jpeg_quality = 10;
|
||||
camera_config.fb_location = CAMERA_FB_IN_PSRAM;
|
||||
camera_config.fb_count = 2;
|
||||
camera_config.grab_mode = CAMERA_GRAB_LATEST;
|
||||
} else {
|
||||
camera_config.frame_size = FRAMESIZE_SVGA;
|
||||
camera_config.jpeg_quality = 12;
|
||||
camera_config.fb_count = 1;
|
||||
}
|
||||
|
||||
log_i("Initializing camera");
|
||||
esp_err_t err = esp_camera_init(&camera_config);
|
||||
if (err == ESP_OK)
|
||||
ESP_LOGI(TAG, "Camera probe successful");
|
||||
else
|
||||
ESP_LOGE(TAG, "Camera probe failed with error 0x%x", err);
|
||||
|
||||
return err;
|
||||
}
|
||||
|
||||
esp_err_t CameraService::cameraStill(PsychicRequest *request) {
|
||||
camera_fb_t *fb = safe_camera_fb_get();
|
||||
if (!fb) {
|
||||
ESP_LOGE(TAG, "Camera capture failed");
|
||||
request->reply(500, "text/plain", "Camera capture failed");
|
||||
return ESP_FAIL;
|
||||
}
|
||||
PsychicStreamResponse response = PsychicStreamResponse(request, "image/jpeg", "capture.jpg");
|
||||
response.beginSend();
|
||||
response.write(fb->buf, fb->len);
|
||||
esp_camera_fb_return(fb);
|
||||
return response.endSend();
|
||||
}
|
||||
|
||||
void streamTask(void *pv) {
|
||||
esp_err_t res = ESP_OK;
|
||||
|
||||
PsychicRequest *request = static_cast<PsychicRequest *>(pv);
|
||||
|
||||
httpd_req_t *copy = nullptr;
|
||||
res = httpd_req_async_handler_begin(request->request(), ©);
|
||||
if (res != ESP_OK) {
|
||||
return;
|
||||
}
|
||||
PsychicHttpServer *server = request->server();
|
||||
PsychicRequest new_request = PsychicRequest(server, copy);
|
||||
request = &new_request;
|
||||
|
||||
PsychicStreamResponse response = PsychicStreamResponse(request, _STREAM_CONTENT_TYPE);
|
||||
camera_fb_t *fb = NULL;
|
||||
|
||||
char *part_buf[64];
|
||||
size_t buf_len = 0;
|
||||
uint8_t *buf = NULL;
|
||||
int64_t fr_start = esp_timer_get_time();
|
||||
|
||||
response.beginSend();
|
||||
|
||||
for (;;) {
|
||||
fb = safe_camera_fb_get();
|
||||
if (!fb) {
|
||||
ESP_LOGE("Stream", "Camera capture failed");
|
||||
break;
|
||||
}
|
||||
if (fb->format != PIXFORMAT_JPEG) {
|
||||
if (!frame2jpg(fb, 80, &buf, &buf_len)) break;
|
||||
} else {
|
||||
buf_len = fb->len;
|
||||
buf = fb->buf;
|
||||
}
|
||||
|
||||
size_t hlen = snprintf((char *)part_buf, 64, _STREAM_PART, buf_len);
|
||||
size_t w = response.write((const char *)part_buf, hlen);
|
||||
w += response.write((const char *)buf, buf_len);
|
||||
w += response.write((char *)_STREAM_BOUNDARY, strlen(_STREAM_BOUNDARY));
|
||||
if (w == 62) break;
|
||||
esp_camera_fb_return(fb);
|
||||
safe_sensor_return();
|
||||
buf = NULL;
|
||||
taskYIELD();
|
||||
int64_t delay = 30000ll - esp_timer_get_time() - fr_start;
|
||||
if (delay > 0) vTaskDelay(pdMS_TO_TICKS(delay));
|
||||
}
|
||||
ESP_LOGI("Stream", "Stream ended");
|
||||
response.endSend();
|
||||
httpd_req_async_handler_complete(copy);
|
||||
vTaskDelete(NULL);
|
||||
}
|
||||
|
||||
esp_err_t CameraService::cameraStream(PsychicRequest *request) {
|
||||
g_taskManager.createTask(streamTask, "Stream client task", 4096, request, 4);
|
||||
vTaskDelay(pdMS_TO_TICKS(100));
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
void CameraService::updateCamera() {
|
||||
ESP_LOGI("CameraSettings", "Updating camera settings");
|
||||
sensor_t *s = safe_sensor_get();
|
||||
if (!s) {
|
||||
ESP_LOGE("CameraSettings", "Failed to update camera settings");
|
||||
safe_sensor_return();
|
||||
return;
|
||||
}
|
||||
s->set_pixformat(s, state().pixformat);
|
||||
s->set_framesize(s, state().framesize);
|
||||
s->set_brightness(s, state().brightness);
|
||||
s->set_contrast(s, state().contrast);
|
||||
s->set_saturation(s, state().saturation);
|
||||
s->set_sharpness(s, state().sharpness);
|
||||
s->set_denoise(s, state().denoise);
|
||||
s->set_gainceiling(s, state().gainceiling);
|
||||
s->set_quality(s, state().quality);
|
||||
s->set_colorbar(s, state().colorbar);
|
||||
s->set_awb_gain(s, state().awb_gain);
|
||||
s->set_wb_mode(s, state().wb_mode);
|
||||
s->set_aec2(s, state().aec2);
|
||||
s->set_ae_level(s, state().ae_level);
|
||||
s->set_aec_value(s, state().aec_value);
|
||||
s->set_agc_gain(s, state().agc_gain);
|
||||
s->set_bpc(s, state().bpc);
|
||||
s->set_wpc(s, state().wpc);
|
||||
s->set_special_effect(s, state().special_effect);
|
||||
s->set_raw_gma(s, state().raw_gma);
|
||||
s->set_lenc(s, state().lenc);
|
||||
s->set_hmirror(s, state().hmirror);
|
||||
s->set_vflip(s, state().vflip);
|
||||
s->set_dcw(s, state().dcw);
|
||||
safe_sensor_return();
|
||||
}
|
||||
|
||||
} // namespace Camera
|
||||
@@ -0,0 +1,182 @@
|
||||
#include "system_service.h"
|
||||
|
||||
namespace system_service {
|
||||
|
||||
static const char *TAG = "SystemService";
|
||||
|
||||
static JsonDocument analyticsDoc;
|
||||
static char analyticsMessage[MAX_ESP_ANALYTICS_SIZE];
|
||||
|
||||
esp_err_t handleReset(PsychicRequest *request) {
|
||||
reset();
|
||||
return request->reply(200);
|
||||
}
|
||||
|
||||
esp_err_t handleRestart(PsychicRequest *request) {
|
||||
restart();
|
||||
return request->reply(200);
|
||||
}
|
||||
|
||||
esp_err_t handleSleep(PsychicRequest *request) {
|
||||
sleep();
|
||||
return request->reply(200);
|
||||
}
|
||||
|
||||
esp_err_t getStatus(PsychicRequest *request) {
|
||||
PsychicJsonResponse response = PsychicJsonResponse(request, false);
|
||||
JsonObject root = response.getRoot();
|
||||
status(root);
|
||||
return response.send();
|
||||
}
|
||||
|
||||
esp_err_t getMetrics(PsychicRequest *request) {
|
||||
PsychicJsonResponse response = PsychicJsonResponse(request, false);
|
||||
JsonObject root = response.getRoot();
|
||||
metrics(root);
|
||||
return response.send();
|
||||
}
|
||||
|
||||
void reset() {
|
||||
ESP_LOGI(TAG, "Resetting device");
|
||||
File root = ESPFS.open(FS_CONFIG_DIRECTORY);
|
||||
File file;
|
||||
while (file = root.openNextFile()) {
|
||||
String path = file.path();
|
||||
file.close();
|
||||
ESPFS.remove(path);
|
||||
}
|
||||
restart();
|
||||
}
|
||||
|
||||
void restart() {
|
||||
xTaskCreate(
|
||||
[](void *pvParameters) {
|
||||
for (;;) {
|
||||
delay(250);
|
||||
MDNS.end();
|
||||
delay(100);
|
||||
WiFi.disconnect(true);
|
||||
delay(500);
|
||||
ESP.restart();
|
||||
}
|
||||
},
|
||||
"Restart task", 4096, nullptr, 10, nullptr);
|
||||
}
|
||||
|
||||
void sleep() {
|
||||
xTaskCreate(
|
||||
[](void *pvParameters) {
|
||||
for (;;) {
|
||||
delay(250);
|
||||
MDNS.end();
|
||||
delay(100);
|
||||
WiFi.disconnect(true);
|
||||
delay(500);
|
||||
|
||||
uint64_t bitmask = (uint64_t)1 << (WAKEUP_PIN_NUMBER);
|
||||
|
||||
#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
|
||||
esp_deep_sleep_start();
|
||||
}
|
||||
},
|
||||
"Sleep task", 4096, nullptr, 10, nullptr);
|
||||
ESP_LOGI(TAG, "Setting device to sleep");
|
||||
}
|
||||
|
||||
void status(JsonObject &root) {
|
||||
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"] = resetReason(esp_reset_reason());
|
||||
root["uptime"] = millis() / 1000;
|
||||
}
|
||||
|
||||
void metrics(JsonObject &root) {
|
||||
root["uptime"] = millis() / 1000;
|
||||
root["free_heap"] = ESP.getFreeHeap();
|
||||
root["total_heap"] = ESP.getHeapSize();
|
||||
root["min_free_heap"] = ESP.getMinFreeHeap();
|
||||
root["max_alloc_heap"] = ESP.getMaxAllocHeap();
|
||||
root["fs_used"] = ESPFS.usedBytes();
|
||||
root["fs_total"] = ESPFS.totalBytes();
|
||||
root["core_temp"] = temperatureRead();
|
||||
root["cpu0_usage"] = g_taskManager.getCpuUsage(0);
|
||||
root["cpu1_usage"] = g_taskManager.getCpuUsage(1);
|
||||
root["cpu_usage"] = g_taskManager.getCpuUsage();
|
||||
JsonArray tasks = root["tasks"].to<JsonArray>();
|
||||
for (auto const &task : g_taskManager.getTasks()) {
|
||||
JsonObject nested = tasks.add<JsonObject>();
|
||||
nested["name"] = task.name;
|
||||
nested["stackSize"] = task.stackSize;
|
||||
nested["priority"] = task.priority;
|
||||
nested["coreId"] = task.coreId;
|
||||
}
|
||||
}
|
||||
|
||||
void emitMetrics() {
|
||||
if (!socket.hasSubscribers(EVENT_ANALYTICS)) return;
|
||||
analyticsDoc.clear();
|
||||
JsonObject root = analyticsDoc.to<JsonObject>();
|
||||
system_service::metrics(root);
|
||||
JsonVariant data = analyticsDoc.as<JsonVariant>();
|
||||
socket.emit(EVENT_ANALYTICS, data);
|
||||
}
|
||||
|
||||
const char *resetReason(esp_reset_reason_t reason) {
|
||||
switch (reason) {
|
||||
case ESP_RST_UNKNOWN: return "Reset reason can not be determined";
|
||||
case ESP_RST_POWERON: return "Reset due to power-on event";
|
||||
case ESP_RST_EXT: return "Reset by external pin (not applicable for ESP32)";
|
||||
case ESP_RST_SW: return "Software reset via esp_restart";
|
||||
case ESP_RST_PANIC: return "Software reset due to exception/panic";
|
||||
case ESP_RST_INT_WDT: return "Reset (software or hardware) due to interrupt watchdog";
|
||||
case ESP_RST_TASK_WDT: return "Reset due to task watchdog";
|
||||
case ESP_RST_WDT: return "Reset due to other watchdogs";
|
||||
case ESP_RST_DEEPSLEEP: return "Reset after exiting deep sleep mode";
|
||||
case ESP_RST_BROWNOUT: return "Brownout reset (software or hardware)";
|
||||
case ESP_RST_SDIO: return "Reset over SDIO";
|
||||
#ifdef ESP_RST_USB
|
||||
case ESP_RST_USB: return "Reset by USB peripheral";
|
||||
#endif
|
||||
#ifdef ESP_RST_JTAG
|
||||
case ESP_RST_JTAG: return "Reset by JTAG";
|
||||
#endif
|
||||
#ifdef ESP_RST_EFUSE
|
||||
case ESP_RST_EFUSE: return "Reset due to efuse error";
|
||||
#endif
|
||||
#ifdef ESP_RST_PWR_GLITCH
|
||||
case ESP_RST_PWR_GLITCH: return "Reset due to power glitch detected";
|
||||
#endif
|
||||
#ifdef ESP_RST_CPU_LOCKUP
|
||||
case ESP_RST_CPU_LOCKUP: return "Reset due to CPU lock up (double exception)";
|
||||
#endif
|
||||
default:
|
||||
static char buffer[48];
|
||||
snprintf(buffer, sizeof(buffer), "Unknown reset reason (%d)", reason);
|
||||
return buffer;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace system_service
|
||||
@@ -0,0 +1,3 @@
|
||||
#include <task_manager.h>
|
||||
|
||||
TaskManager g_taskManager;
|
||||
@@ -0,0 +1,213 @@
|
||||
#include <wifi_service.h>
|
||||
|
||||
WiFiService::WiFiService()
|
||||
: endpoint(WiFiSettings::read, WiFiSettings::update, this),
|
||||
_persistence(WiFiSettings::read, WiFiSettings::update, this, WIFI_SETTINGS_FILE) {
|
||||
addUpdateHandler([&](const String &originId) { reconfigureWiFiConnection(); }, false);
|
||||
}
|
||||
|
||||
WiFiService::~WiFiService() {}
|
||||
|
||||
void WiFiService::begin() {
|
||||
WiFi.mode(WIFI_MODE_STA);
|
||||
|
||||
WiFi.persistent(false);
|
||||
WiFi.setAutoReconnect(false);
|
||||
|
||||
WiFi.onEvent(std::bind(&WiFiService::onStationModeDisconnected, this, std::placeholders::_1, std::placeholders::_2),
|
||||
WiFiEvent_t::ARDUINO_EVENT_WIFI_STA_DISCONNECTED);
|
||||
WiFi.onEvent(std::bind(&WiFiService::onStationModeStop, this, std::placeholders::_1, std::placeholders::_2),
|
||||
WiFiEvent_t::ARDUINO_EVENT_WIFI_STA_STOP);
|
||||
|
||||
WiFi.onEvent(onStationModeGotIP, WiFiEvent_t::ARDUINO_EVENT_WIFI_STA_GOT_IP);
|
||||
|
||||
_persistence.readFromFS();
|
||||
reconfigureWiFiConnection();
|
||||
|
||||
if (state().wifiSettings.size() == 1) {
|
||||
configureNetwork(state().wifiSettings[0]);
|
||||
delay(500);
|
||||
}
|
||||
}
|
||||
|
||||
void WiFiService::reconfigureWiFiConnection() {
|
||||
_lastConnectionAttempt = 0;
|
||||
|
||||
if (WiFi.disconnect(true)) _stopping = true;
|
||||
}
|
||||
|
||||
void WiFiService::loop() { EXECUTE_EVERY_N_MS(reconnectDelay, manageSTA()); }
|
||||
|
||||
esp_err_t WiFiService::handleScan(PsychicRequest *request) {
|
||||
if (WiFi.scanComplete() != -1) {
|
||||
WiFi.scanDelete();
|
||||
WiFi.scanNetworks(true);
|
||||
}
|
||||
return request->reply(202);
|
||||
}
|
||||
|
||||
esp_err_t WiFiService::getNetworks(PsychicRequest *request) {
|
||||
int numNetworks = WiFi.scanComplete();
|
||||
if (numNetworks == -1)
|
||||
return request->reply(202);
|
||||
else if (numNetworks < -1)
|
||||
return handleScan(request);
|
||||
|
||||
PsychicJsonResponse response = PsychicJsonResponse(request, false);
|
||||
JsonObject root = response.getRoot();
|
||||
getNetworks(root);
|
||||
return response.send();
|
||||
}
|
||||
|
||||
void WiFiService::setupMDNS(const char *hostname) {
|
||||
MDNS.begin(state().hostname.c_str());
|
||||
MDNS.setInstanceName(hostname);
|
||||
MDNS.addService("http", "tcp", 80);
|
||||
MDNS.addService("ws", "tcp", 80);
|
||||
MDNS.addServiceTxt("http", "tcp", "Firmware Version", APP_VERSION);
|
||||
}
|
||||
|
||||
void WiFiService::getNetworks(JsonObject &root) {
|
||||
JsonArray networks = root["networks"].to<JsonArray>();
|
||||
int numNetworks = WiFi.scanComplete();
|
||||
for (int i = 0; i < numNetworks; i++) {
|
||||
JsonObject network = networks.add<JsonObject>();
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
esp_err_t WiFiService::getNetworkStatus(PsychicRequest *request) {
|
||||
PsychicJsonResponse response = PsychicJsonResponse(request, false);
|
||||
JsonObject root = response.getRoot();
|
||||
getNetworkStatus(root);
|
||||
return response.send();
|
||||
}
|
||||
|
||||
void WiFiService::getNetworkStatus(JsonObject &root) {
|
||||
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 (dnsIP1 != INADDR_NONE) {
|
||||
root["dns_ip_1"] = dnsIP1.toString();
|
||||
}
|
||||
if (dnsIP2 != INADDR_NONE) {
|
||||
root["dns_ip_2"] = dnsIP2.toString();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void WiFiService::manageSTA() {
|
||||
if (WiFi.isConnected() || state().wifiSettings.empty()) return;
|
||||
if ((WiFi.getMode() & WIFI_STA) == 0) connectToWiFi();
|
||||
}
|
||||
|
||||
void WiFiService::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_LOGI("WiFiSettingsService", "No networks found.");
|
||||
} else {
|
||||
ESP_LOGI("WiFiSettingsService", "%d networks found.", scanResult);
|
||||
|
||||
// find the best network to connect
|
||||
wifi_settings_t *bestNetwork = nullptr;
|
||||
int32_t 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);
|
||||
|
||||
for (auto &network : state().wifiSettings) {
|
||||
if (ssid_scan == network.ssid) {
|
||||
if (rssi_scan >= FACTORY_WIFI_RSSI_THRESHOLD) {
|
||||
network.available = true;
|
||||
}
|
||||
if (rssi_scan > bestNetworkDb) {
|
||||
bestNetworkDb = rssi_scan;
|
||||
bestNetwork = &network;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!state().priorityBySignalStrength) {
|
||||
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 && 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 {
|
||||
ESP_LOGI("WiFiSettingsService", "No known networks found.");
|
||||
}
|
||||
|
||||
WiFi.scanDelete();
|
||||
}
|
||||
}
|
||||
|
||||
void WiFiService::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 WiFiService::onStationModeDisconnected(WiFiEvent_t event, WiFiEventInfo_t info) {
|
||||
WiFi.disconnect(true);
|
||||
ESP_LOGI("WiFiStatus", "WiFi Disconnected. Reason code=%d", info.wifi_sta_disconnected.reason);
|
||||
}
|
||||
|
||||
void WiFiService::onStationModeStop(WiFiEvent_t event, WiFiEventInfo_t info) {
|
||||
if (_stopping) {
|
||||
_lastConnectionAttempt = 0;
|
||||
_stopping = false;
|
||||
}
|
||||
ESP_LOGI("WiFiStatus", "WiFi Connected.");
|
||||
}
|
||||
|
||||
void WiFiService::onStationModeGotIP(WiFiEvent_t event, WiFiEventInfo_t info) {
|
||||
ESP_LOGI("WiFiStatus", "WiFi Got IP. localIP=%s, hostName=%s", WiFi.localIP().toString().c_str(),
|
||||
WiFi.getHostname());
|
||||
}
|
||||
Reference in New Issue
Block a user