15 Commits

Author SHA1 Message Date
Niklas Jensen 41e6ff9ba6 Fix task and added timings for write 2026-02-21 15:24:37 +01:00
Niklas Jensen 0ee459b1a7 Imrpove upload speed by spam sending 2026-02-21 15:24:00 +01:00
Niklas Jensen c9a5b6c2fc Improve timing function 2026-02-01 12:04:54 +01:00
Niklas Jensen 6af809e419 Reverted dumb sleep to yield 2026-01-31 21:36:32 +01:00
Niklas Jensen 9d8c79f9f8 Fixed upload progress (svelte) - added emit await 2026-01-31 21:31:56 +01:00
Niklas Jensen cc2f6d4747 Added sd card pins to globals 2026-01-31 21:15:08 +01:00
Niklas Jensen 18aa9e9e96 Added mutex for ws sending to avoid concurrent send 2026-01-31 21:15:08 +01:00
Niklas Jensen 8d4ce16460 Added sd support and fixed proto malloc 2026-01-31 21:15:08 +01:00
Rune Harlyk ff1444b2bc Makes wifi try to connect to latest 2026-01-31 21:05:37 +01:00
Rune Harlyk e5e9841dd3 Sets cpu freq to 240 2026-01-31 21:00:39 +01:00
Rune Harlyk f4f8035f37 🐛 Handle spa 2026-01-31 19:32:43 +01:00
Rune Harlyk 13300aa9e0 🎨 Update monitor filters 2026-01-31 19:32:43 +01:00
Rune Harlyk cdf6c83be5 ♻️ Moves sdkconfig.defaults 2026-01-31 19:32:43 +01:00
Rune Harlyk bd984309f1 ♻️ Handle merging 2026-01-31 19:32:43 +01:00
Rune Harlyk aca8ee6de5 Full migration to esp-idf 2026-01-31 19:32:43 +01:00
54 changed files with 2380 additions and 673 deletions
+5
View File
@@ -6,6 +6,11 @@ __pycache__/
*.py[cod]
*$py.class
.pio
managed_components/
dependencies.lock
sdkconfig
sdkconfig.*
!sdkconfig.defaults
esp32/src/platform_shared/*
!esp32/src/platform_shared/.gitkeep
app/src/lib/platform_shared/*
+3
View File
@@ -0,0 +1,3 @@
cmake_minimum_required(VERSION 3.16.0)
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
project(Spot_Micro_Leika)
+2 -2
View File
@@ -15,7 +15,7 @@ import type {
} from '$lib/platform_shared/filesystem'
import type { Result, DataResult, ListResult, ProgressCallback } from '$lib/types/models'
const MAX_CHUNK_SIZE = 2 ** 14
const MAX_CHUNK_SIZE = 1024 * 64 // 64KB - must match ESP32 FS_MAX_CHUNK_SIZE
type TimeoutId = ReturnType<typeof setTimeout>
type CleanupFn = (() => void) | null
@@ -51,7 +51,7 @@ export class FileSystemClient {
private downloadListenerCleanup: CleanupFn = null
private completeListenerCleanup: CleanupFn = null
private uploadCompleteListenerCleanup: CleanupFn = null
private transferTimeout = 60000
private transferTimeout = 300000
constructor() {
this.setupListeners()
+2
View File
@@ -153,6 +153,8 @@ function createWebSocket() {
}
const { tag, msg } = decodeMessage(frame.data)
const key: keyof Message = (MESSAGE_TAG_TO_KEY.get(tag) ?? "") as keyof Message;
console.log(key + ": ", msg[key])
if (msg.correlationResponse) {
const pending = pending_requests.get(msg.correlationResponse.correlationId)
if (pending) {
+1 -1
View File
@@ -21,7 +21,7 @@ export default defineConfig({
server: {
proxy: {
'/api': {
target: 'http://spot-micro.local/',
target: 'http://192.168.50.141/',
changeOrigin: true,
ws: true
}
+6 -4
View File
@@ -5,8 +5,10 @@
#include <template/stateful_persistence_pb.h>
#include <settings/ap_settings.h>
#include <utils/timing.h>
#include <WiFi.h>
#include "esp_timer.h"
#include <wifi/wifi_idf.h>
#include <wifi/dns_server.h>
#include <esp_timer.h>
#include <string>
class APService : public StatefulService<APSettings> {
public:
@@ -28,8 +30,8 @@ class APService : public StatefulService<APSettings> {
DNSServer *_dnsServer;
volatile unsigned long _lastManaged;
volatile boolean _reconfigureAp;
volatile boolean _recoveryMode = false;
volatile bool _reconfigureAp;
volatile bool _recoveryMode = false;
void reconfigureAP();
void manageAP();
+8 -1
View File
@@ -1,10 +1,14 @@
#pragma once
#include <esp_log.h>
#include <freertos/FreeRTOS.h>
#include <freertos/semphr.h>
#include <functional>
#include <list>
#include <map>
#include <type_traits>
#include <communication/proto_helpers.h>
#include <utils/timing.h>
class CommAdapterBase {
public:
@@ -51,7 +55,8 @@ class CommAdapterBase {
pb_ostream_t stream = pb_ostream_from_buffer(buffer, out_size);
if (!pb_encode(&stream, socket_message_Message_fields, &msg_)) {
ESP_LOGE("ProtoComm", "Failed to encode message (tag %d), buffer too small?", (int)tag);
ESP_LOGE("ProtoComm", "Failed to encode message (tag %d): %s (calc=%u, written=%u)",
(int)tag, PB_GET_ERROR(&stream), out_size, stream.bytes_written);
return;
}
@@ -92,9 +97,11 @@ class CommAdapterBase {
}
void handleIncoming(const uint8_t* data, size_t len, int cid) {
TIME_IT(
if (!decoder_.decode(data, len, cid)) {
ESP_LOGE("ProtoComm", "Failed to decode incoming message from client %d", cid);
}
, INCOMING_DECODE)
}
void sendPong(int cid) {
+23 -6
View File
@@ -3,6 +3,7 @@
#include <pb_encode.h>
#include <pb_decode.h>
#include <platform_shared/message.pb.h>
#include <esp_log.h>
#include <functional>
#include <map>
@@ -71,32 +72,48 @@ class ProtoDecoder {
bool decode(const uint8_t* data, size_t len, int clientId) {
pb_istream_t stream = pb_istream_from_buffer(data, len);
if (!pb_decode(&stream, socket_message_Message_fields, &msg_)) {
// Reset message before decoding (nanopb will malloc FT_POINTER fields)
msg_ = socket_message_Message_init_zero;
bool success = pb_decode(&stream, socket_message_Message_fields, &msg_);
if (!success) {
ESP_LOGE("ProtoHelpers", "Decode failed: %s (len=%u)", PB_GET_ERROR(&stream), len);
pb_release(socket_message_Message_fields, &msg_);
return false;
}
bool handled = false;
switch (msg_.which_message) {
case socket_message_Message_sub_notif_tag:
if (subscribeHandler_) subscribeHandler_(msg_.message.sub_notif.tag, clientId);
return true;
handled = true;
break;
case socket_message_Message_unsub_notif_tag:
if (unsubscribeHandler_) unsubscribeHandler_(msg_.message.unsub_notif.tag, clientId);
return true;
handled = true;
break;
case socket_message_Message_pingmsg_tag:
if (pingHandler_) pingHandler_(clientId);
return true;
handled = true;
break;
default: {
auto it = handlers_.find(msg_.which_message);
if (it != handlers_.end()) {
it->second(clientId);
return true;
handled = true;
}
return false;
break;
}
}
// Free any malloc'd FT_POINTER fields
pb_release(socket_message_Message_fields, &msg_);
return handled;
}
private:
+7
View File
@@ -1,6 +1,13 @@
#pragma once
#ifndef CONFIG_HTTPD_WS_SUPPORT
#define CONFIG_HTTPD_WS_SUPPORT 1
#endif
#include <esp_http_server.h>
#include <esp_log.h>
#include <freertos/FreeRTOS.h>
#include <freertos/semphr.h>
#include <functional>
#include <vector>
#include <string>
+1 -1
View File
@@ -1,6 +1,6 @@
#pragma once
#include <Arduino.h>
#include <cstdint>
#include <communication/webserver.h>
#include <communication/comm_base.hpp>
+21
View File
@@ -0,0 +1,21 @@
#pragma once
#ifndef PROGMEM
#define PROGMEM
#endif
#ifndef PGM_P
#define PGM_P const char *
#endif
#ifndef pgm_read_byte
#define pgm_read_byte(addr) (*(const unsigned char *)(addr))
#endif
#ifndef pgm_read_word
#define pgm_read_word(addr) (*(const unsigned short *)(addr))
#endif
#ifndef pgm_read_dword
#define pgm_read_dword(addr) (*(const unsigned long *)(addr))
#endif
+1 -2
View File
@@ -1,7 +1,6 @@
#pragma once
#include <WiFi.h>
#include <ArduinoJson.h>
#include <wifi/wifi_idf.h>
#include <esp_http_server.h>
#include "platform_shared/message.pb.h"
+24 -12
View File
@@ -1,29 +1,41 @@
#pragma once
#include <esp_http_server.h>
#include <ArduinoJson.h>
#include <LittleFS.h>
#include <esp_littlefs.h>
#include <esp_vfs.h>
#include <dirent.h>
#include <sys/stat.h>
#include <string>
#include <cstdio>
#include <platform_shared/api.pb.h>
#define ESP_FS LittleFS
#define FS_CONFIG_DIRECTORY "/config"
#define DEVICE_CONFIG_FILE "/config/peripheral.json"
#define CAMERA_SETTINGS_FILE "/config/cameraSettings.pb"
#define AP_SETTINGS_FILE "/config/apSettings.pb"
#define MDNS_SETTINGS_FILE "/config/mdnsSettings.pb"
#define WIFI_SETTINGS_FILE "/config/wifiSettings.pb"
#define PERIPHERAL_SETTINGS_FILE "/config/peripheralSettings.pb"
#define SERVO_SETTINGS_FILE "/config/servoSettings.pb"
#define MOUNT_POINT "/"
#define LITTLEFS_MOUNT_POINT "/littlefs"
#define SD_MOUNT_POINT "/sdcard"
#define FS_CONFIG_DIRECTORY LITTLEFS_MOUNT_POINT "/config"
#define DEVICE_CONFIG_FILE LITTLEFS_MOUNT_POINT "/config/peripheral.pb"
#define CAMERA_SETTINGS_FILE LITTLEFS_MOUNT_POINT "/config/cameraSettings.pb"
#define AP_SETTINGS_FILE LITTLEFS_MOUNT_POINT "/config/apSettings.pb"
#define MDNS_SETTINGS_FILE LITTLEFS_MOUNT_POINT "/config/mdnsSettings.pb"
#define WIFI_SETTINGS_FILE LITTLEFS_MOUNT_POINT "/config/wifiSettings.pb"
#define PERIPHERAL_SETTINGS_FILE LITTLEFS_MOUNT_POINT "/config/peripheralSettings.pb"
#define SERVO_SETTINGS_FILE LITTLEFS_MOUNT_POINT "/config/servoSettings.pb"
namespace FileSystem {
bool init();
void listFilesProto(const std::string &directory, api_FileEntry *entry);
std::string listFiles(const std::string &directory, bool isRoot = true);
bool deleteFile(const char *filename);
bool editFile(const char *filename, const uint8_t *content, size_t size);
bool editFile(const char *filename, const char *content);
bool fileExists(const char *filename);
std::string readFile(const char *filename);
bool writeFile(const char *filename, const char *content);
bool writeFile(const char *filename, const uint8_t *content, size_t size);
bool mkdirRecursive(const char *path);
esp_err_t getFilesProto(httpd_req_t *request);
esp_err_t getFiles(httpd_req_t *request);
+34 -27
View File
@@ -1,20 +1,27 @@
#pragma once
#include <LittleFS.h>
#include <platform_shared/message.pb.h>
#include <filesystem.h>
#include <map>
#include <string>
#include <functional>
#include <cstdio>
#include <freertos/FreeRTOS.h>
#include <freertos/queue.h>
#include <freertos/task.h>
#include <freertos/semphr.h>
// Make sure that this aligns with socket_message.FSDownloadData.data max_size (and for upload)
#define FS_MAX_CHUNK_SIZE 16384 // ~= 16 kb
#define FS_TRANSFER_TIMEOUT_MS 30000 // 30 seconds
#define FS_MAX_CHUNK_SIZE (1024 * 64)
#define FS_TRANSFER_TIMEOUT_MS 30000
#define FS_WRITE_QUEUE_SIZE 4
#define FS_WRITER_TASK_STACK_SIZE 4096
#define FS_WRITER_TASK_PRIORITY 5
namespace FileSystemWS {
struct DownloadState {
std::string path;
File file;
FILE* file;
uint32_t fileSize;
uint32_t chunkSize;
uint32_t totalChunks;
@@ -25,10 +32,11 @@ struct DownloadState {
struct UploadState {
std::string path;
File file;
FILE* file;
uint32_t fileSize;
uint32_t totalChunks;
uint32_t chunksReceived;
uint32_t chunksWritten;
uint32_t bytesReceived;
uint32_t lastActivityTime;
int clientId;
@@ -36,7 +44,14 @@ struct UploadState {
std::string errorMessage;
};
// Callback type for sending messages to clients
struct WriteRequest {
uint32_t transferId;
uint8_t* data;
size_t size;
uint32_t chunkIndex;
bool isLastChunk;
};
using SendMetadataCallback = std::function<void(const socket_message_FSDownloadMetadata&, int clientId)>;
using SendCallback = std::function<void(const socket_message_FSDownloadData&, int clientId)>;
using SendCompleteCallback = std::function<void(const socket_message_FSDownloadComplete&, int clientId)>;
@@ -45,36 +60,22 @@ using SendUploadCompleteCallback = std::function<void(const socket_message_FSUpl
class FileSystemHandler {
public:
FileSystemHandler();
~FileSystemHandler();
void startWriterTask();
void stopWriterTask();
// Set callbacks for sending streaming data
void setSendCallbacks(SendMetadataCallback sendMetadata, SendCallback sendData, SendCompleteCallback sendComplete,
SendUploadCompleteCallback sendUploadComplete);
// Delete file/directory
socket_message_FSDeleteResponse handleDelete(const socket_message_FSDeleteRequest& req);
// Create directory
socket_message_FSMkdirResponse handleMkdir(const socket_message_FSMkdirRequest& req);
// List directory
socket_message_FSListResponse handleList(const socket_message_FSListRequest& req);
// Streaming download - starts the download and streams all chunks
void handleDownloadRequest(const socket_message_FSDownloadRequest& req, int clientId);
// Streaming upload - start upload session
socket_message_FSUploadStartResponse handleUploadStart(const socket_message_FSUploadStart& req, int clientId);
// Streaming upload - receive chunk data (fire-and-forget from client)
void handleUploadData(const socket_message_FSUploadData& req);
// Cancel transfer
socket_message_FSCancelTransferResponse handleCancelTransfer(const socket_message_FSCancelTransfer& req);
// Cleanup expired transfers
void cleanupExpiredTransfers();
// Process pending downloads (call from main loop)
void processPendingDownloads();
private:
@@ -82,6 +83,12 @@ class FileSystemHandler {
std::map<uint32_t, UploadState> uploads_;
uint32_t transferIdCounter_;
// Async writer task
QueueHandle_t writeQueue_;
TaskHandle_t writerTaskHandle_;
SemaphoreHandle_t uploadsMutex_;
volatile bool writerTaskRunning_;
inline uint32_t generateTransferId() { return ++transferIdCounter_; }
SendMetadataCallback sendMetadataCallback_;
@@ -91,10 +98,10 @@ class FileSystemHandler {
void listDirectory(const std::string& path, socket_message_FSListResponse& response);
bool deleteRecursive(const std::string& path);
bool sendNextDownloadChunk(uint32_t transferId);
void finalizeUpload(uint32_t transferId, bool success, const std::string& error = "");
void processWriteRequest(const WriteRequest& req);
static void writerTaskFunc(void* param);
};
extern FileSystemHandler fsHandler;
+20 -18
View File
@@ -1,14 +1,15 @@
#pragma once
#include <esp32-hal.h>
#include <sdkconfig.h>
#include <esp_system.h>
#if CONFIG_IDF_TARGET_ESP32 // ESP32/PICO-D4
#if CONFIG_IDF_TARGET_ESP32
#include "esp32/rom/rtc.h"
#ifndef ESP_PLATFORM_NAME
#define ESP_PLATFORM_NAME "ESP32"
#endif
#elif CONFIG_IDF_TARGET_ESP32S2
#include "esp32/rom/rtc.h"
#include "esp32s2/rom/rtc.h"
#ifndef ESP_PLATFORM_NAME
#define ESP_PLATFORM_NAME "ESP32-S2"
#endif
@@ -26,24 +27,25 @@
#error Target CONFIG_IDF_TARGET is not supported
#endif
#ifndef ARDUINO_VERSION
#ifndef STRINGIFY
#define STRINGIFY(s) #s
#endif
#define ARDUINO_VERSION_STR(major, minor, patch) "v" STRINGIFY(major) "." STRINGIFY(minor) "." STRINGIFY(patch)
#define ARDUINO_VERSION \
ARDUINO_VERSION_STR(ESP_ARDUINO_VERSION_MAJOR, ESP_ARDUINO_VERSION_MINOR, ESP_ARDUINO_VERSION_PATCH)
#endif
/*
* I2C software connection
*/
#ifndef SDA_PIN
#define SDA_PIN SDA
#define SDA_PIN 21
#endif
#ifndef SCL_PIN
#define SCL_PIN SCL
#define SCL_PIN 22
#endif
#ifndef I2C_FREQUENCY
#define I2C_FREQUENCY 100000UL
#endif
#endif
// Optional SD card mounting via SDMMC (1-bit mode for ESP32-S3-CAM)
// Pin definitions - override in build flags if needed
#ifndef SD_CMD_PIN
#define SD_CMD_PIN GPIO_NUM_38
#endif
#ifndef SD_CLK_PIN
#define SD_CLK_PIN GPIO_NUM_39
#endif
#ifndef SD_DATA_PIN
#define SD_DATA_PIN GPIO_NUM_40
#endif
+10 -10
View File
@@ -1,7 +1,7 @@
#pragma once
#include <esp_http_server.h>
#include <ESPmDNS.h>
#include <mdns.h>
#include <template/stateful_service.h>
#include <template/stateful_proto_endpoint.h>
#include <template/stateful_persistence_pb.h>
@@ -9,15 +9,6 @@
#include <utils/timing.h>
class MDNSService : public StatefulService<MDNSSettings> {
private:
FSPersistencePB<MDNSSettings> _persistence;
bool _started {false};
void reconfigureMDNS();
void startMDNS();
void stopMDNS();
void addServices();
public:
MDNSService();
~MDNSService();
@@ -28,4 +19,13 @@ class MDNSService : public StatefulService<MDNSSettings> {
esp_err_t queryServices(httpd_req_t *request, api_Request *protoReq);
StatefulProtoEndpoint<MDNSSettings, api_MDNSSettings> protoEndpoint;
private:
FSPersistencePB<MDNSSettings> _persistence;
bool _started {false};
void reconfigureMDNS();
void startMDNS();
void stopMDNS();
void addServices();
};
+5 -3
View File
@@ -2,6 +2,8 @@
#include <kinematics.h>
#include <message_types.h>
#include <utils/math_utils.h>
#include <cstring>
class MotionState {
protected:
@@ -26,15 +28,15 @@ class MotionState {
}
void updateFeet(body_state_t& body_state, const float smoothing_factor = default_smoothing_factor) {
if (target_body_state.feet != body_state.feet) {
if (std::memcmp(target_body_state.feet, body_state.feet, sizeof(body_state.feet)) != 0) {
body_state.updateFeet(target_body_state.feet);
}
}
public:
void updateImuOffsets(const float new_omega, const float new_psi) {
omega_offset = new_omega * RAD_TO_DEG;
psi_offset = new_psi * RAD_TO_DEG;
omega_offset = RAD_TO_DEG_F(new_omega);
psi_offset = RAD_TO_DEG_F(new_psi);
}
virtual ~MotionState() {}
@@ -1,7 +1,6 @@
#pragma once
#include <esp_http_server.h>
#include <WiFi.h>
#include <features.h>
#include <template/stateful_service.h>
+1 -2
View File
@@ -316,8 +316,7 @@ class MPU6050Driver {
i += chunkSize;
address += chunkSize;
if (address == 0 || address >= 256) {
address = 0;
if (address == 0) {
bank++;
setMemoryBank(bank);
}
+72 -23
View File
@@ -1,56 +1,105 @@
#ifndef LEDService_h
#define LEDService_h
#include <FastLED.h>
#include <driver/rmt_tx.h>
#include <led_strip.h>
#include <led_strip_rmt.h>
#include <wifi/wifi_idf.h>
#include <utils/timing.h>
#include <esp_log.h>
#ifndef WS2812_PIN
#define WS2812_PIN 12
#endif
#ifndef WS2812_NUM_LEDS
#define WS2812_NUM_LEDS 1 + 12
#define WS2812_NUM_LEDS 13
#endif
#define COLOR_ORDER GRB
#define CHIPSET WS2811
class LEDService {
private:
CRGB leds[WS2812_NUM_LEDS];
CRGBPalette16 currentPalette;
TBlendType currentBlending;
led_strip_handle_t led_strip = nullptr;
int _brightness = 255;
int direction = 1;
bool initialized = false;
struct RGB {
uint8_t r, g, b;
};
RGB oceanColor = {0, 119, 190};
RGB forestColor = {34, 139, 34};
public:
LEDService() {
FastLED.addLeds<CHIPSET, WS2812_PIN, COLOR_ORDER>(leds, WS2812_NUM_LEDS).setCorrection(TypicalLEDStrip);
currentPalette = OceanColors_p;
currentBlending = LINEARBLEND;
LEDService() {}
~LEDService() {
if (initialized && led_strip) {
led_strip_del(led_strip);
}
}
void begin() {
if (initialized) return;
led_strip_config_t strip_config = {
.strip_gpio_num = WS2812_PIN,
.max_leds = WS2812_NUM_LEDS,
.led_pixel_format = LED_PIXEL_FORMAT_GRB,
.led_model = LED_MODEL_WS2812,
.flags = {.invert_out = false},
};
led_strip_rmt_config_t rmt_config = {
.clk_src = RMT_CLK_SRC_DEFAULT,
.resolution_hz = 10 * 1000 * 1000,
.mem_block_symbols = 64,
.flags = {.with_dma = false},
};
esp_err_t err = led_strip_new_rmt_device(&strip_config, &rmt_config, &led_strip);
if (err == ESP_OK) {
initialized = true;
led_strip_clear(led_strip);
ESP_LOGI("LEDService", "LED strip initialized on GPIO %d", WS2812_PIN);
} else {
ESP_LOGE("LEDService", "Failed to initialize LED strip: %s", esp_err_to_name(err));
}
}
~LEDService() {}
void loop() {
if (!initialized) return;
EXECUTE_EVERY_N_MS(1000 / 60, {
if (_brightness >= 200) direction = -5;
if (_brightness <= 50) direction = 5;
_brightness += direction;
if (WiFi.isConnected()) {
fillFromPallette(OceanColors_p, 0);
} else {
fillFromPallette(ForestColors_p, 128);
RGB color = WiFi.isConnected() ? oceanColor : forestColor;
uint8_t r = (color.r * _brightness) / 255;
uint8_t g = (color.g * _brightness) / 255;
uint8_t b = (color.b * _brightness) / 255;
for (int i = 0; i < WS2812_NUM_LEDS; ++i) {
led_strip_set_pixel(led_strip, i, r, g, b);
}
FastLED.show();
led_strip_refresh(led_strip);
});
}
void fillFromPallette(CRGBPalette16 colorPalette, uint8_t colorIndex) {
CRGB color = ColorFromPalette(colorPalette, colorIndex, _brightness, currentBlending);
void setColor(uint8_t r, uint8_t g, uint8_t b) {
if (!initialized) return;
for (int i = 0; i < WS2812_NUM_LEDS; ++i) {
leds[i] = color;
led_strip_set_pixel(led_strip, i, r, g, b);
}
led_strip_refresh(led_strip);
}
void clear() {
if (!initialized) return;
led_strip_clear(led_strip);
}
};
#endif
#endif
+2
View File
@@ -12,7 +12,9 @@
#include <list>
#if FT_ENABLED(USE_USS)
#include <NewPing.h>
#endif
#include <peripherals/i2c_bus.h>
#include <peripherals/imu.h>
#include <peripherals/magnetometer.h>
+4 -7
View File
@@ -1,12 +1,11 @@
#pragma once
#include <WiFi.h>
#include <wifi/wifi_idf.h>
#include <wifi/dns_server.h>
#include <template/state_result.h>
#include <platform_shared/api.pb.h>
#include <cstring>
#include <DNSServer.h>
#ifndef FACTORY_AP_PROVISION_MODE
#define FACTORY_AP_PROVISION_MODE api_APProvisionMode_AP_MODE_DISCONNECTED
#endif
@@ -77,11 +76,9 @@ inline APSettings APSettings_defaults() {
return settings;
}
inline void APSettings_read(const APSettings &settings, APSettings &proto) {
proto = settings;
}
inline void APSettings_read(const APSettings &settings, APSettings &proto) { proto = settings; }
inline StateUpdateResult APSettings_update(const APSettings &proto, APSettings &settings) {
settings = proto;
return StateUpdateResult::CHANGED;
}
}
-1
View File
@@ -1,4 +1,3 @@
#include <Arduino.h>
#include <template/state_result.h>
#include <ArduinoJson.h>
#include <string>
@@ -7,10 +7,10 @@
* I2C software connection
*/
#ifndef SDA_PIN
#define SDA_PIN SDA
#define SDA_PIN 21
#endif
#ifndef SCL_PIN
#define SCL_PIN SCL
#define SCL_PIN 22
#endif
#ifndef I2C_FREQUENCY
#define I2C_FREQUENCY 1000000UL
@@ -35,7 +35,8 @@ inline void PeripheralsConfiguration_read(const PeripheralsConfiguration& settin
proto = settings;
}
inline StateUpdateResult PeripheralsConfiguration_update(const PeripheralsConfiguration& proto, PeripheralsConfiguration& settings) {
inline StateUpdateResult PeripheralsConfiguration_update(const PeripheralsConfiguration& proto,
PeripheralsConfiguration& settings) {
settings = proto;
return StateUpdateResult::CHANGED;
}
+3 -4
View File
@@ -1,6 +1,6 @@
#pragma once
#include <WiFi.h>
#include <wifi/wifi_idf.h>
#include <template/state_result.h>
#include <platform_shared/api.pb.h>
#include <cstring>
@@ -42,6 +42,7 @@ inline WiFiSettings WiFiSettings_defaults() {
strncpy(settings.hostname, FACTORY_WIFI_HOSTNAME, sizeof(settings.hostname) - 1);
settings.priority_rssi = true;
settings.wifi_networks_count = 0;
settings.selected_network = 0;
if (strlen(FACTORY_WIFI_SSID) > 0) {
settings.wifi_networks[0] = WiFiNetwork_defaults();
settings.wifi_networks_count = 1;
@@ -49,9 +50,7 @@ inline WiFiSettings WiFiSettings_defaults() {
return settings;
}
inline void WiFiSettings_read(const WiFiSettings &settings, WiFiSettings &proto) {
proto = settings;
}
inline void WiFiSettings_read(const WiFiSettings &settings, WiFiSettings &proto) { proto = settings; }
inline StateUpdateResult WiFiSettings_update(const WiFiSettings &proto, WiFiSettings &settings) {
settings = proto;
+2 -3
View File
@@ -1,8 +1,8 @@
#pragma once
#include <ESPmDNS.h>
#include <mdns.h>
#include <esp_http_server.h>
#include <WiFi.h>
#include <wifi/wifi_idf.h>
#include <filesystem.h>
#include <global.h>
#include <esp_timer.h>
@@ -20,7 +20,6 @@ esp_err_t handleSleep(httpd_req_t *request);
void reset();
void restart();
void sleep();
void status(JsonObject &root);
void getAnalytics(socket_message_AnalyticsData &analytics);
void getStaticSystemInformation(socket_message_StaticSystemInformation &info);
+14 -40
View File
@@ -1,24 +1,11 @@
#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
* Copyright (C) 2025 runeharlyk
*
* 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 <FS.h>
#include <template/stateful_service.h>
#include <filesystem.h>
#include <ArduinoJson.h>
#include <cstdio>
#include <sys/stat.h>
template <class T>
class FSPersistence {
@@ -34,25 +21,18 @@ class FSPersistence {
}
void readFromFS() {
File settingsFile = _fs->open(_filePath, "r");
std::string content = FileSystem::readFile(_filePath);
if (settingsFile) {
if (!content.empty()) {
JsonDocument jsonDocument;
DeserializationError error = deserializeJson(jsonDocument, settingsFile);
DeserializationError error = deserializeJson(jsonDocument, content);
if (error == DeserializationError::Ok) {
JsonVariant jsonObject = jsonDocument.as<JsonVariant>();
_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();
}
@@ -64,13 +44,10 @@ class FSPersistence {
mkdirs();
File file = _fs->open(_filePath, "w");
std::string content;
serializeJson(jsonDocument, content);
if (!file) return false;
serializeJson(jsonDocument, file);
file.close();
return true;
return FileSystem::writeFile(_filePath, content.c_str());
}
void disableUpdateHandler() {
@@ -90,26 +67,23 @@ class FSPersistence {
JsonStateReader<T> _stateReader;
JsonStateUpdater<T> _stateUpdater;
StatefulService<T> *_statefulService;
FS *_fs {&ESP_FS};
const char *_filePath;
size_t _bufferSize;
HandlerId _updateHandlerId;
// We assume we have a _filePath with format
// "/directory1/directory2/filename" We create a directory for each missing
// parent
void mkdirs() {
std::string path(_filePath);
size_t index = 0;
while ((index = path.find('/', index + 1)) != std::string::npos) {
std::string segment = path.substr(0, index);
if (!_fs->exists(segment.c_str())) _fs->mkdir(segment.c_str());
struct stat st;
if (stat(segment.c_str(), &st) != 0) {
FileSystem::mkdirRecursive(segment.c_str());
}
}
}
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() {
JsonDocument jsonDocument;
JsonVariant jsonObject = jsonDocument.as<JsonVariant>();
@@ -117,4 +91,4 @@ class FSPersistence {
}
};
#endif // end FSPersistence
#endif
@@ -1,29 +1,24 @@
#pragma once
#include <FS.h>
#include <template/stateful_service.h>
#include <template/state_result.h>
#include <filesystem.h>
#include <pb_encode.h>
#include <pb_decode.h>
#include <cstdio>
#include <sys/stat.h>
#include <esp_log.h>
static const char *TAG_PERSISTENCE = "FSPersistencePB";
/**
* Protobuf-based filesystem persistence for StatefulService.
*
* @tparam T The state type (should be a nanopb-generated struct like api_APSettings)
*/
template <class T>
class FSPersistencePB {
public:
// Formats are passed as referenced const (local variable) we want to read from, and a reference (proto) we write to
using ProtoStateReader = std::function<void(const T&, T&)>;
// Formats are passed as referenced const (new object) we read from, and a reference to the local variable we write to
using ProtoStateUpdater = std::function<StateUpdateResult(const T&, T&)>;
using ProtoStateReader = std::function<void(const T &, T &)>;
using ProtoStateUpdater = std::function<StateUpdateResult(const T &, T &)>;
FSPersistencePB(ProtoStateReader stateReader, ProtoStateUpdater stateUpdater,
StatefulService<T> *statefulService, const char *filePath,
const pb_msgdesc_t *msgDescriptor, size_t maxSize,
const T &defaultState)
FSPersistencePB(ProtoStateReader stateReader, ProtoStateUpdater stateUpdater, StatefulService<T> *statefulService,
const char *filePath, const pb_msgdesc_t *msgDescriptor, size_t maxSize, const T &defaultState)
: _stateReader(stateReader),
_stateUpdater(stateUpdater),
_statefulService(statefulService),
@@ -36,17 +31,19 @@ class FSPersistencePB {
}
void readFromFS() {
File file = _fs->open(_filePath, "r");
FILE *file = fopen(_filePath, "rb");
if (file) {
size_t fileSize = file.size();
fseek(file, 0, SEEK_END);
size_t fileSize = ftell(file);
fseek(file, 0, SEEK_SET);
if (fileSize > 0 && fileSize <= _maxSize) {
uint8_t *buffer = new uint8_t[fileSize];
size_t bytesRead = file.read(buffer, fileSize);
file.close();
size_t bytesRead = fread(buffer, 1, fileSize, file);
fclose(file);
if (bytesRead == fileSize) {
// Allocate on heap to avoid stack overflow with large proto messages
T *protoMsg = new T();
*protoMsg = {};
pb_istream_t stream = pb_istream_from_buffer(buffer, bytesRead);
@@ -62,7 +59,7 @@ class FSPersistencePB {
}
delete[] buffer;
} else {
file.close();
fclose(file);
}
}
@@ -74,7 +71,6 @@ class FSPersistencePB {
uint8_t *buffer = new uint8_t[_maxSize];
pb_ostream_t stream = pb_ostream_from_buffer(buffer, _maxSize);
// Allocate on heap to avoid stack overflow with large proto messages
T *protoMsg = new T();
*protoMsg = {};
_statefulService->read([this, protoMsg](const T &state) { _stateReader(state, *protoMsg); });
@@ -89,14 +85,15 @@ class FSPersistencePB {
mkdirs();
File file = _fs->open(_filePath, "w");
FILE *file = fopen(_filePath, "wb");
if (!file) {
ESP_LOGE(TAG_PERSISTENCE, "Failed to open file for writing: %s", _filePath);
delete[] buffer;
return false;
}
size_t written = file.write(buffer, stream.bytes_written);
file.close();
size_t written = fwrite(buffer, 1, stream.bytes_written, file);
fclose(file);
delete[] buffer;
return written == stream.bytes_written;
@@ -111,8 +108,7 @@ class FSPersistencePB {
void enableUpdateHandler() {
if (!_updateHandlerId) {
_updateHandlerId = _statefulService->addUpdateHandler(
[&](const std::string &originId) { writeToFS(); });
_updateHandlerId = _statefulService->addUpdateHandler([&](const std::string &originId) { writeToFS(); });
}
}
@@ -120,7 +116,6 @@ class FSPersistencePB {
ProtoStateReader _stateReader;
ProtoStateUpdater _stateUpdater;
StatefulService<T> *_statefulService;
FS *_fs{&ESP_FS};
const char *_filePath;
const pb_msgdesc_t *_msgDescriptor;
size_t _maxSize;
@@ -132,13 +127,15 @@ class FSPersistencePB {
size_t index = 0;
while ((index = path.find('/', index + 1)) != std::string::npos) {
std::string segment = path.substr(0, index);
if (!_fs->exists(segment.c_str())) _fs->mkdir(segment.c_str());
struct stat st;
if (stat(segment.c_str(), &st) != 0) {
FileSystem::mkdirRecursive(segment.c_str());
}
}
}
protected:
void applyDefaults() {
_statefulService->updateWithoutPropagation(
[this](T &state) { return _stateUpdater(_defaultState, state); });
_statefulService->updateWithoutPropagation([this](T &state) { return _stateUpdater(_defaultState, state); });
}
};
+5 -29
View File
@@ -1,8 +1,5 @@
#pragma once
#include <Arduino.h>
#include <ArduinoJson.h>
#include <list>
#include <functional>
#include <freertos/FreeRTOS.h>
@@ -11,19 +8,13 @@
#include <template/state_result.h>
template <typename T>
using JsonStateUpdater = std::function<StateUpdateResult(JsonVariant &root, T &settings)>;
template <typename T>
using JsonStateReader = std::function<void(T &settings, JsonVariant &root)>;
using HandlerId = size_t;
using StateUpdateCallback = std::function<void(const std::string &originId)>;
using StateHookCallback = std::function<void(const std::string &originId, StateUpdateResult &result)>;
class HandlerBase {
protected:
static inline HandlerId nextId_ = 1; // Start from 1, 0 is invalid
static inline HandlerId nextId_ = 1;
HandlerId id_;
bool allowRemove_;
@@ -99,31 +90,16 @@ class StatefulService {
return result;
}
StateUpdateResult update(JsonVariant &jsonObject, JsonStateUpdater<T> stateUpdater, const std::string &originId) {
lock();
StateUpdateResult result = stateUpdater(jsonObject, state_);
unlock();
notifyStateChange(originId, result);
return result;
}
StateUpdateResult updateWithoutPropagation(JsonVariant &jsonObject, JsonStateUpdater<T> stateUpdater) {
lock();
StateUpdateResult result = stateUpdater(jsonObject, state_);
unlock();
return result;
}
void read(std::function<void(T &)> stateReader) {
lock();
stateReader(state_);
unlock();
}
void read(JsonVariant &jsonObject, JsonStateReader<T> stateReader) {
lock();
stateReader(state_, jsonObject);
unlock();
void read(std::function<void(const T &)> stateReader) const {
const_cast<StatefulService *>(this)->lock();
stateReader(state_);
const_cast<StatefulService *>(this)->unlock();
}
void callUpdateHandlers(const std::string &originId) {
+76
View File
@@ -0,0 +1,76 @@
#pragma once
#include <cstdint>
#include <cstring>
#include <string>
#include <esp_netif.h>
class IPAddress {
public:
IPAddress() : _addr {0, 0, 0, 0} {}
IPAddress(uint8_t a, uint8_t b, uint8_t c, uint8_t d) : _addr {a, b, c, d} {}
IPAddress(uint32_t addr) {
_addr[0] = addr & 0xFF;
_addr[1] = (addr >> 8) & 0xFF;
_addr[2] = (addr >> 16) & 0xFF;
_addr[3] = (addr >> 24) & 0xFF;
}
IPAddress(const esp_ip4_addr_t& ip4) {
_addr[0] = ip4.addr & 0xFF;
_addr[1] = (ip4.addr >> 8) & 0xFF;
_addr[2] = (ip4.addr >> 16) & 0xFF;
_addr[3] = (ip4.addr >> 24) & 0xFF;
}
operator uint32_t() const {
return static_cast<uint32_t>(_addr[0]) | (static_cast<uint32_t>(_addr[1]) << 8) |
(static_cast<uint32_t>(_addr[2]) << 16) | (static_cast<uint32_t>(_addr[3]) << 24);
}
operator esp_ip4_addr_t() const {
esp_ip4_addr_t ip4;
ip4.addr = static_cast<uint32_t>(*this);
return ip4;
}
bool operator==(const IPAddress& other) const {
return _addr[0] == other._addr[0] && _addr[1] == other._addr[1] && _addr[2] == other._addr[2] &&
_addr[3] == other._addr[3];
}
bool operator!=(const IPAddress& other) const { return !(*this == other); }
uint8_t operator[](int index) const { return _addr[index]; }
uint8_t& operator[](int index) { return _addr[index]; }
bool fromString(const char* str) {
int parts[4];
if (sscanf(str, "%d.%d.%d.%d", &parts[0], &parts[1], &parts[2], &parts[3]) != 4) {
return false;
}
for (int i = 0; i < 4; i++) {
if (parts[i] < 0 || parts[i] > 255) return false;
_addr[i] = static_cast<uint8_t>(parts[i]);
}
return true;
}
std::string toString() const {
char buf[16];
snprintf(buf, sizeof(buf), "%d.%d.%d.%d", _addr[0], _addr[1], _addr[2], _addr[3]);
return std::string(buf);
}
const char* c_str() const {
static char buf[16];
snprintf(buf, sizeof(buf), "%d.%d.%d.%d", _addr[0], _addr[1], _addr[2], _addr[3]);
return buf;
}
private:
uint8_t _addr[4];
};
+9 -8
View File
@@ -16,17 +16,18 @@
} \
} while (0)
#define TIME_IT(code) \
// Note: name must be a valid variable name too
#define TIME_IT(code, name) \
{ \
uint64_t time_it_start = esp_timer_get_time(); \
uint64_t time_it_start##name = esp_timer_get_time(); \
code; \
uint64_t time_it_elapsed = esp_timer_get_time() - time_it_start; \
if (time_it_elapsed < 1000) { \
ESP_LOGI("Time It", "Time elapsed: %llu microseconds", time_it_elapsed); \
} else if (time_it_elapsed < 1000000) { \
ESP_LOGI("Time It", "Time elapsed: %llu milliseconds", time_it_elapsed / 1000); \
uint64_t time_it_elapsed##name = esp_timer_get_time() - time_it_start##name; \
if (time_it_elapsed##name < 1000) { \
ESP_LOGI("Time It - " #name, "Time elapsed: %llu microseconds", time_it_elapsed##name); \
} else if (time_it_elapsed##name < 1000000) { \
ESP_LOGI("Time It - " #name, "Time elapsed: %llu milliseconds", time_it_elapsed##name / 1000); \
} else { \
ESP_LOGI("Time It", "Time elapsed: %.2f seconds", time_it_elapsed / 1000000.0); \
ESP_LOGI("Time It - " #name, "Time elapsed: %.2f seconds", time_it_elapsed##name / 1000000.0); \
} \
}
+144
View File
@@ -0,0 +1,144 @@
#pragma once
#include <lwip/sockets.h>
#include <lwip/netdb.h>
#include <esp_log.h>
#include <utils/ip_address.h>
#include <freertos/FreeRTOS.h>
#include <freertos/task.h>
#include <string>
#include <atomic>
#define DNS_PORT 53
#define DNS_MAX_PACKET_SIZE 512
class DNSServer {
public:
DNSServer() : _socket(-1), _running(false), _task(nullptr) {}
~DNSServer() { stop(); }
bool start(uint16_t port, const char* domainName, const IPAddress& resolvedIP) {
if (_running) return true;
_port = port;
_resolvedIP = resolvedIP;
_domainName = domainName ? domainName : "*";
_socket = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
if (_socket < 0) {
ESP_LOGE("DNSServer", "Failed to create socket");
return false;
}
int opt = 1;
setsockopt(_socket, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
struct timeval tv;
tv.tv_sec = 1;
tv.tv_usec = 0;
setsockopt(_socket, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
struct sockaddr_in serverAddr = {};
serverAddr.sin_family = AF_INET;
serverAddr.sin_addr.s_addr = INADDR_ANY;
serverAddr.sin_port = htons(_port);
if (bind(_socket, (struct sockaddr*)&serverAddr, sizeof(serverAddr)) < 0) {
ESP_LOGE("DNSServer", "Failed to bind socket");
close(_socket);
_socket = -1;
return false;
}
_running = true;
xTaskCreate(dnsTask, "dns_server", 4096, this, 3, &_task);
ESP_LOGI("DNSServer", "Started on port %d, resolving to %s", _port, _resolvedIP.toString().c_str());
return true;
}
void stop() {
_running = false;
if (_task) {
vTaskDelay(100 / portTICK_PERIOD_MS);
_task = nullptr;
}
if (_socket >= 0) {
close(_socket);
_socket = -1;
}
ESP_LOGI("DNSServer", "Stopped");
}
void processNextRequest() {}
private:
static void dnsTask(void* param) {
DNSServer* self = static_cast<DNSServer*>(param);
self->run();
vTaskDelete(nullptr);
}
void run() {
uint8_t buffer[DNS_MAX_PACKET_SIZE];
struct sockaddr_in clientAddr;
socklen_t clientAddrLen = sizeof(clientAddr);
while (_running) {
int len = recvfrom(_socket, buffer, DNS_MAX_PACKET_SIZE, 0, (struct sockaddr*)&clientAddr, &clientAddrLen);
if (len > 0) {
processRequest(buffer, len, &clientAddr);
}
}
}
void processRequest(uint8_t* buffer, int len, struct sockaddr_in* clientAddr) {
if (len < 12) return;
uint16_t flags = (buffer[2] << 8) | buffer[3];
if ((flags & 0x8000) != 0) return;
uint8_t response[DNS_MAX_PACKET_SIZE];
memcpy(response, buffer, len);
response[2] = 0x81;
response[3] = 0x80;
response[6] = 0x00;
response[7] = 0x01;
int responseLen = len;
response[responseLen++] = 0xC0;
response[responseLen++] = 0x0C;
response[responseLen++] = 0x00;
response[responseLen++] = 0x01;
response[responseLen++] = 0x00;
response[responseLen++] = 0x01;
response[responseLen++] = 0x00;
response[responseLen++] = 0x00;
response[responseLen++] = 0x00;
response[responseLen++] = 0x3C;
response[responseLen++] = 0x00;
response[responseLen++] = 0x04;
uint32_t ip = static_cast<uint32_t>(_resolvedIP);
response[responseLen++] = ip & 0xFF;
response[responseLen++] = (ip >> 8) & 0xFF;
response[responseLen++] = (ip >> 16) & 0xFF;
response[responseLen++] = (ip >> 24) & 0xFF;
sendto(_socket, response, responseLen, 0, (struct sockaddr*)clientAddr, sizeof(*clientAddr));
}
int _socket;
uint16_t _port;
IPAddress _resolvedIP;
std::string _domainName;
std::atomic<bool> _running;
TaskHandle_t _task;
};
+127
View File
@@ -0,0 +1,127 @@
#pragma once
#include <esp_wifi.h>
#include <esp_event.h>
#include <esp_netif.h>
#include <esp_log.h>
#include <nvs_flash.h>
#include <freertos/FreeRTOS.h>
#include <freertos/task.h>
#include <freertos/event_groups.h>
#include <utils/ip_address.h>
#include <string>
#include <cstring>
#include <functional>
#include <vector>
typedef enum {
WL_NO_SHIELD = 255,
WL_IDLE_STATUS = 0,
WL_NO_SSID_AVAIL = 1,
WL_SCAN_COMPLETED = 2,
WL_CONNECTED = 3,
WL_CONNECT_FAILED = 4,
WL_CONNECTION_LOST = 5,
WL_DISCONNECTED = 6
} wl_status_t;
typedef enum {
WIFI_AUTH_OPEN_IDF = 0,
WIFI_AUTH_WEP_IDF,
WIFI_AUTH_WPA_PSK_IDF,
WIFI_AUTH_WPA2_PSK_IDF
} wifi_enc_type_t;
using WiFiEventCb = std::function<void(int32_t event, void* event_data)>;
struct WiFiEventHandler {
int32_t event_id;
WiFiEventCb callback;
};
class WiFiClass {
public:
WiFiClass();
~WiFiClass();
bool init();
bool mode(wifi_mode_t mode);
wifi_mode_t getMode();
bool begin(const char* ssid, const char* password = nullptr, int32_t channel = 0, const uint8_t* bssid = nullptr);
bool disconnect(bool wifiOff = false);
bool reconnect();
bool config(IPAddress local_ip, IPAddress gateway, IPAddress subnet, IPAddress dns1 = IPAddress(),
IPAddress dns2 = IPAddress());
bool setHostname(const char* hostname);
const char* getHostname();
wl_status_t status();
bool isConnected();
IPAddress localIP();
IPAddress subnetMask();
IPAddress gatewayIP();
IPAddress dnsIP(uint8_t dns_no = 0);
std::string macAddress();
std::string SSID();
std::string BSSIDstr();
int32_t RSSI();
uint8_t channel();
int16_t scanNetworks(bool async = false);
int16_t scanComplete();
void scanDelete();
std::string SSID(uint8_t i);
int32_t RSSI(uint8_t i);
wifi_enc_type_t encryptionType(uint8_t i);
std::string BSSIDstr(uint8_t i);
int32_t channel(uint8_t i);
void getNetworkInfo(uint8_t i, std::string& ssid, uint8_t& encType, int32_t& rssi, uint8_t*& bssid, int32_t& ch);
bool softAP(const char* ssid, const char* password = nullptr, int channel = 1, bool ssid_hidden = false,
int max_connection = 4);
bool softAPConfig(IPAddress local_ip, IPAddress gateway, IPAddress subnet);
bool softAPdisconnect(bool wifiOff = false);
IPAddress softAPIP();
std::string softAPmacAddress();
uint8_t softAPgetStationNum();
bool setAutoReconnect(bool autoReconnect);
bool persistent(bool persistent);
bool setTxPower(int8_t power);
void onEvent(WiFiEventCb callback, int32_t event_id);
private:
static void eventHandler(void* arg, esp_event_base_t event_base, int32_t event_id, void* event_data);
void dispatchEvent(int32_t event_id, void* event_data);
esp_netif_t* _sta_netif;
esp_netif_t* _ap_netif;
bool _initialized;
bool _autoReconnect;
bool _persistent;
wl_status_t _status;
wifi_mode_t _mode;
std::string _hostname;
wifi_ap_record_t* _scanResult;
uint16_t _scanCount;
int16_t _scanStatus;
std::vector<WiFiEventHandler> _eventHandlers;
IPAddress _sta_static_ip;
IPAddress _sta_static_gw;
IPAddress _sta_static_sn;
IPAddress _sta_static_dns1;
IPAddress _sta_static_dns2;
bool _useStaticIp;
};
extern WiFiClass WiFi;
+23 -19
View File
@@ -1,8 +1,8 @@
#pragma once
#include <esp_http_server.h>
#include <WiFi.h>
#include <ESPmDNS.h>
#include <wifi/wifi_idf.h>
#include <mdns.h>
#include <string>
#include <filesystem.h>
@@ -12,24 +12,11 @@
#include <template/stateful_proto_endpoint.h>
#include <settings/wifi_settings.h>
#define WIFI_EVENT_STA_DISCONNECTED_IDF WIFI_EVENT_STA_DISCONNECTED
#define WIFI_EVENT_STA_STOP_IDF WIFI_EVENT_STA_STOP
#define IP_EVENT_STA_GOT_IP_IDF 1000
class WiFiService : public StatefulService<WiFiSettings> {
private:
void onStationModeDisconnected(WiFiEvent_t event, WiFiEventInfo_t info);
void onStationModeStop(WiFiEvent_t event, WiFiEventInfo_t info);
static void onStationModeGotIP(WiFiEvent_t event, WiFiEventInfo_t info);
FSPersistencePB<WiFiSettings> _persistence;
void reconfigureWiFiConnection();
void manageSTA();
void connectToWiFi();
void configureNetwork(WiFiNetwork &network);
unsigned long _lastConnectionAttempt;
bool _stopping;
constexpr static uint16_t reconnectDelay {10000};
public:
WiFiService();
~WiFiService();
@@ -38,6 +25,7 @@ class WiFiService : public StatefulService<WiFiSettings> {
void loop();
void setupMDNS(const char *hostname);
void selectNetwork(uint32_t index);
const char *getHostname() { return state().hostname; }
@@ -46,4 +34,20 @@ class WiFiService : public StatefulService<WiFiSettings> {
static esp_err_t getNetworkStatus(httpd_req_t *request);
StatefulProtoEndpoint<WiFiSettings, api_WifiSettings> protoEndpoint;
private:
void onStationModeDisconnected(int32_t event, void *event_data);
void onStationModeStop(int32_t event, void *event_data);
static void onStationModeGotIP(int32_t event, void *event_data);
FSPersistencePB<WiFiSettings> _persistence;
void reconfigureWiFiConnection();
void manageSTA();
void configureNetwork(WiFiNetwork &network);
unsigned long _lastConnectionAttempt;
bool _stopping;
constexpr static uint16_t reconnectDelay {10000};
};
+1
View File
@@ -3,3 +3,4 @@
#include "WWWData.h"
void mountStaticAssets(WebServer& s);
void mountSpaFallback(WebServer& s);
+1 -1
View File
@@ -114,7 +114,7 @@ def write_header():
with open(output_file, "w", newline="\n") as f:
f.write("#pragma once\n")
f.write("#include <Arduino.h>\n\n")
f.write("#include <compat/pgmspace.h>\n\n")
f.write(
"struct WebAsset { const char* uri; const char* mime; const uint8_t* data; uint32_t len; uint32_t etag; uint8_t gz; };\n")
f.write(
+26
View File
@@ -0,0 +1,26 @@
idf_component_register(
SRC_DIRS
"."
"communication"
"peripherals"
"wifi"
"platform_shared"
"../../submodules/nanopb"
INCLUDE_DIRS
"../include"
"../../submodules/nanopb"
REQUIRES
driver
esp_http_server
nvs_flash
esp_wifi
esp_event
esp_netif
mdns
esp_timer
esp_psram
spi_flash
littlefs
esp32-camera
esp-dsp
)
+22 -21
View File
@@ -4,21 +4,26 @@
static const char *TAG = "APService";
APService::APService()
: protoEndpoint(APSettings_read, APSettings_update, this,
API_REQUEST_EXTRACTOR(ap_settings, api_APSettings),
: protoEndpoint(APSettings_read, APSettings_update, this, API_REQUEST_EXTRACTOR(ap_settings, api_APSettings),
API_RESPONSE_ASSIGNER(ap_settings, api_APSettings)),
_persistence(APSettings_read, APSettings_update, this,
AP_SETTINGS_FILE, api_APSettings_fields, api_APSettings_size,
APSettings_defaults()) {
_persistence(APSettings_read, APSettings_update, this, AP_SETTINGS_FILE, api_APSettings_fields,
api_APSettings_size, APSettings_defaults()),
_dnsServer(nullptr),
_lastManaged(0),
_reconfigureAp(false),
_recoveryMode(false) {
addUpdateHandler([&](const std::string &originId) { reconfigureAP(); }, false);
}
APService::~APService() {}
void APService::begin() {
_persistence.readFromFS();
APService::~APService() {
if (_dnsServer) {
delete _dnsServer;
_dnsServer = nullptr;
}
}
void APService::begin() { _persistence.readFromFS(); }
esp_err_t APService::getStatusProto(httpd_req_t *request) {
api_Response res = api_Response_init_zero;
res.status_code = 200;
@@ -30,15 +35,15 @@ esp_err_t APService::getStatusProto(httpd_req_t *request) {
void APService::statusProto(api_APStatus &proto) {
proto.status = getAPNetworkStatus();
proto.ip_address = static_cast<uint32_t>(WiFi.softAPIP());
String mac = WiFi.softAPmacAddress();
std::string mac = WiFi.softAPmacAddress();
strncpy(proto.mac_address, mac.c_str(), sizeof(proto.mac_address) - 1);
proto.mac_address[sizeof(proto.mac_address) - 1] = '\0';
proto.station_num = WiFi.softAPgetStationNum();
}
APNetworkStatus APService::getAPNetworkStatus() {
WiFiMode_t currentWiFiMode = WiFi.getMode();
bool apActive = currentWiFiMode == WIFI_AP || currentWiFiMode == WIFI_AP_STA;
wifi_mode_t currentWiFiMode = WiFi.getMode();
bool apActive = currentWiFiMode == WIFI_MODE_AP || currentWiFiMode == WIFI_MODE_APSTA;
if (apActive && state().provision_mode != AP_MODE_ALWAYS && WiFi.status() == WL_CONNECTED) {
return LINGERING;
}
@@ -64,13 +69,13 @@ void APService::loop() {
}
void APService::manageAP() {
WiFiMode_t currentWiFiMode = WiFi.getMode();
wifi_mode_t currentWiFiMode = WiFi.getMode();
if (state().provision_mode == AP_MODE_ALWAYS ||
(state().provision_mode == AP_MODE_DISCONNECTED && WiFi.status() != WL_CONNECTED) || _recoveryMode) {
if (_reconfigureAp || currentWiFiMode == WIFI_OFF || currentWiFiMode == WIFI_STA) {
if (_reconfigureAp || currentWiFiMode == WIFI_MODE_NULL || currentWiFiMode == WIFI_MODE_STA) {
startAP();
}
} else if ((currentWiFiMode == WIFI_AP || currentWiFiMode == WIFI_AP_STA) &&
} else if ((currentWiFiMode == WIFI_MODE_AP || currentWiFiMode == WIFI_MODE_APSTA) &&
(_reconfigureAp || !WiFi.softAPgetStationNum())) {
stopAP();
}
@@ -82,7 +87,7 @@ void APService::startAP() {
WiFi.softAPConfig(IPAddress(state().local_ip), IPAddress(state().gateway_ip), IPAddress(state().subnet_mask));
WiFi.softAP(state().ssid, state().password, state().channel, state().ssid_hidden, state().max_clients);
#if CONFIG_IDF_TARGET_ESP32C3
WiFi.setTxPower(WIFI_POWER_8_5dBm);
WiFi.setTxPower(8);
#endif
if (!_dnsServer) {
IPAddress apIp = WiFi.softAPIP();
@@ -103,8 +108,4 @@ void APService::stopAP() {
WiFi.softAPdisconnect(true);
}
void APService::handleDNS() {
if (_dnsServer) {
_dnsServer->processNextRequest();
}
}
void APService::handleDNS() {}
+30 -5
View File
@@ -2,6 +2,7 @@
#include <esp_log.h>
#include <cstring>
#include <algorithm>
#include <utils/timing.h>
static const char* TAG = "WebServer";
@@ -121,6 +122,10 @@ esp_err_t WebServer::httpHandler(httpd_req_t* req) {
}
esp_err_t WebServer::wsHandler(httpd_req_t* req) {
esp_err_t result;
httpd_ws_frame_t frame;
esp_err_t ret;
TIME_IT(
WebServer* self = static_cast<WebServer*>(req->user_ctx);
if (req->method == HTTP_GET) {
@@ -133,17 +138,21 @@ esp_err_t WebServer::wsHandler(httpd_req_t* req) {
return ESP_OK;
}
httpd_ws_frame_t frame;
memset(&frame, 0, sizeof(httpd_ws_frame_t));
frame.type = HTTPD_WS_TYPE_BINARY;
esp_err_t ret = httpd_ws_recv_frame(req, &frame, 0);
TIME_IT(
ret = httpd_ws_recv_frame(req, &frame, 0);
if (ret != ESP_OK) {
ESP_LOGE(TAG, "Failed to get frame len: %s", esp_err_to_name(ret));
return ret;
}
, FRAME_LEN)
if (frame.len > 0) {
TIME_IT(
frame.payload = (uint8_t*)malloc(frame.len);
if (!frame.payload) {
ESP_LOGE(TAG, "Failed to allocate frame payload");
@@ -156,6 +165,7 @@ esp_err_t WebServer::wsHandler(httpd_req_t* req) {
free(frame.payload);
return ret;
}
, FRAME_RECEIVE)
}
if (frame.type == HTTPD_WS_TYPE_CLOSE) {
@@ -169,15 +179,18 @@ esp_err_t WebServer::wsHandler(httpd_req_t* req) {
return ESP_OK;
}
esp_err_t result = ESP_OK;
result = ESP_OK;
if (self->wsFrameHandler_) {
TIME_IT(
result = self->wsFrameHandler_(req, &frame);
, FRAME_HANDLER)
}
if (frame.payload) {
free(frame.payload);
}
, WS_HANDLER)
return result;
}
@@ -265,7 +278,19 @@ esp_err_t WebServer::wsSend(int sockfd, const uint8_t* data, size_t len) {
.type = HTTPD_WS_TYPE_BINARY,
.payload = const_cast<uint8_t*>(data),
.len = len};
return httpd_ws_send_frame_async(server_, sockfd, &frame);
xSemaphoreTake(wsMutex_, portMAX_DELAY);
esp_err_t ret = httpd_ws_send_frame_async(server_, sockfd, &frame);
xSemaphoreGive(wsMutex_);
if (ret != ESP_OK) {
if (httpd_ws_get_fd_info(server_, sockfd) != HTTPD_WS_CLIENT_WEBSOCKET) {
ESP_LOGW(TAG, "Removing disconnected client %d", sockfd);
removeWsClient(sockfd);
return ESP_ERR_INVALID_STATE;
}
}
return ret;
}
esp_err_t WebServer::wsSendAll(const uint8_t* data, size_t len) {
@@ -278,7 +303,7 @@ esp_err_t WebServer::wsSendAll(const uint8_t* data, size_t len) {
}
esp_err_t WebServer::sendError(httpd_req_t* req, int status, const char* message) {
return send(req, status, (uint8_t*) message, strlen(message));
return send(req, status, (uint8_t*)message, strlen(message));
}
esp_err_t WebServer::sendOk(httpd_req_t* req) { return send(req, 200, nullptr, 0); }
+19 -1
View File
@@ -24,8 +24,26 @@ void Websocket::onWsClose(int sockfd) {
}
esp_err_t Websocket::onFrame(httpd_req_t* req, httpd_ws_frame_t* frame) {
// Handle PING - respond with PONG
if (frame->type == HTTPD_WS_TYPE_PING) {
httpd_ws_frame_t pong = {
.final = true,
.fragmented = false,
.type = HTTPD_WS_TYPE_PONG,
.payload = frame->payload,
.len = frame->len
};
return httpd_ws_send_frame(req, &pong);
}
// Ignore PONG frames
if (frame->type == HTTPD_WS_TYPE_PONG) {
return ESP_OK;
}
// Ignore other non-binary frames
if (frame->type != HTTPD_WS_TYPE_BINARY) {
ESP_LOGW(TAG, "Expected binary frame, got type %d", frame->type);
ESP_LOGD(TAG, "Ignoring frame type %d", frame->type);
return ESP_OK;
}
+241 -65
View File
@@ -3,13 +3,18 @@
#include <vector>
#include <cstring>
#include "utils/string_utils.hpp"
#include <esp_log.h>
#include <pb_encode.h>
#include <pb_decode.h>
#include "esp_vfs_fat.h"
#include <sdmmc_cmd.h>
#include <driver/sdmmc_host.h>
static const char *TAG = "FileService";
static const char *TAG = "FileSystem";
namespace FileSystem {
// Storage for dynamically allocated FileEntry arrays
static std::vector<api_FileEntry*> allocatedEntries;
static std::vector<api_FileEntry *> allocatedEntries;
static void freeAllocatedEntries() {
for (auto ptr : allocatedEntries) {
@@ -18,67 +23,81 @@ static void freeAllocatedEntries() {
allocatedEntries.clear();
}
void listFilesProto(const std::string &directory, api_FileEntry *entry) {
File root = ESP_FS.open(directory.find("/") == 0 ? directory.c_str() : ("/" + directory).c_str());
if (!root.isDirectory()) {
static void listFilesProtoRecursive(const std::string &directory, api_FileEntry *entry) {
DIR *dir = opendir(directory.c_str());
if (!dir) {
entry->children_count = 0;
entry->children = nullptr;
return;
}
// First pass: count children
std::vector<File> files;
File file = root.openNextFile();
while (file) {
files.push_back(file);
file = root.openNextFile();
}
std::vector<std::string> names;
std::vector<bool> isDirs;
std::vector<size_t> sizes;
if (files.empty()) {
struct dirent *d;
while ((d = readdir(dir)) != nullptr) {
if (strcmp(d->d_name, ".") == 0 || strcmp(d->d_name, "..") == 0) continue;
std::string fullPath = directory + "/" + d->d_name;
struct stat st;
if (stat(fullPath.c_str(), &st) == 0) {
names.push_back(d->d_name);
isDirs.push_back(S_ISDIR(st.st_mode));
sizes.push_back(st.st_size);
}
}
closedir(dir);
if (names.empty()) {
entry->children_count = 0;
entry->children = nullptr;
return;
}
// Allocate children array
entry->children_count = files.size();
entry->children = new api_FileEntry[files.size()];
entry->children_count = names.size();
entry->children = new api_FileEntry[names.size()];
allocatedEntries.push_back(entry->children);
// Fill children
for (size_t i = 0; i < files.size(); i++) {
for (size_t i = 0; i < names.size(); i++) {
api_FileEntry &child = entry->children[i];
memset(&child, 0, sizeof(child));
std::string name = std::string(files[i].name());
strncpy(child.name, name.c_str(), sizeof(child.name) - 1);
strncpy(child.name, names[i].c_str(), sizeof(child.name) - 1);
child.name[sizeof(child.name) - 1] = '\0';
child.is_directory = files[i].isDirectory();
child.is_directory = isDirs[i];
if (child.is_directory) {
listFilesProto(name, &child);
std::string childPath = directory + "/" + names[i];
listFilesProtoRecursive(childPath, &child);
} else {
child.size = files[i].size();
child.size = sizes[i];
child.children_count = 0;
child.children = nullptr;
}
}
}
void listFilesProto(const std::string &directory, api_FileEntry *entry) {
std::string path = directory;
if (path.empty() || path[0] != '/') {
path = "/" + directory;
}
std::string fullPath = path;
listFilesProtoRecursive(fullPath, entry);
}
esp_err_t getFilesProto(httpd_req_t *request) {
freeAllocatedEntries(); // Clean up any previous allocations
freeAllocatedEntries();
api_Response res = api_Response_init_zero;
res.status_code = 200;
res.which_payload = api_Response_file_list_tag;
// Create root entry
api_FileEntry rootEntry = api_FileEntry_init_zero;
strncpy(rootEntry.name, "root", sizeof(rootEntry.name) - 1);
rootEntry.is_directory = true;
listFilesProto("/", &rootEntry);
listFilesProtoRecursive(MOUNT_POINT, &rootEntry);
// Allocate entries array for FileList
res.payload.file_list.entries_count = 1;
res.payload.file_list.entries = new api_FileEntry[1];
allocatedEntries.push_back(res.payload.file_list.entries);
@@ -86,28 +105,178 @@ esp_err_t getFilesProto(httpd_req_t *request) {
esp_err_t result = WebServer::send(request, 200, res, api_Response_fields);
freeAllocatedEntries(); // Clean up after sending
freeAllocatedEntries();
return result;
}
bool init() {
esp_vfs_littlefs_conf_t conf = {
.base_path = LITTLEFS_MOUNT_POINT,
.partition_label = "spiffs",
.format_if_mount_failed = true,
.dont_mount = false,
};
esp_err_t ret = esp_vfs_littlefs_register(&conf);
if (ret != ESP_OK) {
if (ret == ESP_FAIL) {
ESP_LOGE(TAG, "Failed to mount or format filesystem");
} else if (ret == ESP_ERR_NOT_FOUND) {
ESP_LOGE(TAG, "Failed to find LittleFS partition");
} else {
ESP_LOGE(TAG, "Failed to initialize LittleFS (%s)", esp_err_to_name(ret));
}
return false;
}
size_t total = 0, used = 0;
ret = esp_littlefs_info("spiffs", &total, &used);
if (ret == ESP_OK) {
ESP_LOGI(TAG, "Partition size: total: %d, used: %d", total, used);
}
mkdirRecursive(FS_CONFIG_DIRECTORY);
esp_vfs_fat_sdmmc_mount_config_t sd_mount_config = {
.format_if_mount_failed = false,
.max_files = 4,
.allocation_unit_size = 16 * 1024,
};
sdmmc_host_t host = SDMMC_HOST_DEFAULT();
host.flags = SDMMC_HOST_FLAG_1BIT; // Use 1-bit mode
host.max_freq_khz = SDMMC_FREQ_DEFAULT;
sdmmc_slot_config_t slot_config = SDMMC_SLOT_CONFIG_DEFAULT();
slot_config.width = 1; // 1-bit mode
slot_config.clk = SD_CLK_PIN;
slot_config.cmd = SD_CMD_PIN;
slot_config.d0 = SD_DATA_PIN;
slot_config.flags |= SDMMC_SLOT_FLAG_INTERNAL_PULLUP;
sdmmc_card_t *card = nullptr;
esp_err_t err = esp_vfs_fat_sdmmc_mount(SD_MOUNT_POINT, &host, &slot_config, &sd_mount_config, &card);
if (err != ESP_OK) {
if (err == ESP_FAIL) {
ESP_LOGW(TAG, "Failed to mount SD card filesystem");
} else {
ESP_LOGW(TAG, "SD card not present or failed to initialize (%s)", esp_err_to_name(err));
}
// Don't fail - SD card is optional
} else {
ESP_LOGI(TAG, "SD card mounted at %s", SD_MOUNT_POINT);
ESP_LOGI(TAG, "SD card: %s, %lluMB", card->cid.name,
((uint64_t)card->csd.capacity) * card->csd.sector_size / (1024 * 1024));
}
return true;
}
bool fileExists(const char *filename) {
struct stat st;
return stat(filename, &st) == 0;
}
std::string readFile(const char *filename) {
FILE *f = fopen(filename, "r");
if (!f) {
return "";
}
fseek(f, 0, SEEK_END);
long size = ftell(f);
fseek(f, 0, SEEK_SET);
std::string content;
content.resize(size);
fread(&content[0], 1, size, f);
fclose(f);
return content;
}
bool writeFile(const char *filename, const char *content) {
FILE *f = fopen(filename, "w");
if (!f) {
ESP_LOGE(TAG, "Failed to open file for writing: %s", filename);
return false;
}
size_t len = strlen(content);
size_t written = fwrite(content, 1, len, f);
fclose(f);
return written == len;
}
bool writeFile(const char *filename, const uint8_t *content, size_t size) {
FILE *f = fopen(filename, "wb");
if (!f) {
ESP_LOGE(TAG, "Failed to open file for writing: %s", filename);
return false;
}
size_t written = fwrite(content, 1, size, f);
fclose(f);
return written == size;
}
bool mkdirRecursive(const char *path) {
char tmp[256];
char *p = nullptr;
size_t len;
snprintf(tmp, sizeof(tmp), "%s", path);
len = strlen(tmp);
if (tmp[len - 1] == '/') {
tmp[len - 1] = 0;
}
for (p = tmp + 1; *p; p++) {
if (*p == '/') {
*p = 0;
struct stat st;
if (stat(tmp, &st) != 0) {
if (::mkdir(tmp, 0755) != 0) {
ESP_LOGE(TAG, "Failed to create directory: %s", tmp);
return false;
}
}
*p = '/';
}
}
struct stat st;
if (stat(tmp, &st) != 0) {
if (::mkdir(tmp, 0755) != 0) {
ESP_LOGE(TAG, "Failed to create directory: %s", tmp);
return false;
}
}
return true;
}
esp_err_t getFiles(httpd_req_t *request) {
std::string files = listFiles("/");
std::string files = listFiles(MOUNT_POINT);
httpd_resp_set_type(request, "application/json");
return httpd_resp_send(request, files.c_str(), files.length());
}
esp_err_t getConfigFile(httpd_req_t *request) {
const char *uri = request->uri;
std::string path = "/config" + std::string(uri).substr(11);
if (!ESP_FS.exists(path.c_str())) {
std::string path = std::string(LITTLEFS_MOUNT_POINT) + "/config" + std::string(uri).substr(11);
if (!fileExists(path.c_str())) {
return WebServer::sendError(request, 404, "File not found");
}
File file = ESP_FS.open(path.c_str(), "r");
if (!file) {
return WebServer::sendError(request, 500, "Failed to open file");
std::string content = readFile(path.c_str());
if (content.empty()) {
return WebServer::sendError(request, 500, "Failed to read file");
}
String content = file.readString();
file.close();
if (ends_with(path, ".pb")) {
httpd_resp_set_type(request, "application/x-protobuf");
} else if (ends_with(path, ".json")) {
@@ -119,10 +288,11 @@ esp_err_t getConfigFile(httpd_req_t *request) {
}
esp_err_t handleDelete(httpd_req_t *request, const api_FileDeleteRequest &req) {
ESP_LOGI(TAG, "Deleting file: %s", req.path);
std::string fullPath = req.path;
ESP_LOGI(TAG, "Deleting file: %s", fullPath.c_str());
api_Response res = api_Response_init_zero;
if (deleteFile(req.path)) {
if (deleteFile(fullPath.c_str())) {
res.status_code = 200;
res.which_payload = api_Response_empty_message_tag;
return WebServer::send(request, 200, res, api_Response_fields);
@@ -132,10 +302,11 @@ esp_err_t handleDelete(httpd_req_t *request, const api_FileDeleteRequest &req) {
}
esp_err_t handleEdit(httpd_req_t *request, const api_FileEditRequest &req) {
ESP_LOGI(TAG, "Editing file: %s", req.path);
std::string fullPath = req.path;
ESP_LOGI(TAG, "Editing file: %s", fullPath.c_str());
api_Response res = api_Response_init_zero;
if (editFile(req.path, req.content->bytes, req.content->size)) {
if (editFile(fullPath.c_str(), req.content->bytes, req.content->size)) {
res.status_code = 200;
res.which_payload = api_Response_empty_message_tag;
return WebServer::send(request, 200, res, api_Response_fields);
@@ -144,52 +315,57 @@ esp_err_t handleEdit(httpd_req_t *request, const api_FileEditRequest &req) {
}
}
bool deleteFile(const char *filename) { return ESP_FS.remove(filename); }
bool deleteFile(const char *filename) { return remove(filename) == 0; }
std::string listFiles(const std::string &directory, bool isRoot) {
File root = ESP_FS.open(directory.find("/") == 0 ? directory.c_str() : ("/" + directory).c_str());
if (!root.isDirectory()) return "{}";
File file = root.openNextFile();
if (!file) {
DIR *dir = opendir(directory.c_str());
if (!dir) {
return isRoot ? "{ \"root\": {} }" : "{}";
}
std::string output = isRoot ? "{ \"root\": {" : "{";
bool first = true;
while (file) {
std::string name = std::string(file.name());
if (file.isDirectory()) {
output += "\"" + name + "\": " + listFiles(name, false);
} else {
output += "\"" + name + "\": " + std::to_string(file.size());
struct dirent *entry;
while ((entry = readdir(dir)) != nullptr) {
if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0) {
continue;
}
File next = root.openNextFile();
if (next) output += ", ";
file = next;
if (!first) {
output += ", ";
}
first = false;
std::string fullPath = directory + "/" + entry->d_name;
struct stat st;
if (stat(fullPath.c_str(), &st) == 0) {
if (S_ISDIR(st.st_mode)) {
output += "\"" + std::string(entry->d_name) + "\": " + listFiles(fullPath, false);
} else {
output += "\"" + std::string(entry->d_name) + "\": " + std::to_string(st.st_size);
}
}
}
closedir(dir);
output += "}";
if (isRoot) output += "}";
return output;
}
bool editFile(const char *filename, const uint8_t *content, size_t size) {
File file = ESP_FS.open(filename, FILE_WRITE);
if (!file) return false;
bool editFile(const char *filename, const uint8_t *content, size_t size) { return writeFile(filename, content, size); }
file.write(content, size);
file.close();
return true;
}
bool editFile(const char *filename, const char *content) { return writeFile(filename, content); }
esp_err_t mkdir(httpd_req_t *request, const api_FileMkdirRequest &req) {
ESP_LOGI(TAG, "Creating directory: %s", req.path);
std::string fullPath = req.path;
ESP_LOGI(TAG, "Creating directory: %s", fullPath.c_str());
api_Response res = api_Response_init_zero;
if (ESP_FS.mkdir(req.path)) {
if (mkdirRecursive(fullPath.c_str())) {
res.status_code = 200;
res.which_payload = api_Response_empty_message_tag;
return WebServer::send(request, 200, res, api_Response_fields);
+338 -106
View File
@@ -1,9 +1,14 @@
#include <filesystem_ws.h>
#include <filesystem.h>
#include <pb_encode.h>
#include <pb_decode.h>
#include <esp_littlefs.h>
#include <esp_timer.h>
#include <esp_log.h>
#include <dirent.h>
#include <sys/stat.h>
#include <cstring>
#include <cerrno>
#include <utils/timing.h>
static const char* TAG = "FileSystemWS";
@@ -11,7 +16,137 @@ namespace FileSystemWS {
FileSystemHandler fsHandler;
FileSystemHandler::FileSystemHandler() : transferIdCounter_(0) {}
FileSystemHandler::FileSystemHandler()
: transferIdCounter_(0), writeQueue_(nullptr), writerTaskHandle_(nullptr), uploadsMutex_(nullptr),
writerTaskRunning_(false) {
uploadsMutex_ = xSemaphoreCreateMutex();
}
FileSystemHandler::~FileSystemHandler() {
stopWriterTask();
if (uploadsMutex_) {
vSemaphoreDelete(uploadsMutex_);
}
}
void FileSystemHandler::startWriterTask() {
if (writerTaskHandle_ != nullptr) {
return;
}
writeQueue_ = xQueueCreate(FS_WRITE_QUEUE_SIZE, sizeof(WriteRequest));
if (!writeQueue_) {
ESP_LOGE(TAG, "Failed to create write queue");
return;
}
writerTaskRunning_ = true;
BaseType_t result =
xTaskCreate(writerTaskFunc, "fs_writer", FS_WRITER_TASK_STACK_SIZE, this, FS_WRITER_TASK_PRIORITY, &writerTaskHandle_);
if (result != pdPASS) {
ESP_LOGE(TAG, "Failed to create writer task");
vQueueDelete(writeQueue_);
writeQueue_ = nullptr;
writerTaskRunning_ = false;
} else {
ESP_LOGI(TAG, "Writer task started");
}
}
void FileSystemHandler::stopWriterTask() {
if (!writerTaskRunning_) {
return;
}
writerTaskRunning_ = false;
// Send a poison pill to wake up the task
WriteRequest poison = {0, nullptr, 0, 0, false};
if (writeQueue_) {
xQueueSend(writeQueue_, &poison, portMAX_DELAY);
}
// Wait for task to finish
if (writerTaskHandle_) {
vTaskDelay(pdMS_TO_TICKS(100));
writerTaskHandle_ = nullptr;
}
if (writeQueue_) {
// Drain any remaining requests and free their data
WriteRequest req;
while (xQueueReceive(writeQueue_, &req, 0) == pdTRUE) {
if (req.data) {
free(req.data);
}
}
vQueueDelete(writeQueue_);
writeQueue_ = nullptr;
}
ESP_LOGI(TAG, "Writer task stopped");
}
void FileSystemHandler::writerTaskFunc(void* param) {
FileSystemHandler* self = static_cast<FileSystemHandler*>(param);
WriteRequest req;
while (self->writerTaskRunning_) {
if (xQueueReceive(self->writeQueue_, &req, pdMS_TO_TICKS(10)) == pdTRUE) {
if (req.data == nullptr) {
// Poison pill - exit
break;
}
self->processWriteRequest(req);
free(req.data);
}
}
vTaskDelete(nullptr);
}
void FileSystemHandler::processWriteRequest(const WriteRequest& req) {
xSemaphoreTake(uploadsMutex_, portMAX_DELAY);
auto it = uploads_.find(req.transferId);
if (it == uploads_.end()) {
xSemaphoreGive(uploadsMutex_);
return;
}
UploadState& state = it->second;
if (state.hasError) {
xSemaphoreGive(uploadsMutex_);
return;
}
size_t bytesWritten = fwrite(req.data, 1, req.size, state.file);
if (bytesWritten != req.size) {
state.hasError = true;
state.errorMessage = "Failed to write chunk";
xSemaphoreGive(uploadsMutex_);
finalizeUpload(req.transferId, false, state.errorMessage);
return;
}
state.chunksWritten++;
ESP_LOGD(TAG, "Async write chunk %u/%u: %u bytes", state.chunksWritten, state.totalChunks, bytesWritten);
// Periodic flush
if (state.chunksWritten > 0 && state.chunksWritten % 64 == 0) {
fflush(state.file);
}
bool shouldFinalize = req.isLastChunk;
xSemaphoreGive(uploadsMutex_);
if (shouldFinalize) {
finalizeUpload(req.transferId, true);
}
}
void FileSystemHandler::setSendCallbacks(SendMetadataCallback sendMetadata, SendCallback sendData,
SendCompleteCallback sendComplete,
@@ -29,7 +164,7 @@ void FileSystemHandler::cleanupExpiredTransfers() {
while (dlIt != downloads_.end()) {
if (now - dlIt->second.lastActivityTime > FS_TRANSFER_TIMEOUT_MS) {
if (dlIt->second.file) {
dlIt->second.file.close();
fclose(dlIt->second.file);
}
ESP_LOGW(TAG, "Download %u timed out", dlIt->first);
@@ -49,38 +184,50 @@ void FileSystemHandler::cleanupExpiredTransfers() {
}
}
xSemaphoreTake(uploadsMutex_, portMAX_DELAY);
auto ulIt = uploads_.begin();
while (ulIt != uploads_.end()) {
if (now - ulIt->second.lastActivityTime > FS_TRANSFER_TIMEOUT_MS) {
if (ulIt->second.file) {
ulIt->second.file.close();
fclose(ulIt->second.file);
ulIt->second.file = nullptr;
}
ESP_FS.remove(ulIt->second.path.c_str());
ESP_LOGW(TAG, "Upload %u timed out, deleted partial file", ulIt->first);
std::string path = ulIt->second.path;
uint32_t chunksReceived = ulIt->second.chunksReceived;
int clientId = ulIt->second.clientId;
uint32_t transferId = ulIt->first;
ulIt = uploads_.erase(ulIt);
xSemaphoreGive(uploadsMutex_);
remove(path.c_str());
ESP_LOGW(TAG, "Upload %u timed out, deleted partial file", transferId);
if (sendUploadCompleteCallback_) {
socket_message_FSUploadComplete complete = socket_message_FSUploadComplete_init_zero;
complete.transfer_id = ulIt->first;
complete.transfer_id = transferId;
complete.success = false;
strncpy(complete.error, "Transfer timed out", sizeof(complete.error) - 1);
complete.chunks_received = ulIt->second.chunksReceived;
sendUploadCompleteCallback_(complete, ulIt->second.clientId);
complete.chunks_received = chunksReceived;
sendUploadCompleteCallback_(complete, clientId);
}
ulIt = uploads_.erase(ulIt);
xSemaphoreTake(uploadsMutex_, portMAX_DELAY);
} else {
++ulIt;
}
}
xSemaphoreGive(uploadsMutex_);
}
socket_message_FSDeleteResponse FileSystemHandler::handleDelete(const socket_message_FSDeleteRequest& req) {
socket_message_FSDeleteResponse response = socket_message_FSDeleteResponse_init_zero;
std::string path(req.path);
std::string path = req.path;
ESP_LOGI(TAG, "Delete request: %s", path.c_str());
if (!ESP_FS.exists(path.c_str())) {
struct stat st;
if (stat(path.c_str(), &st) != 0) {
response.success = false;
strncpy(response.error, "File or directory not found", sizeof(response.error) - 1);
return response;
@@ -97,41 +244,45 @@ socket_message_FSDeleteResponse FileSystemHandler::handleDelete(const socket_mes
}
bool FileSystemHandler::deleteRecursive(const std::string& path) {
File file = ESP_FS.open(path.c_str());
if (!file) return false;
struct stat st;
if (stat(path.c_str(), &st) != 0) return false;
if (file.isDirectory()) {
File child = file.openNextFile();
while (child) {
std::string childPath = std::string(child.name());
child.close();
if (S_ISDIR(st.st_mode)) {
DIR* dir = opendir(path.c_str());
if (!dir) return false;
struct dirent* entry;
while ((entry = readdir(dir)) != nullptr) {
if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0) {
continue;
}
std::string childPath = path + "/" + entry->d_name;
if (!deleteRecursive(childPath)) {
file.close();
closedir(dir);
return false;
}
child = file.openNextFile();
}
file.close();
return ESP_FS.rmdir(path.c_str());
closedir(dir);
return rmdir(path.c_str()) == 0;
} else {
file.close();
return ESP_FS.remove(path.c_str());
return remove(path.c_str()) == 0;
}
}
socket_message_FSMkdirResponse FileSystemHandler::handleMkdir(const socket_message_FSMkdirRequest& req) {
socket_message_FSMkdirResponse response = socket_message_FSMkdirResponse_init_zero;
std::string path(req.path);
std::string path = req.path;
ESP_LOGI(TAG, "Mkdir request: %s", path.c_str());
if (ESP_FS.exists(path.c_str())) {
struct stat st;
if (stat(path.c_str(), &st) == 0) {
response.success = false;
strncpy(response.error, "Path already exists", sizeof(response.error) - 1);
return response;
}
if (ESP_FS.mkdir(path.c_str())) {
if (FileSystem::mkdirRecursive(path.c_str())) {
response.success = true;
} else {
response.success = false;
@@ -142,32 +293,49 @@ socket_message_FSMkdirResponse FileSystemHandler::handleMkdir(const socket_messa
}
void FileSystemHandler::listDirectory(const std::string& path, socket_message_FSListResponse& response) {
File dir = ESP_FS.open(path.c_str());
if (!dir || !dir.isDirectory()) {
// Root "/" is virtual - list mount points instead
if (strcmp(path.c_str(), "/") == 0) {
strncpy(response.directories[0].name, LITTLEFS_MOUNT_POINT + 1, sizeof(response.directories[0].name) - 1);
strncpy(response.directories[1].name, SD_MOUNT_POINT + 1, sizeof(response.directories[1].name) - 1);
response.directories_count = 2;
return;
}
File file = dir.openNextFile();
DIR* dir = opendir(path.c_str());
if (!dir) {
return;
}
struct dirent* entry;
int fileCount = 0;
int dirCount = 0;
while (file && fileCount < 20 && dirCount < 20) { // Limit to match protobuf max_count
if (file.isDirectory()) {
while ((entry = readdir(dir)) != nullptr && fileCount < 20 && dirCount < 20) {
if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0) {
continue;
}
std::string fullPath = path + "/" + entry->d_name;
struct stat st;
if (stat(fullPath.c_str(), &st) != 0) continue;
if (S_ISDIR(st.st_mode)) {
if (dirCount < 20) {
strncpy(response.directories[dirCount].name, file.name(),
strncpy(response.directories[dirCount].name, entry->d_name,
sizeof(response.directories[dirCount].name) - 1);
dirCount++;
}
} else {
if (fileCount < 20) {
strncpy(response.files[fileCount].name, file.name(), sizeof(response.files[fileCount].name) - 1);
response.files[fileCount].size = file.size();
strncpy(response.files[fileCount].name, entry->d_name, sizeof(response.files[fileCount].name) - 1);
response.files[fileCount].size = st.st_size;
fileCount++;
}
}
file = dir.openNextFile();
}
closedir(dir);
response.files_count = fileCount;
response.directories_count = dirCount;
}
@@ -175,12 +343,13 @@ void FileSystemHandler::listDirectory(const std::string& path, socket_message_FS
socket_message_FSListResponse FileSystemHandler::handleList(const socket_message_FSListRequest& req) {
socket_message_FSListResponse response = socket_message_FSListResponse_init_zero;
std::string path(req.path);
if (path.empty()) path = "/";
std::string path = req.path;
ESP_LOGI(TAG, "List request: %s", path.c_str());
if (!ESP_FS.exists(path.c_str())) {
struct stat st;
// Make sure that path exists, or that it is a root listing
if (strcmp(path.c_str(), "/") != 0 && stat(path.c_str(), &st) != 0) {
response.success = false;
strncpy(response.error, "Path not found", sizeof(response.error) - 1);
return response;
@@ -192,14 +361,12 @@ socket_message_FSListResponse FileSystemHandler::handleList(const socket_message
return response;
}
// ===== STREAMING DOWNLOAD =====
void FileSystemHandler::handleDownloadRequest(const socket_message_FSDownloadRequest& req, int clientId) {
std::string path(req.path);
std::string path = req.path;
ESP_LOGI(TAG, "Download request: %s", path.c_str());
// Validate file exists
if (!ESP_FS.exists(path.c_str())) {
struct stat st;
if (stat(path.c_str(), &st) != 0 || S_ISDIR(st.st_mode)) {
if (sendMetadataCallback_) {
socket_message_FSDownloadMetadata metadata = socket_message_FSDownloadMetadata_init_zero;
metadata.success = false;
@@ -209,8 +376,8 @@ void FileSystemHandler::handleDownloadRequest(const socket_message_FSDownloadReq
return;
}
File file = ESP_FS.open(path.c_str(), "r");
if (!file || file.isDirectory()) {
FILE* file = fopen(path.c_str(), "rb");
if (!file) {
if (sendMetadataCallback_) {
socket_message_FSDownloadMetadata metadata = socket_message_FSDownloadMetadata_init_zero;
metadata.success = false;
@@ -220,11 +387,7 @@ void FileSystemHandler::handleDownloadRequest(const socket_message_FSDownloadReq
return;
}
// Set file buffer size large, so we use ram to read from
// Set buffer size so 1 mb (static) TODO: Check that there is enough ram to do this
file.setBufferSize(1000000);
uint32_t fileSize = file.size();
uint32_t fileSize = st.st_size;
uint32_t chunkSize = FS_MAX_CHUNK_SIZE;
uint32_t totalChunks = (fileSize + chunkSize - 1) / chunkSize;
if (totalChunks == 0) totalChunks = 1;
@@ -254,10 +417,8 @@ void FileSystemHandler::handleDownloadRequest(const socket_message_FSDownloadReq
ESP_LOGI(TAG, "Download started: %s, size=%u, chunks=%u, id=%u", path.c_str(), fileSize, totalChunks, transferId);
// Start streaming chunks immediately
while (sendNextDownloadChunk(transferId)) {
// Keep sending until done or error
taskYIELD(); // Allow other tasks to run
taskYIELD();
}
}
@@ -280,7 +441,7 @@ bool FileSystemHandler::sendNextDownloadChunk(uint32_t transferId) {
sendCompleteCallback_(complete, state.clientId);
}
state.file.close();
fclose(state.file);
downloads_.erase(it);
ESP_LOGI(TAG, "Download completed: %u", transferId);
return false;
@@ -297,8 +458,28 @@ bool FileSystemHandler::sendNextDownloadChunk(uint32_t transferId) {
bytesToRead = state.fileSize - position;
}
size_t bytesRead = state.file.read(data->data.bytes, bytesToRead);
// Allocate buffer for FT_POINTER data field
data->data = (pb_bytes_array_t*)malloc(PB_BYTES_ARRAY_T_ALLOCSIZE(bytesToRead));
if (!data->data) {
delete data;
if (sendCompleteCallback_) {
socket_message_FSDownloadComplete complete = socket_message_FSDownloadComplete_init_zero;
complete.transfer_id = transferId;
complete.success = false;
strncpy(complete.error, "Memory allocation failed", sizeof(complete.error) - 1);
complete.total_chunks = state.chunksSent;
complete.file_size = state.fileSize;
sendCompleteCallback_(complete, state.clientId);
}
fclose(state.file);
downloads_.erase(it);
ESP_LOGE(TAG, "Download failed - memory allocation: %u", transferId);
return false;
}
size_t bytesRead = fread(data->data->bytes, 1, bytesToRead, state.file);
if (bytesRead == 0 && bytesToRead > 0) {
free(data->data);
delete data;
if (sendCompleteCallback_) {
socket_message_FSDownloadComplete complete = socket_message_FSDownloadComplete_init_zero;
@@ -310,17 +491,18 @@ bool FileSystemHandler::sendNextDownloadChunk(uint32_t transferId) {
sendCompleteCallback_(complete, state.clientId);
}
state.file.close();
fclose(state.file);
downloads_.erase(it);
ESP_LOGE(TAG, "Download failed - read error: %u", transferId);
return false;
}
data->data.size = bytesRead;
data->data->size = bytesRead;
if (sendDataCallback_) {
sendDataCallback_(*data, state.clientId);
}
free(data->data);
delete data;
state.chunksSent++;
ESP_LOGD(TAG, "Download chunk %u/%u sent: %u bytes", state.chunksSent, state.totalChunks, bytesRead);
@@ -328,44 +510,48 @@ bool FileSystemHandler::sendNextDownloadChunk(uint32_t transferId) {
return true;
}
// ===== STREAMING UPLOAD =====
socket_message_FSUploadStartResponse FileSystemHandler::handleUploadStart(const socket_message_FSUploadStart& req,
int clientId) {
socket_message_FSUploadStartResponse response = socket_message_FSUploadStartResponse_init_zero;
std::string path(req.path);
std::string path = req.path;
ESP_LOGI(TAG, "Upload start request: %s, size=%u, chunks=%u", path.c_str(), req.file_size, req.total_chunks);
// Check available space
size_t fs_total = 0, fs_used = 0;
esp_littlefs_info("spiffs", &fs_total, &fs_used);
size_t freeSpace = fs_total - fs_used;
if (freeSpace < req.file_size + 4096) { // 4KB safety margin
response.success = false;
strncpy(response.error, "Insufficient storage space", sizeof(response.error) - 1);
return response;
// Check available space on the target filesystem
if (path.find(SD_MOUNT_POINT) != 0) {
// LittleFS path
size_t fs_total = 0, fs_used = 0;
esp_littlefs_info("spiffs", &fs_total, &fs_used);
size_t freeSpace = fs_total - fs_used;
if (freeSpace < req.file_size + 4096) {
response.success = false;
strncpy(response.error, "Insufficient storage space", sizeof(response.error) - 1);
return response;
}
}
// TODO: SD card space check skipped - FAT doesn't have a simple API for this
// Ensure parent directory exists
size_t lastSlash = path.find_last_of('/');
if (lastSlash != std::string::npos && lastSlash > 0) {
std::string parentDir = path.substr(0, lastSlash);
if (!ESP_FS.exists(parentDir.c_str())) {
struct stat st;
if (stat(parentDir.c_str(), &st) != 0) {
response.success = false;
strncpy(response.error, "Parent directory does not exist", sizeof(response.error) - 1);
return response;
}
}
File file = ESP_FS.open(path.c_str(), FILE_WRITE);
FILE* file = fopen(path.c_str(), "wb");
if (!file) {
ESP_LOGE(TAG, "fopen failed for '%s': %s (errno=%d)", path.c_str(), strerror(errno), errno);
response.success = false;
strncpy(response.error, "Cannot open file for writing", sizeof(response.error) - 1);
snprintf(response.error, sizeof(response.error) - 1, "Cannot open file: %s", strerror(errno));
return response;
}
file.setBufferSize(1000000);
// Set larger buffer for better write performance
setvbuf(file, nullptr, _IOFBF, 32 * 1024);
uint32_t transferId = generateTransferId();
@@ -375,12 +561,15 @@ socket_message_FSUploadStartResponse FileSystemHandler::handleUploadStart(const
state.fileSize = req.file_size;
state.totalChunks = req.total_chunks;
state.chunksReceived = 0;
state.chunksWritten = 0;
state.bytesReceived = 0;
state.lastActivityTime = esp_timer_get_time() / 1000;
state.clientId = clientId;
state.hasError = false;
xSemaphoreTake(uploadsMutex_, portMAX_DELAY);
uploads_[transferId] = state;
xSemaphoreGive(uploadsMutex_);
response.success = true;
response.transfer_id = transferId;
@@ -393,8 +582,16 @@ socket_message_FSUploadStartResponse FileSystemHandler::handleUploadStart(const
void FileSystemHandler::handleUploadData(const socket_message_FSUploadData& req) {
uint32_t transferId = req.transfer_id;
// Auto-start writer task if not running
if (!writerTaskRunning_) {
startWriterTask();
}
xSemaphoreTake(uploadsMutex_, portMAX_DELAY);
auto it = uploads_.find(transferId);
if (it == uploads_.end()) {
xSemaphoreGive(uploadsMutex_);
ESP_LOGW(TAG, "Upload data for unknown transfer: %u", transferId);
return;
}
@@ -402,61 +599,93 @@ void FileSystemHandler::handleUploadData(const socket_message_FSUploadData& req)
UploadState& state = it->second;
state.lastActivityTime = esp_timer_get_time() / 1000;
// Skip if we already have an error
if (state.hasError) {
xSemaphoreGive(uploadsMutex_);
return;
}
// Validate chunk index (allow out-of-order but warn)
if (req.chunk_index != state.chunksReceived) {
ESP_LOGW(TAG, "Upload chunk out of order: expected %u, got %u", state.chunksReceived, req.chunk_index);
// For now, we'll accept it anyway and write sequentially
// A more robust implementation would buffer out-of-order chunks
}
// Write chunk data
size_t bytesWritten = state.file.write(req.data.bytes, req.data.size);
if (bytesWritten != req.data.size) {
if (!req.data || req.data->size == 0) {
state.hasError = true;
state.errorMessage = "Failed to write chunk";
state.errorMessage = "Empty or invalid data chunk";
xSemaphoreGive(uploadsMutex_);
finalizeUpload(transferId, false, state.errorMessage);
return;
}
state.chunksReceived++;
state.bytesReceived += bytesWritten;
// Copy data for async write
WriteRequest writeReq;
writeReq.transferId = transferId;
writeReq.size = req.data->size;
writeReq.chunkIndex = req.chunk_index;
writeReq.data = static_cast<uint8_t*>(malloc(req.data->size));
// Flush periodically to prevent LittleFS buffer issues
if (state.chunksReceived > 0 && state.chunksReceived % 64 == 0) {
ESP_LOGD(TAG, "Flushing file at chunk %u", state.chunksReceived);
state.file.flush();
if (!writeReq.data) {
state.hasError = true;
state.errorMessage = "Memory allocation failed";
xSemaphoreGive(uploadsMutex_);
finalizeUpload(transferId, false, state.errorMessage);
return;
}
ESP_LOGD(TAG, "Upload chunk %u/%u: %u bytes", state.chunksReceived, state.totalChunks, bytesWritten);
memcpy(writeReq.data, req.data->bytes, req.data->size);
// Check if upload is complete
if (state.chunksReceived >= state.totalChunks) {
finalizeUpload(transferId, true);
state.chunksReceived++;
state.bytesReceived += req.data->size;
writeReq.isLastChunk = (state.chunksReceived >= state.totalChunks);
ESP_LOGD(TAG, "Queuing chunk %u/%u: %u bytes", state.chunksReceived, state.totalChunks, req.data->size);
xSemaphoreGive(uploadsMutex_);
// Check queue is valid
if (!writeQueue_) {
ESP_LOGE(TAG, "Write queue not initialized");
free(writeReq.data);
finalizeUpload(transferId, false, "Write queue not initialized");
return;
}
// Try to queue (non-blocking) - if full, do sync write to avoid blocking HTTP server
if (xQueueSend(writeQueue_, &writeReq, 0) != pdTRUE) {
ESP_LOGD(TAG, "Queue full, doing sync write for chunk %u", writeReq.chunkIndex);
processWriteRequest(writeReq);
free(writeReq.data);
}
}
void FileSystemHandler::finalizeUpload(uint32_t transferId, bool success, const std::string& error) {
xSemaphoreTake(uploadsMutex_, portMAX_DELAY);
auto it = uploads_.find(transferId);
if (it == uploads_.end()) {
xSemaphoreGive(uploadsMutex_);
return;
}
UploadState& state = it->second;
if (state.file) {
state.file.close();
fclose(state.file);
state.file = nullptr;
}
std::string path = state.path;
uint32_t bytesReceived = state.bytesReceived;
uint32_t chunksReceived = state.chunksReceived;
int clientId = state.clientId;
uploads_.erase(it);
xSemaphoreGive(uploadsMutex_);
if (!success) {
ESP_FS.remove(state.path.c_str());
ESP_LOGW(TAG, "Upload failed, deleted partial file: %s", state.path.c_str());
remove(path.c_str());
ESP_LOGW(TAG, "Upload failed, deleted partial file: %s", path.c_str());
} else {
ESP_LOGI(TAG, "Upload completed: %s (%u bytes)", state.path.c_str(), state.bytesReceived);
ESP_LOGI(TAG, "Upload completed: %s (%u bytes)", path.c_str(), bytesReceived);
}
if (sendUploadCompleteCallback_) {
@@ -466,15 +695,11 @@ void FileSystemHandler::finalizeUpload(uint32_t transferId, bool success, const
if (!error.empty()) {
strncpy(complete.error, error.c_str(), sizeof(complete.error) - 1);
}
complete.chunks_received = state.chunksReceived;
sendUploadCompleteCallback_(complete, state.clientId);
complete.chunks_received = chunksReceived;
sendUploadCompleteCallback_(complete, clientId);
}
uploads_.erase(it);
}
// ===== TRANSFER CONTROL =====
socket_message_FSCancelTransferResponse FileSystemHandler::handleCancelTransfer(
const socket_message_FSCancelTransfer& req) {
socket_message_FSCancelTransferResponse response = socket_message_FSCancelTransferResponse_init_zero;
@@ -484,7 +709,7 @@ socket_message_FSCancelTransferResponse FileSystemHandler::handleCancelTransfer(
auto dlIt = downloads_.find(transferId);
if (dlIt != downloads_.end()) {
if (dlIt->second.file) {
dlIt->second.file.close();
fclose(dlIt->second.file);
}
downloads_.erase(dlIt);
response.success = true;
@@ -492,20 +717,27 @@ socket_message_FSCancelTransferResponse FileSystemHandler::handleCancelTransfer(
return response;
}
xSemaphoreTake(uploadsMutex_, portMAX_DELAY);
auto ulIt = uploads_.find(transferId);
if (ulIt != uploads_.end()) {
if (ulIt->second.file) {
ulIt->second.file.close();
fclose(ulIt->second.file);
ulIt->second.file = nullptr;
}
ESP_FS.remove(ulIt->second.path.c_str());
std::string path = ulIt->second.path;
uploads_.erase(ulIt);
xSemaphoreGive(uploadsMutex_);
remove(path.c_str());
response.success = true;
ESP_LOGI(TAG, "Upload cancelled: %u", transferId);
return response;
}
xSemaphoreGive(uploadsMutex_);
response.success = false;
return response;
}
void FileSystemHandler::processPendingDownloads() {}
} // namespace FileSystemWS
+18
View File
@@ -0,0 +1,18 @@
dependencies:
idf:
version: ">=5.0.0"
espressif/mdns:
version: "^1.3.0"
joltwallet/littlefs:
version: "^1.14.0"
espressif/esp-dsp:
version: "^1.4.0"
espressif/led_strip:
version: "^2.5.0"
espressif/esp32-camera:
version: "^2.0.0"
+71 -58
View File
@@ -1,6 +1,10 @@
#include <Arduino.h>
#include <ESPmDNS.h>
#include <WiFi.h>
#include <freertos/FreeRTOS.h>
#include <freertos/task.h>
#include <esp_log.h>
#include <esp_heap_caps.h>
#include <nvs_flash.h>
#include <wifi/wifi_idf.h>
#include <mdns.h>
#include <map>
#include <filesystem.h>
@@ -20,7 +24,7 @@
#include <www_mount.hpp>
Websocket socket {server, "/api/ws"};
Websocket wsSocket {server, "/api/ws"};
Peripherals peripherals;
ServoController servoController;
@@ -39,7 +43,9 @@ WiFiService wifiService;
APService apService;
void setupServer() {
server.config(50 + WWW_ASSETS_COUNT, 32768);
ESP_LOGI("Main", "Free heap before server: %lu, largest block: %lu",
esp_get_free_heap_size(), heap_caps_get_largest_free_block(MALLOC_CAP_8BIT));
server.config(50 + WWW_ASSETS_COUNT, 16384);
server.listen(80);
server.on("/api/system/reset", HTTP_POST,
@@ -73,42 +79,41 @@ void setupServer() {
server.on("/api/wifi/networks", HTTP_GET, [&](httpd_req_t *request) { return wifiService.getNetworks(request); });
server.on("/api/wifi/sta/status", HTTP_GET,
[&](httpd_req_t *request) { return wifiService.getNetworkStatus(request); });
server.on("/api/ap/status", HTTP_GET, [&](httpd_req_t *request) { return apService.getStatusProto(request); });
server.on("/api/ap/settings", HTTP_GET,
[&](httpd_req_t *request) { return apService.protoEndpoint.getState(request); });
server.on("/api/ap/settings", HTTP_POST,
[&](httpd_req_t *request, api_Request *protoReq) {
return apService.protoEndpoint.handleStateUpdate(request, protoReq);
});
[&](httpd_req_t *request) { return apService.protoEndpoint.getState(request); });
server.on("/api/ap/settings", HTTP_POST, [&](httpd_req_t *request, api_Request *protoReq) {
return apService.protoEndpoint.handleStateUpdate(request, protoReq);
});
server.on("/api/peripherals/settings", HTTP_GET,
[&](httpd_req_t *request) { return peripherals.protoEndpoint.getState(request); });
server.on("/api/peripherals/settings", HTTP_POST,
[&](httpd_req_t *request, api_Request *protoReq) {
return peripherals.protoEndpoint.handleStateUpdate(request, protoReq);
});
server.on("/api/peripherals/settings", HTTP_POST, [&](httpd_req_t *request, api_Request *protoReq) {
return peripherals.protoEndpoint.handleStateUpdate(request, protoReq);
});
#if FT_ENABLED(USE_MDNS)
server.on("/api/mdns/settings", HTTP_GET, [&](httpd_req_t *request) { return mdnsService.protoEndpoint.getState(request); });
server.on("/api/mdns/settings", HTTP_POST,
[&](httpd_req_t *request, api_Request *protoReq) {
return mdnsService.protoEndpoint.handleStateUpdate(request, protoReq);
});
server.on("/api/mdns/settings", HTTP_GET,
[&](httpd_req_t *request) { return mdnsService.protoEndpoint.getState(request); });
server.on("/api/mdns/settings", HTTP_POST, [&](httpd_req_t *request, api_Request *protoReq) {
return mdnsService.protoEndpoint.handleStateUpdate(request, protoReq);
});
server.on("/api/mdns/status", HTTP_GET, [&](httpd_req_t *request) { return mdnsService.getStatus(request); });
server.on("/api/mdns/query", HTTP_POST,
[&](httpd_req_t *request, api_Request *protoReq) {
return mdnsService.queryServices(request, protoReq);
});
server.on("/api/mdns/query", HTTP_POST, [&](httpd_req_t *request, api_Request *protoReq) {
return mdnsService.queryServices(request, protoReq);
});
#endif
server.on("/api/config/*", HTTP_GET, [](httpd_req_t *request) { return FileSystem::getConfigFile(request); });
server.on("/api/files", HTTP_GET, [&](httpd_req_t *request) { return FileSystem::getFilesProto(request); });
STAITC_PROTO_POST_ENDPOINT(server, "/api/files/delete", file_delete_request, FileSystem::handleDelete);
STAITC_PROTO_POST_ENDPOINT(server, "/api/files/edit", file_edit_request, FileSystem::handleEdit);
STAITC_PROTO_POST_ENDPOINT(server, "/api/files/mkdir", file_mkdir_request, FileSystem::mkdir);
wsSocket.begin();
#if EMBED_WEBAPP
mountStaticAssets(server);
mountSpaFallback(server);
#endif
server.on("/*", HTTP_OPTIONS, [](httpd_req_t *request) {
httpd_resp_set_status(request, "200 OK");
@@ -123,36 +128,36 @@ void setupServer() {
void setupEventSocket() {
FileSystemWS::fsHandler.setSendCallbacks(
[](const socket_message_FSDownloadMetadata &metadata, int clientId) { socket.emit(metadata, clientId); },
[](const socket_message_FSDownloadData &data, int clientId) { socket.emit(data, clientId); },
[](const socket_message_FSDownloadComplete &complete, int clientId) { socket.emit(complete, clientId); },
[](const socket_message_FSUploadComplete &complete, int clientId) { socket.emit(complete, clientId); });
[](const socket_message_FSDownloadMetadata &metadata, int clientId) { wsSocket.emit(metadata, clientId); },
[](const socket_message_FSDownloadData &data, int clientId) { wsSocket.emit(data, clientId); },
[](const socket_message_FSDownloadComplete &complete, int clientId) { wsSocket.emit(complete, clientId); },
[](const socket_message_FSUploadComplete &complete, int clientId) { wsSocket.emit(complete, clientId); });
socket.on<socket_message_ControllerData>(
wsSocket.on<socket_message_ControllerData>(
[&](const socket_message_ControllerData &data, int clientId) { motionService.handleInput(data); });
socket.on<socket_message_ModeData>([&](const socket_message_ModeData &data, int clientId) {
wsSocket.on<socket_message_ModeData>([&](const socket_message_ModeData &data, int clientId) {
servoController.setMode(SERVO_CONTROL_STATE::ANGLE);
motionService.handleMode(data);
motionService.isActive() ? servoController.activate() : servoController.deactivate();
});
socket.on<socket_message_WalkGaitData>(
wsSocket.on<socket_message_WalkGaitData>(
[&](const socket_message_WalkGaitData &data, int clientId) { motionService.handleWalkGait(data); });
socket.on<socket_message_AnglesData>(
wsSocket.on<socket_message_AnglesData>(
[&](const socket_message_AnglesData &data, int clientId) { motionService.handleAngles(data); });
socket.on<socket_message_ServoPWMData>([&](const socket_message_ServoPWMData &data, int clientId) {
wsSocket.on<socket_message_ServoPWMData>([&](const socket_message_ServoPWMData &data, int clientId) {
servoController.setServoPWM(data.servo_id, data.servo_pwm);
});
socket.on<socket_message_ServoStateData>([&](const socket_message_ServoStateData &data, int clientId) {
wsSocket.on<socket_message_ServoStateData>([&](const socket_message_ServoStateData &data, int clientId) {
data.active ? servoController.activate() : servoController.deactivate();
});
socket.on<socket_message_FSUploadData>(
[&](const socket_message_FSUploadData &data, int clientId) { FileSystemWS::fsHandler.handleUploadData(data); });
wsSocket.on<socket_message_FSUploadData>(
[&](const socket_message_FSUploadData &data, int clientId) { TIME_IT(FileSystemWS::fsHandler.handleUploadData(data), handle_upload) });
using CorrelationHandler =
std::function<void(const socket_message_CorrelationRequest &, socket_message_CorrelationResponse &, int)>;
@@ -225,7 +230,7 @@ void setupEventSocket() {
}},
};
socket.on<socket_message_CorrelationRequest>([&](const socket_message_CorrelationRequest &data, int clientId) {
wsSocket.on<socket_message_CorrelationRequest>([&](const socket_message_CorrelationRequest &data, int clientId) {
auto res = new socket_message_CorrelationResponse();
*res = socket_message_CorrelationResponse_init_default;
res->correlation_id = data.correlation_id;
@@ -235,7 +240,7 @@ void setupEventSocket() {
if (it != correlationHandlers.end()) {
it->second(data, *res, clientId);
if (res->status_code != 0) {
socket.emit(*res, clientId);
wsSocket.emit(*res, clientId);
}
} else {
printf("WARNING: no handler for correlation request: %d\n", data.which_request);
@@ -248,15 +253,18 @@ void setupEventSocket() {
void IRAM_ATTR SpotControlLoopEntry(void *) {
ESP_LOGI("main", "Control task starting");
TickType_t xLastWakeTime = xTaskGetTickCount();
const TickType_t xFrequency = 5 / portTICK_PERIOD_MS;
const TickType_t xFrequency = pdMS_TO_TICKS(10);
peripherals.begin();
servoController.begin();
motionService.begin();
#if FT_ENABLED(USE_WS2812)
ledService.begin();
#endif
peripherals.calibrateIMU();
for (;;) {
WARN_IF_SLOW(SpotControlLoopEntry, 5);
WARN_IF_SLOW(SpotControlLoopEntry, 10);
peripherals.update();
motionService.update(&peripherals);
servoController.setAngles(motionService.getAngles());
@@ -271,9 +279,11 @@ void IRAM_ATTR SpotControlLoopEntry(void *) {
void IRAM_ATTR serviceLoopEntry(void *) {
ESP_LOGI("main", "Service task starting");
WiFi.init();
wifiService.begin();
MDNS.begin(APP_NAME);
MDNS.setInstanceName(APP_NAME);
mdns_init();
mdns_hostname_set(APP_NAME);
mdns_instance_name_set(APP_NAME);
apService.begin();
#if FT_ENABLED(USE_CAMERA)
@@ -281,8 +291,6 @@ void IRAM_ATTR serviceLoopEntry(void *) {
#endif
setupServer();
socket.begin();
setupEventSocket();
ESP_LOGI("main", "Service task started");
@@ -291,23 +299,23 @@ void IRAM_ATTR serviceLoopEntry(void *) {
apService.loop();
EXECUTE_EVERY_N_MS(2000, {
if (socket.hasSubscribers(socket_message_Message_analytics_tag)) {
if (wsSocket.hasSubscribers(socket_message_Message_analytics_tag)) {
socket_message_AnalyticsData analytics = socket_message_AnalyticsData_init_zero;
system_service::getAnalytics(analytics);
socket.emit(analytics);
wsSocket.emit(analytics);
}
});
EXECUTE_EVERY_N_MS(100, {
if (socket.hasSubscribers(socket_message_Message_imu_tag)) {
if (wsSocket.hasSubscribers(socket_message_Message_imu_tag)) {
socket_message_IMUData imu = socket_message_IMUData_init_zero;
peripherals.getIMUProto(imu);
socket.emit(imu);
wsSocket.emit(imu);
}
if (socket.hasSubscribers(socket_message_Message_rssi_tag)) {
if (wsSocket.hasSubscribers(socket_message_Message_rssi_tag)) {
socket_message_RSSIData rssi = {.rssi = WiFi.RSSI()};
socket.emit(rssi);
wsSocket.emit(rssi);
}
});
@@ -317,18 +325,23 @@ void IRAM_ATTR serviceLoopEntry(void *) {
}
}
void setup() {
ESP_FS.begin();
extern "C" void app_main(void) {
esp_err_t ret = nvs_flash_init();
if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND) {
ESP_ERROR_CHECK(nvs_flash_erase());
ret = nvs_flash_init();
}
ESP_ERROR_CHECK(ret);
FileSystem::init();
ESP_LOGI("main", "Booting robot");
feature_service::printFeatureConfiguration();
xTaskCreate(serviceLoopEntry, "Service task", 4096, nullptr, 2, nullptr);
xTaskCreate(serviceLoopEntry, "Service task", 8192, nullptr, 2, nullptr);
xTaskCreatePinnedToCore(SpotControlLoopEntry, "Control task", 4096, nullptr, 5, nullptr, 1);
xTaskCreatePinnedToCore(SpotControlLoopEntry, "Control task", 8192, nullptr, 5, nullptr, 1);
ESP_LOGI("main", "Finished booting");
}
void loop() { vTaskDelete(nullptr); }
+67 -26
View File
@@ -1,5 +1,6 @@
#include <mdns_service.h>
#include <communication/webserver.h>
#include <esp_netif.h>
static const char *TAG = "MDNSService";
@@ -7,9 +8,8 @@ MDNSService::MDNSService()
: protoEndpoint(MDNSSettings_read, MDNSSettings_update, this,
API_REQUEST_EXTRACTOR(mdns_settings, api_MDNSSettings),
API_RESPONSE_ASSIGNER(mdns_settings, api_MDNSSettings)),
_persistence(MDNSSettings_read, MDNSSettings_update, this,
MDNS_SETTINGS_FILE, api_MDNSSettings_fields, api_MDNSSettings_size,
MDNSSettings_defaults()) {
_persistence(MDNSSettings_read, MDNSSettings_update, this, MDNS_SETTINGS_FILE, api_MDNSSettings_fields,
api_MDNSSettings_size, MDNSSettings_defaults()) {
addUpdateHandler([&](const std::string &originId) { reconfigureMDNS(); }, false);
}
@@ -34,33 +34,50 @@ void MDNSService::reconfigureMDNS() {
void MDNSService::startMDNS() {
ESP_LOGV(TAG, "Starting MDNS with hostname: %s", state().hostname);
if (MDNS.begin(state().hostname)) {
_started = true;
MDNS.setInstanceName(state().instance);
addServices();
ESP_LOGI(TAG, "MDNS started successfully with hostname: %s", state().hostname);
} else {
esp_err_t err = mdns_init();
if (err != ESP_OK) {
ESP_LOGE(TAG, "Failed to initialize MDNS: %s", esp_err_to_name(err));
_started = false;
ESP_LOGE(TAG, "Failed to start MDNS");
return;
}
err = mdns_hostname_set(state().hostname);
if (err != ESP_OK) {
ESP_LOGE(TAG, "Failed to set MDNS hostname: %s", esp_err_to_name(err));
mdns_free();
_started = false;
return;
}
err = mdns_instance_name_set(state().instance);
if (err != ESP_OK) {
ESP_LOGW(TAG, "Failed to set MDNS instance name: %s", esp_err_to_name(err));
}
_started = true;
addServices();
ESP_LOGI(TAG, "MDNS started successfully with hostname: %s", state().hostname);
}
void MDNSService::stopMDNS() {
ESP_LOGV(TAG, "Stopping MDNS");
MDNS.end();
mdns_free();
_started = false;
}
void MDNSService::addServices() {
for (size_t i = 0; i < state().services_count; i++) {
const auto &service = state().services[i];
MDNS.addService(service.service, service.protocol, service.port);
esp_err_t err = mdns_service_add(nullptr, service.service, service.protocol, service.port, nullptr, 0);
if (err != ESP_OK) {
ESP_LOGW(TAG, "Failed to add service %s: %s", service.service, esp_err_to_name(err));
continue;
}
for (size_t j = 0; j < service.txt_records_count; j++) {
const auto &txt = service.txt_records[j];
MDNS.addServiceTxt(service.service, service.protocol, txt.key, txt.value);
mdns_service_txt_item_set(service.service, service.protocol, txt.key, txt.value);
}
}
@@ -68,7 +85,7 @@ void MDNSService::addServices() {
const auto &txt = state().global_txt_records[i];
for (size_t j = 0; j < state().services_count; j++) {
const auto &service = state().services[j];
MDNS.addServiceTxt(service.service, service.protocol, txt.key, txt.value);
mdns_service_txt_item_set(service.service, service.protocol, txt.key, txt.value);
}
}
}
@@ -103,22 +120,46 @@ esp_err_t MDNSService::queryServices(httpd_req_t *request, api_Request *protoReq
const api_MDNSQueryRequest &queryReq = protoReq->payload.mdns_query_request;
ESP_LOGI(TAG, "Querying for service: %s, protocol: %s", queryReq.service, queryReq.protocol);
int n = MDNS.queryService(queryReq.service, queryReq.protocol);
ESP_LOGI(TAG, "Found %d services", n);
mdns_result_t *results = nullptr;
esp_err_t err = mdns_query_ptr(queryReq.service, queryReq.protocol, 3000, 20, &results);
api_Response response = api_Response_init_zero;
response.which_payload = api_Response_mdns_query_response_tag;
api_MDNSQueryResponse &queryResp = response.payload.mdns_query_response;
// Limit to max_count from options file (16)
size_t count = (n > 16) ? 16 : static_cast<size_t>(n);
queryResp.services_count = count;
for (size_t i = 0; i < count; i++) {
strncpy(queryResp.services[i].name, MDNS.hostname(i).c_str(), sizeof(queryResp.services[i].name) - 1);
strncpy(queryResp.services[i].ip, MDNS.IP(i).toString().c_str(), sizeof(queryResp.services[i].ip) - 1);
queryResp.services[i].port = MDNS.port(i);
if (err != ESP_OK) {
ESP_LOGW(TAG, "MDNS query failed: %s", esp_err_to_name(err));
queryResp.services_count = 0;
return WebServer::send(request, 200, response, api_Response_fields);
}
int count = 0;
mdns_result_t *r = results;
while (r && count < 16) {
count++;
r = r->next;
}
ESP_LOGI(TAG, "Found %d services", count);
queryResp.services_count = count;
r = results;
size_t i = 0;
while (r && i < 16) {
if (r->hostname) {
strncpy(queryResp.services[i].name, r->hostname, sizeof(queryResp.services[i].name) - 1);
}
if (r->addr) {
char ip_str[16];
esp_ip4addr_ntoa(&r->addr->addr.u_addr.ip4, ip_str, sizeof(ip_str));
strncpy(queryResp.services[i].ip, ip_str, sizeof(queryResp.services[i].ip) - 1);
}
queryResp.services[i].port = r->port;
r = r->next;
i++;
}
mdns_query_results_free(results);
return WebServer::send(request, 200, response, api_Response_fields);
}
+3 -2
View File
@@ -1,5 +1,6 @@
#include <peripherals/camera_service.h>
#include <communication/webserver.h>
#include <esp_heap_caps.h>
namespace Camera {
@@ -66,7 +67,7 @@ esp_err_t CameraService::begin() {
camera_config.xclk_freq_hz = 20000000;
camera_config.pixel_format = PIXFORMAT_JPEG;
if (psramFound()) {
if (heap_caps_get_total_size(MALLOC_CAP_SPIRAM) > 0) {
camera_config.frame_size = FRAMESIZE_SVGA;
camera_config.jpeg_quality = 10;
camera_config.fb_location = CAMERA_FB_IN_PSRAM;
@@ -78,7 +79,7 @@ esp_err_t CameraService::begin() {
camera_config.fb_count = 1;
}
log_i("Initializing camera");
ESP_LOGI(TAG, "Initializing camera");
esp_err_t err = esp_camera_init(&camera_config);
if (err == ESP_OK)
ESP_LOGI(TAG, "Camera probe successful");
+135 -19
View File
@@ -1,5 +1,43 @@
#include "system_service.h"
#include <communication/webserver.h>
#include <dirent.h>
#include <esp_chip_info.h>
#include <esp_flash.h>
#include <esp_ota_ops.h>
#include <esp_system.h>
#include <esp_sleep.h>
#include <soc/soc.h>
#if CONFIG_IDF_TARGET_ESP32S2 || CONFIG_IDF_TARGET_ESP32S3 || CONFIG_IDF_TARGET_ESP32C3 || CONFIG_IDF_TARGET_ESP32C6
#include <driver/temperature_sensor.h>
static float temperatureRead() {
static temperature_sensor_handle_t temp_sensor = nullptr;
static bool initialized = false;
if (!initialized) {
temperature_sensor_config_t temp_sensor_config = {
.range_min = -10,
.range_max = 80,
.clk_src = TEMPERATURE_SENSOR_CLK_SRC_DEFAULT,
};
if (temperature_sensor_install(&temp_sensor_config, &temp_sensor) == ESP_OK) {
temperature_sensor_enable(temp_sensor);
initialized = true;
}
}
if (initialized && temp_sensor) {
float temp = 0;
if (temperature_sensor_get_celsius(temp_sensor, &temp) == ESP_OK) {
return temp;
}
}
return 0.0f;
}
#else
static inline float temperatureRead() { return 0.0f; }
#endif
namespace system_service {
@@ -22,12 +60,15 @@ esp_err_t handleSleep(httpd_req_t *request) {
void reset() {
ESP_LOGI(TAG, "Resetting device");
File root = ESP_FS.open(FS_CONFIG_DIRECTORY);
File file;
while (file = root.openNextFile()) {
std::string path = file.path();
file.close();
ESP_FS.remove(path.c_str());
DIR *dir = opendir(FS_CONFIG_DIRECTORY);
if (dir) {
struct dirent *entry;
while ((entry = readdir(dir)) != nullptr) {
if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0) continue;
std::string path = std::string(FS_CONFIG_DIRECTORY) + "/" + entry->d_name;
remove(path.c_str());
}
closedir(dir);
}
restart();
}
@@ -37,11 +78,11 @@ void restart() {
[](void *pvParameters) {
for (;;) {
vTaskDelay(250 / portTICK_PERIOD_MS);
MDNS.end();
mdns_free();
vTaskDelay(100 / portTICK_PERIOD_MS);
WiFi.disconnect(true);
vTaskDelay(500 / portTICK_PERIOD_MS);
ESP.restart();
esp_restart();
}
},
"Restart task", 4096, nullptr, 10, nullptr);
@@ -52,7 +93,7 @@ void sleep() {
[](void *pvParameters) {
for (;;) {
vTaskDelay(250 / portTICK_PERIOD_MS);
MDNS.end();
mdns_free();
vTaskDelay(100 / portTICK_PERIOD_MS);
WiFi.disconnect(true);
vTaskDelay(500 / portTICK_PERIOD_MS);
@@ -72,22 +113,97 @@ void sleep() {
ESP_LOGI(TAG, "Setting device to sleep");
}
static const char *getChipModel() {
esp_chip_info_t chip_info;
esp_chip_info(&chip_info);
switch (chip_info.model) {
case CHIP_ESP32: return "ESP32";
case CHIP_ESP32S2: return "ESP32-S2";
case CHIP_ESP32S3: return "ESP32-S3";
case CHIP_ESP32C3: return "ESP32-C3";
case CHIP_ESP32C2: return "ESP32-C2";
case CHIP_ESP32C6: return "ESP32-C6";
case CHIP_ESP32H2: return "ESP32-H2";
default: return "Unknown";
}
}
static uint32_t getCpuFreqMHz() { return CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ; }
static uint8_t getChipCores() {
esp_chip_info_t chip_info;
esp_chip_info(&chip_info);
return chip_info.cores;
}
static uint8_t getChipRevision() {
esp_chip_info_t chip_info;
esp_chip_info(&chip_info);
return chip_info.revision;
}
static uint32_t getFlashChipSize() {
uint32_t size = 0;
esp_flash_get_size(NULL, &size);
return size;
}
static uint32_t getFlashChipSpeed() {
#if defined(CONFIG_ESPTOOLPY_FLASHFREQ_120M)
return 120000000;
#elif defined(CONFIG_ESPTOOLPY_FLASHFREQ_80M)
return 80000000;
#elif defined(CONFIG_ESPTOOLPY_FLASHFREQ_64M)
return 64000000;
#elif defined(CONFIG_ESPTOOLPY_FLASHFREQ_60M)
return 60000000;
#elif defined(CONFIG_ESPTOOLPY_FLASHFREQ_48M)
return 48000000;
#elif defined(CONFIG_ESPTOOLPY_FLASHFREQ_40M)
return 40000000;
#elif defined(CONFIG_ESPTOOLPY_FLASHFREQ_30M)
return 30000000;
#elif defined(CONFIG_ESPTOOLPY_FLASHFREQ_26M)
return 26000000;
#elif defined(CONFIG_ESPTOOLPY_FLASHFREQ_20M)
return 20000000;
#else
return 80000000;
#endif
}
static uint32_t getSketchSize() {
const esp_partition_t *running = esp_ota_get_running_partition();
if (running) {
return running->size;
}
return 0;
}
static uint32_t getFreeSketchSpace() {
const esp_partition_t *next = esp_ota_get_next_update_partition(NULL);
if (next) {
return next->size;
}
return 0;
}
void getStaticSystemInformation(socket_message_StaticSystemInformation &info) {
size_t fs_total = 0, fs_used = 0;
esp_littlefs_info("spiffs", &fs_total, &fs_used);
info.esp_platform = (char *)ESP_PLATFORM_NAME;
info.firmware_version = APP_VERSION;
info.cpu_freq_mhz = ESP.getCpuFreqMHz();
info.cpu_type = (char *)ESP.getChipModel();
info.cpu_rev = ESP.getChipRevision();
info.cpu_cores = ESP.getChipCores();
info.sketch_size = ESP.getSketchSize();
info.free_sketch_space = ESP.getFreeSketchSpace();
info.sdk_version = (char *)ESP.getSdkVersion();
info.arduino_version = ARDUINO_VERSION;
info.flash_chip_size = ESP.getFlashChipSize();
info.flash_chip_speed = ESP.getFlashChipSpeed();
info.cpu_freq_mhz = getCpuFreqMHz();
info.cpu_type = (char *)getChipModel();
info.cpu_rev = getChipRevision();
info.cpu_cores = getChipCores();
info.sketch_size = getSketchSize();
info.free_sketch_space = getFreeSketchSpace();
info.sdk_version = (char *)esp_get_idf_version();
info.arduino_version = "";
info.flash_chip_size = getFlashChipSize();
info.flash_chip_speed = getFlashChipSpeed();
info.cpu_reset_reason = (char *)resetReason(esp_reset_reason());
}
+571
View File
@@ -0,0 +1,571 @@
#include <wifi/wifi_idf.h>
static const char* TAG = "WiFiIDF";
WiFiClass WiFi;
WiFiClass::WiFiClass()
: _sta_netif(nullptr),
_ap_netif(nullptr),
_initialized(false),
_autoReconnect(false),
_persistent(false),
_status(WL_DISCONNECTED),
_mode(WIFI_MODE_NULL),
_scanResult(nullptr),
_scanCount(0),
_scanStatus(-2),
_useStaticIp(false) {}
WiFiClass::~WiFiClass() {
if (_scanResult) {
free(_scanResult);
_scanResult = nullptr;
}
}
bool WiFiClass::init() {
if (_initialized) return true;
esp_err_t ret = nvs_flash_init();
if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND) {
nvs_flash_erase();
ret = nvs_flash_init();
}
ret = esp_netif_init();
if (ret != ESP_OK && ret != ESP_ERR_INVALID_STATE) {
ESP_LOGE(TAG, "esp_netif_init failed: %s", esp_err_to_name(ret));
}
ret = esp_event_loop_create_default();
if (ret != ESP_OK && ret != ESP_ERR_INVALID_STATE) {
ESP_LOGE(TAG, "Event loop create failed: %s", esp_err_to_name(ret));
}
_sta_netif = esp_netif_get_handle_from_ifkey("WIFI_STA_DEF");
if (!_sta_netif) {
_sta_netif = esp_netif_create_default_wifi_sta();
}
_ap_netif = esp_netif_get_handle_from_ifkey("WIFI_AP_DEF");
if (!_ap_netif) {
_ap_netif = esp_netif_create_default_wifi_ap();
}
wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
ret = esp_wifi_init(&cfg);
if (ret != ESP_OK && ret != ESP_ERR_INVALID_STATE) {
ESP_LOGE(TAG, "esp_wifi_init failed: %s", esp_err_to_name(ret));
return false;
}
esp_event_handler_instance_register(WIFI_EVENT, ESP_EVENT_ANY_ID, &WiFiClass::eventHandler, this, nullptr);
esp_event_handler_instance_register(IP_EVENT, ESP_EVENT_ANY_ID, &WiFiClass::eventHandler, this, nullptr);
esp_wifi_set_storage(_persistent ? WIFI_STORAGE_FLASH : WIFI_STORAGE_RAM);
_initialized = true;
return true;
}
void WiFiClass::eventHandler(void* arg, esp_event_base_t event_base, int32_t event_id, void* event_data) {
WiFiClass* self = static_cast<WiFiClass*>(arg);
if (event_base == WIFI_EVENT) {
switch (event_id) {
case WIFI_EVENT_STA_START: ESP_LOGI(TAG, "STA Started"); break;
case WIFI_EVENT_STA_STOP:
ESP_LOGI(TAG, "STA Stopped");
self->_status = WL_DISCONNECTED;
break;
case WIFI_EVENT_STA_CONNECTED: ESP_LOGI(TAG, "STA Connected"); break;
case WIFI_EVENT_STA_DISCONNECTED: {
wifi_event_sta_disconnected_t* event = (wifi_event_sta_disconnected_t*)event_data;
ESP_LOGI(TAG, "STA Disconnected, reason: %d", event->reason);
self->_status = WL_DISCONNECTED;
if (self->_autoReconnect) {
esp_wifi_connect();
}
break;
}
case WIFI_EVENT_AP_START: ESP_LOGI(TAG, "AP Started"); break;
case WIFI_EVENT_AP_STOP: ESP_LOGI(TAG, "AP Stopped"); break;
case WIFI_EVENT_AP_STACONNECTED: {
wifi_event_ap_staconnected_t* event = (wifi_event_ap_staconnected_t*)event_data;
ESP_LOGI(TAG, "Station connected, AID=%d", event->aid);
break;
}
case WIFI_EVENT_AP_STADISCONNECTED: {
wifi_event_ap_stadisconnected_t* event = (wifi_event_ap_stadisconnected_t*)event_data;
ESP_LOGI(TAG, "Station disconnected, AID=%d", event->aid);
break;
}
case WIFI_EVENT_SCAN_DONE: {
wifi_event_sta_scan_done_t* event = (wifi_event_sta_scan_done_t*)event_data;
if (event->status == 0) {
self->_scanCount = event->number;
if (self->_scanResult) {
free(self->_scanResult);
}
self->_scanResult = (wifi_ap_record_t*)malloc(sizeof(wifi_ap_record_t) * self->_scanCount);
if (self->_scanResult) {
esp_wifi_scan_get_ap_records(&self->_scanCount, self->_scanResult);
}
self->_scanStatus = self->_scanCount;
} else {
self->_scanStatus = -2;
}
break;
}
}
self->dispatchEvent(event_id, event_data);
} else if (event_base == IP_EVENT) {
if (event_id == IP_EVENT_STA_GOT_IP) {
ip_event_got_ip_t* event = (ip_event_got_ip_t*)event_data;
ESP_LOGI(TAG, "Got IP: " IPSTR, IP2STR(&event->ip_info.ip));
self->_status = WL_CONNECTED;
self->dispatchEvent(event_id + 1000, event_data);
} else if (event_id == IP_EVENT_STA_LOST_IP) {
ESP_LOGI(TAG, "Lost IP");
self->_status = WL_DISCONNECTED;
self->dispatchEvent(event_id + 1000, event_data);
}
}
}
void WiFiClass::dispatchEvent(int32_t event_id, void* event_data) {
for (auto& handler : _eventHandlers) {
if (handler.event_id == event_id || handler.event_id == -1) {
handler.callback(event_id, event_data);
}
}
}
void WiFiClass::onEvent(WiFiEventCb callback, int32_t event_id) { _eventHandlers.push_back({event_id, callback}); }
bool WiFiClass::mode(wifi_mode_t m) {
if (!_initialized) init();
if (_mode == m) return true;
esp_err_t err;
if (_mode == WIFI_MODE_NULL) {
err = esp_wifi_set_mode(m);
if (err == ESP_OK) {
err = esp_wifi_start();
}
} else if (m == WIFI_MODE_NULL) {
err = esp_wifi_stop();
} else {
err = esp_wifi_set_mode(m);
}
if (err == ESP_OK) {
_mode = m;
return true;
}
ESP_LOGE(TAG, "Failed to set mode: %s", esp_err_to_name(err));
return false;
}
wifi_mode_t WiFiClass::getMode() {
if (!_initialized) return WIFI_MODE_NULL;
wifi_mode_t m;
if (esp_wifi_get_mode(&m) == ESP_OK) {
return m;
}
return WIFI_MODE_NULL;
}
bool WiFiClass::begin(const char* ssid, const char* password, int32_t channel, const uint8_t* bssid) {
if (!_initialized) init();
wifi_mode_t currentMode = getMode();
if (currentMode == WIFI_MODE_NULL || currentMode == WIFI_MODE_AP) {
mode(currentMode == WIFI_MODE_AP ? WIFI_MODE_APSTA : WIFI_MODE_STA);
vTaskDelay(500 / portTICK_PERIOD_MS);
}
wifi_config_t wifi_config = {};
strncpy((char*)wifi_config.sta.ssid, ssid, sizeof(wifi_config.sta.ssid) - 1);
if (password) {
strncpy((char*)wifi_config.sta.password, password, sizeof(wifi_config.sta.password) - 1);
}
if (channel > 0) {
wifi_config.sta.channel = channel;
}
if (bssid) {
memcpy(wifi_config.sta.bssid, bssid, 6);
wifi_config.sta.bssid_set = true;
}
wifi_config.sta.scan_method = WIFI_ALL_CHANNEL_SCAN;
wifi_config.sta.sort_method = WIFI_CONNECT_AP_BY_SIGNAL;
wifi_config.sta.threshold.authmode = password && strlen(password) > 0 ? WIFI_AUTH_WPA2_PSK : WIFI_AUTH_OPEN;
esp_err_t err = esp_wifi_set_config(WIFI_IF_STA, &wifi_config);
if (err != ESP_OK) {
ESP_LOGE(TAG, "Failed to set STA config: %s", esp_err_to_name(err));
return false;
}
if (_useStaticIp) {
esp_netif_dhcpc_stop(_sta_netif);
esp_netif_ip_info_t ip_info = {};
ip_info.ip = _sta_static_ip;
ip_info.gw = _sta_static_gw;
ip_info.netmask = _sta_static_sn;
esp_netif_set_ip_info(_sta_netif, &ip_info);
if (static_cast<uint32_t>(_sta_static_dns1) != 0) {
esp_netif_dns_info_t dns;
dns.ip.u_addr.ip4 = _sta_static_dns1;
dns.ip.type = ESP_IPADDR_TYPE_V4;
esp_netif_set_dns_info(_sta_netif, ESP_NETIF_DNS_MAIN, &dns);
}
if (static_cast<uint32_t>(_sta_static_dns2) != 0) {
esp_netif_dns_info_t dns;
dns.ip.u_addr.ip4 = _sta_static_dns2;
dns.ip.type = ESP_IPADDR_TYPE_V4;
esp_netif_set_dns_info(_sta_netif, ESP_NETIF_DNS_BACKUP, &dns);
}
} else {
esp_netif_dhcpc_start(_sta_netif);
}
err = esp_wifi_connect();
if (err != ESP_OK) {
ESP_LOGE(TAG, "Failed to connect: %s", esp_err_to_name(err));
return false;
}
return true;
}
bool WiFiClass::disconnect(bool wifiOff) {
esp_err_t err = esp_wifi_disconnect();
if (wifiOff) {
wifi_mode_t m = getMode();
if (m == WIFI_MODE_APSTA) {
mode(WIFI_MODE_AP);
} else if (m == WIFI_MODE_STA) {
mode(WIFI_MODE_NULL);
}
}
_status = WL_DISCONNECTED;
return err == ESP_OK;
}
bool WiFiClass::reconnect() { return esp_wifi_connect() == ESP_OK; }
bool WiFiClass::config(IPAddress local_ip, IPAddress gateway, IPAddress subnet, IPAddress dns1, IPAddress dns2) {
_sta_static_ip = local_ip;
_sta_static_gw = gateway;
_sta_static_sn = subnet;
_sta_static_dns1 = dns1;
_sta_static_dns2 = dns2;
_useStaticIp = (static_cast<uint32_t>(local_ip) != 0);
return true;
}
bool WiFiClass::setHostname(const char* hostname) {
_hostname = hostname;
if (_sta_netif) {
return esp_netif_set_hostname(_sta_netif, hostname) == ESP_OK;
}
return true;
}
const char* WiFiClass::getHostname() {
const char* hostname = nullptr;
if (_sta_netif) {
esp_netif_get_hostname(_sta_netif, &hostname);
}
return hostname ? hostname : _hostname.c_str();
}
wl_status_t WiFiClass::status() { return _status; }
bool WiFiClass::isConnected() { return _status == WL_CONNECTED; }
IPAddress WiFiClass::localIP() {
esp_netif_ip_info_t ip_info;
if (_sta_netif && esp_netif_get_ip_info(_sta_netif, &ip_info) == ESP_OK) {
return IPAddress(ip_info.ip);
}
return IPAddress();
}
IPAddress WiFiClass::subnetMask() {
esp_netif_ip_info_t ip_info;
if (_sta_netif && esp_netif_get_ip_info(_sta_netif, &ip_info) == ESP_OK) {
return IPAddress(ip_info.netmask);
}
return IPAddress();
}
IPAddress WiFiClass::gatewayIP() {
esp_netif_ip_info_t ip_info;
if (_sta_netif && esp_netif_get_ip_info(_sta_netif, &ip_info) == ESP_OK) {
return IPAddress(ip_info.gw);
}
return IPAddress();
}
IPAddress WiFiClass::dnsIP(uint8_t dns_no) {
esp_netif_dns_info_t dns;
esp_netif_dns_type_t type = dns_no == 0 ? ESP_NETIF_DNS_MAIN : ESP_NETIF_DNS_BACKUP;
if (_sta_netif && esp_netif_get_dns_info(_sta_netif, type, &dns) == ESP_OK) {
return IPAddress(dns.ip.u_addr.ip4);
}
return IPAddress();
}
std::string WiFiClass::macAddress() {
uint8_t mac[6];
char buf[18];
esp_wifi_get_mac(WIFI_IF_STA, mac);
snprintf(buf, sizeof(buf), "%02X:%02X:%02X:%02X:%02X:%02X", mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);
return std::string(buf);
}
std::string WiFiClass::SSID() {
wifi_ap_record_t info;
if (esp_wifi_sta_get_ap_info(&info) == ESP_OK) {
return std::string((char*)info.ssid);
}
return "";
}
std::string WiFiClass::BSSIDstr() {
wifi_ap_record_t info;
if (esp_wifi_sta_get_ap_info(&info) == ESP_OK) {
char buf[18];
snprintf(buf, sizeof(buf), "%02X:%02X:%02X:%02X:%02X:%02X", info.bssid[0], info.bssid[1], info.bssid[2],
info.bssid[3], info.bssid[4], info.bssid[5]);
return std::string(buf);
}
return "";
}
int32_t WiFiClass::RSSI() {
wifi_ap_record_t info;
if (esp_wifi_sta_get_ap_info(&info) == ESP_OK) {
return info.rssi;
}
return 0;
}
uint8_t WiFiClass::channel() {
wifi_ap_record_t info;
if (esp_wifi_sta_get_ap_info(&info) == ESP_OK) {
return info.primary;
}
return 0;
}
int16_t WiFiClass::scanNetworks(bool async) {
if (!_initialized) init();
wifi_mode_t currentMode = getMode();
if (currentMode == WIFI_MODE_NULL || currentMode == WIFI_MODE_AP) {
mode(currentMode == WIFI_MODE_AP ? WIFI_MODE_APSTA : WIFI_MODE_STA);
vTaskDelay(500 / portTICK_PERIOD_MS);
}
_scanStatus = -1;
wifi_scan_config_t scan_config = {};
scan_config.show_hidden = true;
esp_err_t err = esp_wifi_scan_start(&scan_config, !async);
if (err != ESP_OK) {
ESP_LOGE(TAG, "Scan start failed: %s", esp_err_to_name(err));
_scanStatus = -2;
return -2;
}
if (!async) {
if (_scanResult) {
free(_scanResult);
_scanResult = nullptr;
}
esp_wifi_scan_get_ap_num(&_scanCount);
_scanResult = (wifi_ap_record_t*)malloc(sizeof(wifi_ap_record_t) * _scanCount);
if (_scanResult) {
esp_wifi_scan_get_ap_records(&_scanCount, _scanResult);
}
_scanStatus = _scanCount;
return _scanCount;
}
return -1;
}
int16_t WiFiClass::scanComplete() { return _scanStatus; }
void WiFiClass::scanDelete() {
if (_scanResult) {
free(_scanResult);
_scanResult = nullptr;
}
_scanCount = 0;
_scanStatus = -2;
}
std::string WiFiClass::SSID(uint8_t i) {
if (i < _scanCount && _scanResult) {
return std::string((char*)_scanResult[i].ssid);
}
return "";
}
int32_t WiFiClass::RSSI(uint8_t i) {
if (i < _scanCount && _scanResult) {
return _scanResult[i].rssi;
}
return 0;
}
wifi_enc_type_t WiFiClass::encryptionType(uint8_t i) {
if (i < _scanCount && _scanResult) {
switch (_scanResult[i].authmode) {
case WIFI_AUTH_OPEN: return WIFI_AUTH_OPEN_IDF;
case WIFI_AUTH_WEP: return WIFI_AUTH_WEP_IDF;
case WIFI_AUTH_WPA_PSK: return WIFI_AUTH_WPA_PSK_IDF;
default: return WIFI_AUTH_WPA2_PSK_IDF;
}
}
return WIFI_AUTH_OPEN_IDF;
}
std::string WiFiClass::BSSIDstr(uint8_t i) {
if (i < _scanCount && _scanResult) {
char buf[18];
snprintf(buf, sizeof(buf), "%02X:%02X:%02X:%02X:%02X:%02X", _scanResult[i].bssid[0], _scanResult[i].bssid[1],
_scanResult[i].bssid[2], _scanResult[i].bssid[3], _scanResult[i].bssid[4], _scanResult[i].bssid[5]);
return std::string(buf);
}
return "";
}
int32_t WiFiClass::channel(uint8_t i) {
if (i < _scanCount && _scanResult) {
return _scanResult[i].primary;
}
return 0;
}
void WiFiClass::getNetworkInfo(uint8_t i, std::string& ssid, uint8_t& encType, int32_t& rssi, uint8_t*& bssid,
int32_t& ch) {
if (i < _scanCount && _scanResult) {
ssid = std::string((char*)_scanResult[i].ssid);
encType = static_cast<uint8_t>(encryptionType(i));
rssi = _scanResult[i].rssi;
bssid = _scanResult[i].bssid;
ch = _scanResult[i].primary;
}
}
bool WiFiClass::softAP(const char* ssid, const char* password, int channel, bool ssid_hidden, int max_connection) {
if (!_initialized) init();
wifi_mode_t currentMode = getMode();
if (currentMode == WIFI_MODE_NULL || currentMode == WIFI_MODE_STA) {
mode(currentMode == WIFI_MODE_STA ? WIFI_MODE_APSTA : WIFI_MODE_AP);
}
wifi_config_t wifi_config = {};
strncpy((char*)wifi_config.ap.ssid, ssid, sizeof(wifi_config.ap.ssid) - 1);
wifi_config.ap.ssid_len = strlen(ssid);
wifi_config.ap.channel = channel;
wifi_config.ap.ssid_hidden = ssid_hidden ? 1 : 0;
wifi_config.ap.max_connection = max_connection;
if (password && strlen(password) > 0) {
strncpy((char*)wifi_config.ap.password, password, sizeof(wifi_config.ap.password) - 1);
wifi_config.ap.authmode = WIFI_AUTH_WPA2_PSK;
} else {
wifi_config.ap.authmode = WIFI_AUTH_OPEN;
}
esp_err_t err = esp_wifi_set_config(WIFI_IF_AP, &wifi_config);
if (err != ESP_OK) {
ESP_LOGE(TAG, "Failed to set AP config: %s", esp_err_to_name(err));
return false;
}
return true;
}
bool WiFiClass::softAPConfig(IPAddress local_ip, IPAddress gateway, IPAddress subnet) {
if (!_ap_netif) return false;
esp_netif_dhcps_stop(_ap_netif);
esp_netif_ip_info_t ip_info = {};
ip_info.ip = local_ip;
ip_info.gw = gateway;
ip_info.netmask = subnet;
esp_err_t err = esp_netif_set_ip_info(_ap_netif, &ip_info);
if (err != ESP_OK) {
ESP_LOGE(TAG, "Failed to set AP IP info: %s", esp_err_to_name(err));
return false;
}
esp_netif_dhcps_start(_ap_netif);
return true;
}
bool WiFiClass::softAPdisconnect(bool wifiOff) {
wifi_mode_t m = getMode();
if (wifiOff) {
if (m == WIFI_MODE_APSTA) {
return mode(WIFI_MODE_STA);
} else if (m == WIFI_MODE_AP) {
return mode(WIFI_MODE_NULL);
}
}
return true;
}
IPAddress WiFiClass::softAPIP() {
esp_netif_ip_info_t ip_info;
if (_ap_netif && esp_netif_get_ip_info(_ap_netif, &ip_info) == ESP_OK) {
return IPAddress(ip_info.ip);
}
return IPAddress();
}
std::string WiFiClass::softAPmacAddress() {
uint8_t mac[6];
char buf[18];
esp_wifi_get_mac(WIFI_IF_AP, mac);
snprintf(buf, sizeof(buf), "%02X:%02X:%02X:%02X:%02X:%02X", mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);
return std::string(buf);
}
uint8_t WiFiClass::softAPgetStationNum() {
wifi_sta_list_t sta_list;
if (esp_wifi_ap_get_sta_list(&sta_list) == ESP_OK) {
return sta_list.num;
}
return 0;
}
bool WiFiClass::setAutoReconnect(bool autoReconnect) {
_autoReconnect = autoReconnect;
return true;
}
bool WiFiClass::persistent(bool persistent) {
_persistent = persistent;
if (_initialized) {
return esp_wifi_set_storage(persistent ? WIFI_STORAGE_FLASH : WIFI_STORAGE_RAM) == ESP_OK;
}
return true;
}
bool WiFiClass::setTxPower(int8_t power) { return esp_wifi_set_max_tx_power(power * 4) == ESP_OK; }
+62 -93
View File
@@ -1,45 +1,57 @@
#include <wifi_service.h>
#include <communication/webserver.h>
static const char *TAG = "WiFiService";
WiFiService::WiFiService()
: _persistence(WiFiSettings_read, WiFiSettings_update, this, WIFI_SETTINGS_FILE,
api_WifiSettings_fields, api_WifiSettings_size, WiFiSettings_defaults()),
protoEndpoint(WiFiSettings_read, WiFiSettings_update, this,
: protoEndpoint(WiFiSettings_read, WiFiSettings_update, this,
API_REQUEST_EXTRACTOR(wifi_settings, api_WifiSettings),
API_RESPONSE_ASSIGNER(wifi_settings, api_WifiSettings)) {
API_RESPONSE_ASSIGNER(wifi_settings, api_WifiSettings)),
_persistence(WiFiSettings_read, WiFiSettings_update, this, WIFI_SETTINGS_FILE, api_WifiSettings_fields,
api_WifiSettings_size, WiFiSettings_defaults()),
_lastConnectionAttempt(0),
_stopping(false) {
addUpdateHandler([&](const std::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);
WiFi.onEvent([this](int32_t event, void *data) { this->onStationModeDisconnected(event, data); },
WIFI_EVENT_STA_DISCONNECTED);
WiFi.onEvent([this](int32_t event, void *data) { this->onStationModeStop(event, data); }, WIFI_EVENT_STA_STOP);
WiFi.onEvent(onStationModeGotIP, IP_EVENT_STA_GOT_IP_IDF);
_persistence.readFromFS();
reconfigureWiFiConnection();
_lastConnectionAttempt = 0;
if (state().wifi_networks_count == 1) {
configureNetwork(state().wifi_networks[0]);
vTaskDelay(500 / portTICK_PERIOD_MS);
if (state().wifi_networks_count >= 1) {
WiFi.mode(WIFI_MODE_STA);
vTaskDelay(100 / portTICK_PERIOD_MS);
uint32_t idx = state().selected_network;
if (idx >= state().wifi_networks_count) idx = 0;
configureNetwork(state().wifi_networks[idx]);
}
}
void WiFiService::reconfigureWiFiConnection() {
_lastConnectionAttempt = 0;
if (WiFi.disconnect(true)) _stopping = true;
}
void WiFiService::selectNetwork(uint32_t index) {
if (index >= state().wifi_networks_count) return;
updateWithoutPropagation([&](WiFiSettings &settings) {
settings.selected_network = index;
return StateUpdateResult::CHANGED;
});
_persistence.writeToFS();
reconfigureWiFiConnection();
}
void WiFiService::loop() { EXECUTE_EVERY_N_MS(reconnectDelay, manageSTA()); }
esp_err_t WiFiService::handleScan(httpd_req_t *request) {
@@ -47,7 +59,6 @@ esp_err_t WiFiService::handleScan(httpd_req_t *request) {
WiFi.scanDelete();
WiFi.scanNetworks(true);
}
// Return 202 with empty_message payload (no pointer fields to encode)
api_Response response = api_Response_init_zero;
response.status_code = 202;
response.which_payload = api_Response_empty_message_tag;
@@ -57,7 +68,6 @@ esp_err_t WiFiService::handleScan(httpd_req_t *request) {
esp_err_t WiFiService::getNetworks(httpd_req_t *request) {
int numNetworks = WiFi.scanComplete();
if (numNetworks == -1) {
// Scan in progress - return 202 with empty_message payload
api_Response response = api_Response_init_zero;
response.status_code = 202;
response.which_payload = api_Response_empty_message_tag;
@@ -66,10 +76,8 @@ esp_err_t WiFiService::getNetworks(httpd_req_t *request) {
return handleScan(request);
}
// Limit to 20 networks max
size_t count = (numNetworks > 20) ? 20 : static_cast<size_t>(numNetworks);
// Allocate networks array on stack (pointer type in proto)
api_WifiNetworkScan networks[20] = {};
for (size_t i = 0; i < count; i++) {
@@ -90,11 +98,13 @@ esp_err_t WiFiService::getNetworks(httpd_req_t *request) {
}
void WiFiService::setupMDNS(const char *hostname) {
MDNS.begin(state().hostname);
MDNS.setInstanceName(hostname);
MDNS.addService("http", "tcp", 80);
MDNS.addService("ws", "tcp", 80);
MDNS.addServiceTxt("http", "tcp", "Firmware Version", APP_VERSION);
mdns_init();
mdns_hostname_set(state().hostname);
mdns_instance_name_set(hostname);
mdns_service_add(nullptr, "_http", "_tcp", 80, nullptr, 0);
mdns_service_add(nullptr, "_ws", "_tcp", 80, nullptr, 0);
mdns_txt_item_t txtData = {"Firmware Version", APP_VERSION};
mdns_service_txt_set("_http", "_tcp", &txtData, 1);
}
esp_err_t WiFiService::getNetworkStatus(httpd_req_t *request) {
@@ -114,7 +124,6 @@ esp_err_t WiFiService::getNetworkStatus(httpd_req_t *request) {
wifiStatus.channel = WiFi.channel();
wifiStatus.subnet_mask = static_cast<uint32_t>(WiFi.subnetMask());
wifiStatus.gateway_ip = static_cast<uint32_t>(WiFi.gatewayIP());
IPAddress dnsIP1 = WiFi.dnsIP(0);
IPAddress dnsIP2 = WiFi.dnsIP(1);
if (dnsIP1 != IPAddress(0, 0, 0, 0)) {
@@ -130,98 +139,58 @@ esp_err_t WiFiService::getNetworkStatus(httpd_req_t *request) {
void WiFiService::manageSTA() {
if (WiFi.isConnected() || state().wifi_networks_count == 0) return;
if ((WiFi.getMode() & WIFI_STA) == 0) connectToWiFi();
}
wifi_mode_t mode = WiFi.getMode();
if (mode == WIFI_MODE_NULL || mode == WIFI_MODE_AP) return;
void WiFiService::connectToWiFi() {
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);
static uint32_t startTime = 0;
static bool attempted = false;
WiFiNetwork *bestNetwork = nullptr;
int32_t bestNetworkDb = FACTORY_WIFI_RSSI_THRESHOLD;
if (startTime == 0) {
startTime = esp_timer_get_time() / 1000;
return;
}
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;
uint32_t now = esp_timer_get_time() / 1000;
if (now - startTime < 3000) return;
WiFi.getNetworkInfo(i, ssid_scan, sec_scan, rssi_scan, BSSID_scan, chan_scan);
for (pb_size_t j = 0; j < state().wifi_networks_count; j++) {
WiFiNetwork &network = state().wifi_networks[j];
if (ssid_scan == network.ssid) {
if (rssi_scan >= FACTORY_WIFI_RSSI_THRESHOLD) {
// Network is available
}
if (rssi_scan > bestNetworkDb) {
bestNetworkDb = rssi_scan;
bestNetwork = &network;
}
}
}
}
if (!state().priority_rssi) {
for (pb_size_t j = 0; j < state().wifi_networks_count; j++) {
WiFiNetwork &network = state().wifi_networks[j];
// Check if this network was found in scan
for (int i = 0; i < scanResult; ++i) {
if (WiFi.SSID(i) == network.ssid) {
ESP_LOGI("WiFiSettingsService", "Connecting to first available network: %s", network.ssid);
configureNetwork(network);
WiFi.scanDelete();
return;
}
}
}
} else if (bestNetwork) {
ESP_LOGI("WiFiSettingsService", "Connecting to strongest network: %s", bestNetwork->ssid);
configureNetwork(*bestNetwork);
} else {
ESP_LOGI("WiFiSettingsService", "No known networks found.");
}
WiFi.scanDelete();
if (!attempted && state().wifi_networks_count > 0) {
attempted = true;
uint32_t idx = state().selected_network;
if (idx >= state().wifi_networks_count) idx = 0;
ESP_LOGI(TAG, "Connecting to: %s", state().wifi_networks[idx].ssid);
configureNetwork(state().wifi_networks[idx]);
}
}
void WiFiService::configureNetwork(WiFiNetwork &network) {
if (network.static_ip_config) {
WiFi.config(IPAddress(network.local_ip), IPAddress(network.gateway_ip),
IPAddress(network.subnet_mask), IPAddress(network.dns_ip_1), IPAddress(network.dns_ip_2));
WiFi.config(IPAddress(network.local_ip), IPAddress(network.gateway_ip), IPAddress(network.subnet_mask),
IPAddress(network.dns_ip_1), IPAddress(network.dns_ip_2));
} else {
WiFi.config(IPAddress(0, 0, 0, 0), IPAddress(0, 0, 0, 0), IPAddress(0, 0, 0, 0));
}
WiFi.setHostname(state().hostname);
WiFi.begin(network.ssid, network.password);
#if CONFIG_IDF_TARGET_ESP32C3
WiFi.setTxPower(WIFI_POWER_8_5dBm);
WiFi.setTxPower(8);
#endif
}
void WiFiService::onStationModeDisconnected(WiFiEvent_t event, WiFiEventInfo_t info) {
void WiFiService::onStationModeDisconnected(int32_t event, void *event_data) {
WiFi.disconnect(true);
ESP_LOGI("WiFiStatus", "WiFi Disconnected. Reason code=%d", info.wifi_sta_disconnected.reason);
wifi_event_sta_disconnected_t *info = static_cast<wifi_event_sta_disconnected_t *>(event_data);
ESP_LOGI(TAG, "WiFi Disconnected. Reason code=%d", info ? info->reason : 0);
}
void WiFiService::onStationModeStop(WiFiEvent_t event, WiFiEventInfo_t info) {
void WiFiService::onStationModeStop(int32_t event, void *event_data) {
if (_stopping) {
_lastConnectionAttempt = 0;
_stopping = false;
}
ESP_LOGI("WiFiStatus", "WiFi Connected.");
ESP_LOGI(TAG, "WiFi STA stopped.");
}
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());
void WiFiService::onStationModeGotIP(int32_t event, void *event_data) {
ESP_LOGI(TAG, "WiFi Got IP. localIP=%s, hostName=%s", WiFi.localIP().toString().c_str(), WiFi.getHostname());
}
+25 -2
View File
@@ -1,4 +1,14 @@
#include "www_mount.hpp"
#include <cstring>
static const WebAsset* findAsset(const char* uri) {
for (size_t i = 0; i < WWW_ASSETS_COUNT; i++) {
if (strcmp(WWW_ASSETS[i].uri, uri) == 0) {
return &WWW_ASSETS[i];
}
}
return nullptr;
}
static esp_err_t web_send(httpd_req_t* req, const WebAsset& asset) {
httpd_resp_set_status(req, "200 OK");
@@ -7,11 +17,11 @@ static esp_err_t web_send(httpd_req_t* req, const WebAsset& asset) {
if (WWW_OPT.add_vary) httpd_resp_set_hdr(req, "Vary", "Accept-Encoding");
char cc[64];
snprintf(cc, sizeof(cc), "public, immutable, max-age=%u", WWW_OPT.max_age);
snprintf(cc, sizeof(cc), "public, immutable, max-age=%lu", (unsigned long)WWW_OPT.max_age);
httpd_resp_set_hdr(req, "Cache-Control", cc);
char et[34];
snprintf(et, sizeof(et), "\"%08x\"", asset.etag);
snprintf(et, sizeof(et), "\"%08lx\"", (unsigned long)asset.etag);
httpd_resp_set_hdr(req, "ETag", et);
return httpd_resp_send(req, (const char*)asset.data, asset.len);
@@ -23,3 +33,16 @@ void mountStaticAssets(WebServer& server) {
server.on(a->uri, HTTP_GET, [a](httpd_req_t* req) { return web_send(req, *a); });
}
}
void mountSpaFallback(WebServer& server) {
const WebAsset* indexAsset = findAsset(WWW_OPT.default_uri);
if (indexAsset) {
server.on("/*", HTTP_GET, [indexAsset](httpd_req_t* req) {
if (strncmp(req->uri, "/api/", 5) == 0) {
httpd_resp_send_err(req, HTTPD_404_NOT_FOUND, "Not found");
return ESP_FAIL;
}
return web_send(req, *indexAsset);
});
}
}
+1
View File
@@ -82,6 +82,7 @@ message WifiSettings {
string hostname = 1;
bool priority_rssi = 2;
repeated WifiNetwork wifi_networks = 3; // max 5 networks
uint32 selected_network = 4;
}
message WifiSettingsRequest {}
+2 -2
View File
@@ -16,11 +16,11 @@ socket_message.FSListResponse.directories max_count:20
# Streaming download messages
socket_message.FSDownloadRequest.path max_size:256
socket_message.FSDownloadMetadata.error max_size:128
socket_message.FSDownloadData.data max_size:16384
socket_message.FSDownloadData.data type:FT_POINTER
socket_message.FSDownloadComplete.error max_size:128
# Streaming upload messages
socket_message.FSUploadStart.path max_size:256
socket_message.FSUploadStartResponse.error max_size:128
socket_message.FSUploadData.data max_size:16384
socket_message.FSUploadData.data type:FT_POINTER
socket_message.FSUploadComplete.error max_size:128
+21 -15
View File
@@ -40,14 +40,13 @@ build_flags=
[env:esp32-wroom-camera]
board = esp32-s3-devkitc-1
board_build.arduino.memory_type = qio_opi
board_build.flash_mode = qio
board_upload.flash_size = 8MB
upload_speed = 921600
board_build.partitions = esp32/partition_table/default_8MB.csv
build_flags =
${env.build_flags}
-DBOARD_HAS_PSRAM
-mfix-esp32-psram-cache-issue
-D USE_CAMERA=1
-D CAMERA_MODEL_ESP32S3_EYE=1
-D WS2812_PIN=48
@@ -55,6 +54,12 @@ build_flags =
-D USS_RIGHT_PIN=14
-D SDA_PIN=47
-D SCL_PIN=21
-D SD_CMD_PIN=GPIO_NUM_38
-D SD_CLK_PIN=GPIO_NUM_39
-D SD_DATA_PIN=GPIO_NUM_40
[env:seeed-xiao-esp32s3]
platform = espressif32
@@ -80,12 +85,11 @@ build_flags =
[env]
platform = espressif32 @ 6.8.1
framework = arduino
framework = espidf
monitor_speed = 115200
monitor_filters =
monitor_filters =
direct
esp32_exception_decoder
default
colorize
build_flags =
${factory_settings.build_flags}
${features.build_flags}
@@ -98,25 +102,27 @@ build_flags =
-fdata-sections
-Wl,--gc-sections
-I submodules/nanopb
-Wno-missing-braces ; Added to suppress protobufs extra nested braces causing warning
-D PB_FIELD_32BIT=1
-D PB_ENABLE_MALLOC=1
-Wno-missing-braces
-Wno-format
-D CONFIG_HTTPD_WS_SUPPORT=1
build_unflags = -std=gnu++11
build_src_filter =
+<*>
+<../../submodules/nanopb/pb_*.c>
build_src_flags =
-Wformat=2
-Wformat-truncation
-Wstack-usage=4096
-Wno-format
test_ignore = test_embedded
board_build.filesystem = littlefs
board_build.embed_txtfiles =
board_build.sdkconfig_defaults = esp32/sdkconfig.defaults
lib_deps =
ArduinoJson@>=7.0.0
teckel12/NewPing@^1.9.7
FastLED@3.5.0
lib_ldf_mode = deep
lib_compat_mode = strict
extra_scripts =
pre:esp32/scripts/pre_build.py
pre:esp32/scripts/build_app.py
lib_compat_mode = strict
; debug_tool = esp-builtin
; debug_init_break =
; upload_port = COM[13] # Only use this when upload port is not correctly detected due to multiple COM ports attached.
; upload_port = COM[13] # Only use this when upload port is not correctly detected due to multiple COM ports attached.
+40
View File
@@ -0,0 +1,40 @@
# HTTP Server WebSocket support
CONFIG_HTTPD_WS_SUPPORT=y
CONFIG_HTTPD_MAX_REQ_HDR_LEN=2048
CONFIG_HTTPD_MAX_URI_LEN=1024
# mDNS
CONFIG_MDNS_MAX_SERVICES=10
# LittleFS
CONFIG_LITTLEFS_MAX_PARTITIONS=3
#
# ESP System Settings
#
# CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ_80 is not set
# CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ_160 is not set
CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ_240=y
CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ=240
# Main task stack
CONFIG_ESP_MAIN_TASK_STACK_SIZE=8192
CONFIG_PTHREAD_TASK_STACK_SIZE_DEFAULT=4096
# PSRAM for S3
CONFIG_SPIRAM=y
CONFIG_SPIRAM_MODE_OCT=y
CONFIG_SPIRAM_SPEED_80M=y
# DSP library
CONFIG_DSP_MAX_FFT_SIZE_4096=y
# Compiler
CONFIG_COMPILER_OPTIMIZATION_PERF=y
# Partition table
CONFIG_PARTITION_TABLE_CUSTOM=y
# CONFIG_FATFS_LFN_NONE is not set
# CONFIG_FATFS_LFN_HEAP is not set
CONFIG_FATFS_LFN_STACK=y