♻️ Major clean up of project structure

This commit is contained in:
Rune Harlyk
2025-09-11 17:14:31 +02:00
committed by Rune Harlyk
parent 26c36b8302
commit 7fd35f3f48
25 changed files with 370 additions and 890 deletions
+1 -2
View File
@@ -2,9 +2,8 @@
build_flags =
-D BUILD_TARGET=\"$PIOENV\"
-D APPLICATION_CORE=0
-D EMBED_WWW
-D EMBED_WEBAPP=1
-D SERVE_CONFIG_FILES
-D ENABLE_CORS
-D USE_MSGPACK=1 ; Use either msgpack or json
-D USE_JSON=0 ; Use either msgpack or json
+4 -4
View File
@@ -14,9 +14,9 @@ typedef std::function<void(const String &originId, bool sync)> SubscribeCallback
class EventSocket {
public:
EventSocket();
EventSocket(PsychicHttpServer &server, const char *route = "/api/ws");
PsychicWebSocketHandler *getHandler() { return &_socket; }
void begin();
bool hasSubscribers(const char *event);
@@ -28,6 +28,8 @@ class EventSocket {
private:
PsychicWebSocketHandler _socket;
PsychicHttpServer &_server;
const char *_route;
std::map<String, std::list<int>> client_subscriptions;
std::map<String, std::list<EventCallback>> event_callbacks;
@@ -41,6 +43,4 @@ class EventSocket {
esp_err_t onFrame(PsychicWebSocketRequest *request, httpd_ws_frame *frame);
};
extern EventSocket socket;
#endif
+1 -1
View File
@@ -4,7 +4,7 @@
#include <LittleFS.h>
#define ESPFS LittleFS
#define ESP_FS LittleFS
#define AP_SETTINGS_FILE "/config/apSettings.json"
#define CAMERA_SETTINGS_FILE "/config/cameraSettings.json"
-24
View File
@@ -1,24 +0,0 @@
#pragma once
#include <Arduino.h>
#include <WiFi.h>
#include <ArduinoJson.h>
#include <event_socket.h>
#include <PsychicHttp.h>
#include <HTTPClient.h>
#include <HTTPUpdate.h>
#include <task_manager.h>
#define EVENT_DOWNLOAD_OTA "otastatus"
#define OTA_TASK_STACK_SIZE 9216
class DownloadFirmwareService {
public:
DownloadFirmwareService();
esp_err_t handleDownloadUpdate(PsychicRequest *request, JsonVariant &json);
private:
};
-32
View File
@@ -1,32 +0,0 @@
#ifndef FirmwareUploadService_h
#define FirmwareUploadService_h
#include <Arduino.h>
#include <Update.h>
#include <WiFi.h>
#include <PsychicHttp.h>
#include <system_service.h>
#include <event_socket.h>
enum FileType { ft_none = 0, ft_firmware = 1, ft_md5 = 2 };
class FirmwareUploadService {
public:
FirmwareUploadService();
void begin();
PsychicUploadHandler *getHandler() { return &uploadHandler; }
private:
PsychicUploadHandler uploadHandler;
esp_err_t handleUpload(PsychicRequest *request, const String &filename, uint64_t index, uint8_t *data, size_t len,
bool final);
esp_err_t uploadComplete(PsychicRequest *request);
esp_err_t handleError(PsychicRequest *request, int code);
esp_err_t handleEarlyDisconnect();
};
#endif // end FirmwareUploadService_h
+14 -32
View File
@@ -13,33 +13,13 @@
#include <motion_states/rest_state.h>
#include <message_types.h>
#define DEFAULT_STATE false
#define ANGLES_EVENT "angles"
#define INPUT_EVENT "input"
#define MODE_EVENT "mode"
#define WALK_GAIT_EVENT "walk_gait"
enum class MOTION_STATE { DEACTIVATED, IDLE, CALIBRATION, REST, STAND, WALK };
class MotionService {
public:
MotionService(ServoController *servoController, Peripherals *peripherals)
: _servoController(servoController), _peripherals(peripherals) {}
MotionService() {}
void begin() {
socket.onEvent(INPUT_EVENT, [&](JsonVariant &root, int originId) { handleInput(root, originId); });
socket.onEvent(MODE_EVENT, [&](JsonVariant &root, int originId) { handleMode(root, originId); });
socket.onEvent(WALK_GAIT_EVENT, [&](JsonVariant &root, int originId) { handleWalkGait(root, originId); });
socket.onEvent(ANGLES_EVENT, [&](JsonVariant &root, int originId) { anglesEvent(root, originId); });
socket.onSubscribe(ANGLES_EVENT,
std::bind(&MotionService::syncAngles, this, std::placeholders::_1, std::placeholders::_2));
body_state.updateFeet(KinConfig::default_feet_positions);
}
void begin() { body_state.updateFeet(KinConfig::default_feet_positions); }
void anglesEvent(JsonVariant &root, int originId) {
JsonArray array = root.as<JsonArray>();
@@ -50,12 +30,14 @@ class MotionService {
}
void setState(MotionState *newState) {
_servoController->activate();
if (state) {
state->end();
}
state = newState;
if (state) state->begin();
if (state) {
_servoController->activate();
state->begin();
}
}
void handleInput(JsonVariant &root, int originId) {
@@ -89,7 +71,7 @@ class MotionService {
JsonDocument doc;
doc.set(static_cast<int>(mode));
JsonVariant data = doc.as<JsonVariant>();
socket.emit(MODE_EVENT, data, String(originId).c_str());
// socket.emit(MODE_EVENT, data, String(originId).c_str());
}
void emitAngles(const String &originId = "", bool sync = false) {
@@ -97,7 +79,7 @@ class MotionService {
auto arr = doc.to<JsonArray>();
for (int i = 0; i < 12; i++) arr.add(angles[i]);
JsonVariant data = doc.as<JsonVariant>();
socket.emit(ANGLES_EVENT, data, originId.c_str());
// socket.emit(ANGLES_EVENT, data, originId.c_str());
}
void syncAngles(const String &originId = "", bool sync = false) {
@@ -105,8 +87,7 @@ class MotionService {
_servoController->setAngles(angles);
}
void handleGestures() {
const gesture_t ges = _peripherals->takeGesture();
void handleGestures(const gesture_t ges) {
if (ges != gesture_t::eGestureNone) {
ESP_LOGI("Motion", "Gesture: %d", ges);
switch (ges) {
@@ -120,13 +101,13 @@ class MotionService {
}
}
bool updateMotion() {
handleGestures();
bool update(Peripherals *peripherals) {
handleGestures(peripherals->takeGesture());
if (!state) return false;
unsigned long now = millis();
float dt = (now - lastUpdate) / 1000.0f;
lastUpdate = now;
state->updateImuOffsets(_peripherals->angleY(), _peripherals->angleX());
state->updateImuOffsets(peripherals->angleY(), peripherals->angleX());
state->step(body_state, dt);
kinematics.calculate_inverse_kinematics(body_state, new_angles);
@@ -147,9 +128,10 @@ class MotionService {
float *getAngles() { return angles; }
inline bool isActive() { return state != nullptr; }
private:
ServoController *_servoController;
Peripherals *_peripherals;
Kinematics kinematics;
CommandMsg command = {0, 0, 0, 0, 0, 0, 0};
+1 -1
View File
@@ -161,7 +161,7 @@ class GestureSensor {
gesture_t getGesture() { return msg.gesture; }
gesture_t takeGesture() {
gesture_t const takeGesture() {
const auto g = msg.gesture;
msg.gesture = eGestureNone;
return g;
+19 -12
View File
@@ -49,15 +49,15 @@ class Peripherals : public StatefulService<PeripheralsConfiguration> {
_eventEndpoint.begin();
_persistence.readFromFS();
socket.onEvent(EVENT_I2C_SCAN, [&](JsonVariant &root, int originId) {
scanI2C();
emitI2C();
});
// socket.onEvent(EVENT_I2C_SCAN, [&](JsonVariant &root, int originId) {
// scanI2C();
// emitI2C();
// });
socket.onSubscribe(EVENT_I2C_SCAN, [&](const String &originId, bool sync) {
scanI2C();
emitI2C(originId, sync);
});
// socket.onSubscribe(EVENT_I2C_SCAN, [&](const String &originId, bool sync) {
// scanI2C();
// emitI2C(originId, sync);
// });
updatePins();
@@ -79,6 +79,13 @@ class Peripherals : public StatefulService<PeripheralsConfiguration> {
#endif
};
void update() {
readIMU();
readMag();
// _peripherals.readBMP();
EXECUTE_EVERY_N_MS(100, { readGesture(); });
}
void loop() {
EXECUTE_EVERY_N_MS(_updateInterval, {
beginTransaction();
@@ -112,7 +119,7 @@ class Peripherals : public StatefulService<PeripheralsConfiguration> {
}
ESP_LOGI("Peripherals", "Emitting I2C scan results, %s %d", originId.c_str(), sync);
JsonVariant data = doc.as<JsonVariant>();
socket.emit(EVENT_I2C_SCAN, data, originId.c_str(), sync);
// socket.emit(EVENT_I2C_SCAN, data, originId.c_str(), sync);
}
void scanI2C(uint8_t lower = 1, uint8_t higher = 127) {
@@ -197,7 +204,7 @@ class Peripherals : public StatefulService<PeripheralsConfiguration> {
// float angleZ() { return _imu.getAngleZ(); }
gesture_t takeGesture() {
gesture_t const takeGesture() {
return
#if FT_ENABLED(USE_PAJ7620U2)
_gesture.takeGesture();
@@ -225,7 +232,7 @@ class Peripherals : public StatefulService<PeripheralsConfiguration> {
#endif
#if FT_ENABLED(USE_MPU6050 || USE_BNO055) || FT_ENABLED(USE_HMC5883)
JsonVariant data = doc.as<JsonVariant>();
socket.emit(EVENT_IMU, data);
// socket.emit(EVENT_IMU, data);
#endif
}
@@ -235,7 +242,7 @@ class Peripherals : public StatefulService<PeripheralsConfiguration> {
JsonArray root = doc.to<JsonArray>();
root[0] = _left_distance, root[1] = _right_distance;
JsonVariant data = doc.as<JsonVariant>();
socket.emit("sonar", data);
// socket.emit("sonar", data);
#endif
}
+8 -8
View File
@@ -32,16 +32,16 @@ class ServoController : public StatefulService<ServoSettings> {
_persistence(ServoSettings::read, ServoSettings::update, this, SERVO_SETTINGS_FILE) {}
void begin() {
socket.onEvent(EVENT_SERVO_CONFIGURATION_SETTINGS,
[&](JsonVariant &root, int originId) { servoEvent(root, originId); });
socket.onEvent(EVENT_SERVO_STATE, [&](JsonVariant &root, int originId) { stateUpdate(root, originId); });
// socket.onEvent(EVENT_SERVO_CONFIGURATION_SETTINGS,
// [&](JsonVariant &root, int originId) { servoEvent(root, originId); });
// socket.onEvent(EVENT_SERVO_STATE, [&](JsonVariant &root, int originId) { stateUpdate(root, originId); });
_persistence.readFromFS();
initializePCA();
socket.onEvent(EVENT_SERVO_STATE, [&](JsonVariant &root, int originId) {
is_active = root["active"] | false;
is_active ? activate() : deactivate();
});
// socket.onEvent(EVENT_SERVO_STATE, [&](JsonVariant &root, int originId) {
// is_active = root["active"] | false;
// is_active ? activate() : deactivate();
// });
}
void pcaWrite(int index, int value) {
@@ -110,7 +110,7 @@ class ServoController : public StatefulService<ServoSettings> {
_pca.setMultiplePWM(pwms, 12);
}
void updateServoState() {
void update() {
if (control_state == SERVO_CONTROL_STATE::ANGLE) calculatePWM();
}
-114
View File
@@ -1,114 +0,0 @@
#ifndef Spot_h
#define Spot_h
#include <Arduino.h>
#include <PsychicHttp.h>
#include <ESPmDNS.h>
#include <WiFi.h>
#include <Wire.h>
#include <filesystem.h>
#include <firmware_download_service.h>
#include <firmware_upload_service.h>
#include <peripherals/peripherals.h>
#include <peripherals/servo_controller.h>
#include <peripherals/led_service.h>
#include <peripherals/camera_service.h>
#include <event_socket.h>
#include <features.h>
#include <motion.h>
#include <task_manager.h>
#include <wifi_service.h>
#include <ap_service.h>
#include <mdns_service.h>
#ifdef EMBED_WWW
#include <WWWData.h>
#endif
#ifndef APP_VERSION
#define APP_VERSION "v1"
#endif
#ifndef APP_NAME
#define APP_NAME "SpotMicro"
#endif
#ifndef APPLICATION_CORE
#define APPLICATION_CORE -1
#endif
class Spot {
public:
Spot();
void initialize();
// sense
void readSensors() {
_peripherals.readIMU();
_peripherals.readMag();
_peripherals.readBMP();
EXECUTE_EVERY_N_MS(100, { _peripherals.readGesture(); });
}
// plan
void planMotion() { updatedMotion = _motionService.updateMotion(); }
// act
void updateActuators() {
if (updatedMotion) _servoController.setAngles(_motionService.getAngles());
updatedMotion = false;
_servoController.updateServoState();
#if FT_ENABLED(USE_WS2812)
_ledService.loop();
#endif
}
// communicate
void emitTelemetry() {
if (updatedMotion) EXECUTE_EVERY_N_MS(100, { _motionService.emitAngles(); });
EXECUTE_EVERY_N_MS(250, { _peripherals.emitIMU(); });
// _peripherals.emitSonar();
}
private:
PsychicHttpServer _server;
WiFiService _wifiService;
APService _apService;
EventSocket _socket;
MDNSService _mdnsService;
#if FT_ENABLED(USE_UPLOAD_FIRMWARE)
FirmwareUploadService _uploadFirmwareService;
#endif
#if FT_ENABLED(USE_DOWNLOAD_FIRMWARE)
DownloadFirmwareService _downloadFirmwareService;
#endif
#if FT_ENABLED(USE_MOTION)
MotionService _motionService;
#endif
#if FT_ENABLED(USE_CAMERA)
Camera::CameraService _cameraService;
#endif
Peripherals _peripherals;
ServoController _servoController;
#if FT_ENABLED(USE_WS2812)
LEDService _ledService;
#endif
bool updatedMotion = false;
const char *_appName = APP_NAME;
const u_int16_t _numberEndpoints = 137 + 36;
const u_int32_t _maxFileUpload = 2300000; // 2.3 MB
const uint16_t _port = 80;
protected:
void loop();
static void _loopImpl(void *_this) { static_cast<Spot *>(_this)->loop(); }
void setupServer();
void startServices();
};
#endif
+1 -1
View File
@@ -5,7 +5,7 @@
#include <PsychicHttp.h>
#include <WiFi.h>
#include <task_manager.h>
#include <event_socket.h>
// #include <event_socket.h>
#include <filesystem.h>
#include <global.h>
@@ -90,7 +90,7 @@ class FSPersistence {
JsonStateReader<T> _stateReader;
JsonStateUpdater<T> _stateUpdater;
StatefulService<T> *_statefulService;
FS *_fs {&ESPFS};
FS *_fs {&ESP_FS};
const char *_filePath;
size_t _bufferSize;
HandlerId _updateHandlerId;
+6 -6
View File
@@ -2,7 +2,7 @@
#include <PsychicHttp.h>
#include <event_socket.h>
// #include <event_socket.h>
#include <template/stateful_service.h>
template <class T>
@@ -15,10 +15,10 @@ class EventEndpoint {
}
void begin() {
socket.onEvent(_event,
std::bind(&EventEndpoint::updateState, this, std::placeholders::_1, std::placeholders::_2));
socket.onSubscribe(_event,
std::bind(&EventEndpoint::syncState, this, std::placeholders::_1, std::placeholders::_2));
// socket.onEvent(_event,
// std::bind(&EventEndpoint::updateState, this, std::placeholders::_1, std::placeholders::_2));
// socket.onSubscribe(_event,
// std::bind(&EventEndpoint::syncState, this, std::placeholders::_1, std::placeholders::_2));
}
private:
@@ -35,6 +35,6 @@ class EventEndpoint {
JsonDocument jsonDocument;
JsonVariant root = jsonDocument.to<JsonVariant>();
_statefulService->read(root, _stateReader);
socket.emit(_event, root, originId.c_str(), sync);
// socket.emit(_event, root, originId.c_str(), sync);
}
};
+5
View File
@@ -0,0 +1,5 @@
#pragma once
#include <PsychicHttp.h>
#include "WWWData.h"
void mountStaticAssets(PsychicHttpServer& s);
+113 -156
View File
@@ -1,44 +1,49 @@
# 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) 2023 Maxtrium B.V. [ code available under dual license ]
# Copyright (C) 2024 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.
from functools import lru_cache
from pathlib import Path
from shutil import copytree, rmtree, copyfileobj
from os.path import exists, getmtime
from os.path import exists, getmtime, splitext
import os
import gzip
import mimetypes
import glob
from datetime import datetime
import zlib
Import("env")
project_dir = env["PROJECT_DIR"]
buildFlags = env.ParseFlags(env["BUILD_FLAGS"])
interface_dir = project_dir + "/app"
output_file = project_dir + "/esp32/include/WWWData.h"
source_www_dir = interface_dir + "/src"
build_dir = interface_dir + "/build"
filesystem_dir = project_dir + "/data/www"
interface_dir = f"{project_dir}/app"
output_file = f"{project_dir}/esp32/include/WWWData.h"
source_www_dir = f"{interface_dir}/src"
build_dir = f"{interface_dir}/build"
filesystem_dir = f"{project_dir}/data"
Path(filesystem_dir).mkdir(exist_ok=True)
Path(output_file).parent.mkdir(parents=True, exist_ok=True)
mimetypes.init()
already_compressed_ext = {
".gz", ".br", ".png", ".jpg", ".jpeg", ".webp", ".gif", ".mp4", ".m4v", ".mov", ".avi", ".mkv", ".mp3", ".aac", ".ogg", ".wav",
".wasm", ".pdf", ".ico", ".woff", ".woff2", ".ttf", ".otf", ".7z", ".zip", ".rar", ".bz2", ".xz", ".lz", ".svgz"
}
@lru_cache(1)
def get_flag(flag, default=None):
for d in buildFlags.get("CPPDEFINES", []):
if d == flag:
return True
if isinstance(d, (list, tuple)) and d[0] == flag:
return d[1] if len(d) > 1 else True
return default
def get_files_to_exclude():
files_to_exclude = []
if (flag_exists("SPOTMICRO_ESP32") or flag_exists("SPOTMICRO_ESP32_MINI")) and not flag_exists("SPOTMICRO_YERTLE"):
if (get_flag("SPOTMICRO_ESP32") or get_flag("SPOTMICRO_ESP32_MINI")) and not get_flag("SPOTMICRO_YERTLE"):
print("Excluding Yertle files for SPOTMICRO_ESP32 build")
files_to_exclude.extend(["yertle.URDF", "URDF.zip", "URDF/"])
elif flag_exists("SPOTMICRO_YERTLE") and not flag_exists("SPOTMICRO_ESP32") and not flag_exists("SPOTMICRO_ESP32_MINI"):
elif get_flag("SPOTMICRO_YERTLE") and not get_flag("SPOTMICRO_ESP32") and not get_flag("SPOTMICRO_ESP32_MINI"):
print("Excluding Spot Micro files for SPOTMICRO_YERTLE build")
files_to_exclude.extend(["spot_micro.urdf.xacro", "stl.zip", "stl/"])
else:
@@ -46,157 +51,109 @@ def get_files_to_exclude():
return files_to_exclude
def find_latest_timestamp_for_app():
return max((getmtime(f) for f in glob.glob(f"{source_www_dir}/**/*", recursive=True)))
def latest_ts():
files = [p for p in glob.glob(
f"{source_www_dir}/**/*", recursive=True) if os.path.isfile(p)]
return max(getmtime(p) for p in files) if files else 0
def should_regenerate_output_file():
if not flag_exists("EMBED_WWW") or not exists(output_file):
def needs_rebuild():
if not exists(output_file):
return True
last_source_change = find_latest_timestamp_for_app()
last_build = getmtime(output_file)
print(
f"Newest file: {datetime.fromtimestamp(last_source_change)}, output file: {datetime.fromtimestamp(last_build)}"
)
return last_build < last_source_change
return getmtime(output_file) < latest_ts()
def gzip_file(file):
with open(file, "rb") as f_in:
with gzip.open(file + ".gz", "wb") as f_out:
copyfileobj(f_in, f_out)
os.remove(file)
def flag_exists(flag):
for define in buildFlags.get("CPPDEFINES"):
if define == flag or (isinstance(define, list) and define[0] == flag):
return True
return False
def get_package_manager():
if exists(os.path.join(interface_dir, "package-lock.json")):
return "npm"
if exists(os.path.join(interface_dir, "yarn.lock")):
return "yarn"
def pkg_mgr():
if exists(os.path.join(interface_dir, "pnpm-lock.yaml")):
return "pnpm"
if exists(os.path.join(interface_dir, "yarn.lock")):
return "yarn"
if exists(os.path.join(interface_dir, "package-lock.json")):
return "npm"
def build_webapp():
if package_manager := get_package_manager():
print(f"Building interface with {package_manager}")
def build_web():
m = pkg_mgr()
if not m:
raise Exception(
"No lock-file found. Please install dependencies for interface")
cwd = os.getcwd()
try:
os.chdir(interface_dir)
env.Execute(f"{package_manager} install")
env.Execute(f"{package_manager} run build:embedded")
os.chdir("..")
else:
raise Exception("No lock-file found. Please install dependencies for interface (eg. npm install)")
env.Execute(f"{m} install")
env.Execute(f"{m} run build:embedded")
finally:
os.chdir(cwd)
def embed_webapp():
if flag_exists("EMBED_WWW"):
print("Converting interface to PROGMEM")
build_progmem()
return
add_app_to_filesystem()
def encode_asset_data(path):
ext = splitext(path.name)[1].lower()
raw = path.read_bytes()
if ext in already_compressed_ext:
return raw, 0, zlib.crc32(raw) & 0xFFFFFFFF
gz = gzip.compress(raw, mtime=0)
return gz, 1, zlib.crc32(gz) & 0xFFFFFFFF
def build_progmem():
mimetypes.init()
with open(output_file, "w") as progmem:
progmem.write("#include <functional>\n")
progmem.write("#include <Arduino.h>\n")
def write_header():
exclude = get_files_to_exclude()
assets = []
for p in sorted(Path(build_dir).rglob("*.*"), key=lambda x: x.relative_to(build_dir).as_posix()):
rel_path = p.relative_to(build_dir).as_posix()
if any(rel_path == ex or rel_path.startswith(ex.rstrip("/")) for ex in exclude):
continue
uri = "/" + rel_path
mime = mimetypes.guess_type(uri)[0] or "application/octet-stream"
data, gz_flag, etag = encode_asset_data(p)
assets.append((uri, mime, data, gz_flag, etag))
assetMap = {}
offsets, cursor = [], 0
for _, _, data, _, _ in assets:
offsets.append(cursor)
cursor += len(data)
files_to_exclude = get_files_to_exclude()
with open(output_file, "w", newline="\n") as f:
f.write("#pragma once\n")
f.write("#include <Arduino.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(
"struct WebOptions { const char* default_uri; uint32_t max_age; uint8_t add_vary; };\n\n")
for idx, path in enumerate(Path(build_dir).rglob("*.*")):
asset_path = path.relative_to(build_dir).as_posix()
f.write("static const uint8_t WWW_BLOB[] PROGMEM = {\n")
col = 0
for _, _, data, _, _ in assets:
for b in data:
if col == 0:
f.write("\t")
f.write(f"0x{b:02X},")
col = (col + 1) % 16
if col == 0:
f.write("\n")
if col != 0:
f.write("\n")
f.write("};\n\n")
should_exclude = False
for exclude_pattern in files_to_exclude:
if exclude_pattern.endswith("/"):
if asset_path.startswith(exclude_pattern):
should_exclude = True
print(f"Skipping {asset_path}")
break
elif asset_path == exclude_pattern:
should_exclude = True
print(f"Skipping {asset_path}")
break
for i, (uri, _, _, _, _) in enumerate(assets):
f.write(f'static const char WWW_URI_{i}[] PROGMEM = "{uri}";\n')
for i, (_, mime, _, _, _) in enumerate(assets):
f.write(f'static const char WWW_MIME_{i}[] PROGMEM = "{mime}";\n')
f.write("\n")
if should_exclude:
continue
f.write("static const WebAsset WWW_ASSETS[] PROGMEM = {\n")
for i, (_, _, data, gz_flag, etag) in enumerate(assets):
f.write(
f"\t{{WWW_URI_{i}, WWW_MIME_{i}, WWW_BLOB+{offsets[i]}, {len(data)}, 0x{etag:08X}, {gz_flag}}},\n")
f.write("};\n\n")
asset_mime = mimetypes.guess_type(asset_path)[0] or "application/octet-stream"
print(f"Converting {asset_path}")
asset_var = f"ESP_SVELTEKIT_DATA_{idx}"
progmem.write(f"// {asset_path}\n")
progmem.write(f"const uint8_t {asset_var}[] PROGMEM = {{\n\t")
file_data = gzip.compress(path.read_bytes())
for i, byte in enumerate(file_data):
if i and not (i % 16):
progmem.write("\n\t")
progmem.write(f"0x{byte:02X},")
progmem.write("\n};\n\n")
assetMap[asset_path] = {
"name": asset_var,
"mime": asset_mime,
"size": len(file_data),
}
progmem.write(
"typedef std::function<void(const String& uri, const String& contentType, const uint8_t * content, size_t len)> RouteRegistrationHandler;\n\n"
)
progmem.write("class WWWData {\n")
progmem.write("\tpublic:\n")
progmem.write("\t\tstatic void registerRoutes(RouteRegistrationHandler handler) {\n")
for asset_path, asset in assetMap.items():
progmem.write(f'\t\t\thandler("/{asset_path}", "{asset["mime"]}", {asset["name"]}, {asset["size"]});\n')
progmem.write("\t\t}\n")
progmem.write("};\n\n")
f.write(f"static const size_t WWW_ASSETS_COUNT = {len(assets)};\n")
default_uri = "/index.html" if any(u == "/index.html" for u,
_, _, _, _ in assets) else (assets[0][0] if assets else "/")
f.write(
f'static const WebOptions WWW_OPT = {{ "{default_uri}", 31536000u, 1 }};\n')
def add_app_to_filesystem():
build_path = Path(build_dir)
www_path = Path(filesystem_dir)
if www_path.exists() and www_path.is_dir():
rmtree(www_path)
print("Copying and compress app to data directory")
files_to_exclude = get_files_to_exclude()
def ignore_files(dir, files):
ignored = []
for file in files:
file_path = Path(dir) / file
relative_path = file_path.relative_to(build_path)
if str(relative_path) in files_to_exclude:
ignored.append(file)
print(f"Excluding: {relative_path}")
return ignored
copytree(build_path, www_path, ignore=ignore_files if files_to_exclude else None)
for current_path, _, files in os.walk(www_path):
for file in files:
gzip_file(os.path.join(current_path, file))
print("Build LittleFS file system image and upload to ESP32")
env.Execute("pio run --target uploadfs")
print("running: build_app.py")
if should_regenerate_output_file():
build_webapp()
embed_webapp()
if get_flag("EMBED_WEBAPP") == "1" and needs_rebuild():
print("Building web app")
build_web()
write_header()
+4 -4
View File
@@ -2,12 +2,14 @@
SemaphoreHandle_t clientSubscriptionsMutex = xSemaphoreCreateMutex();
EventSocket::EventSocket() {
EventSocket::EventSocket(PsychicHttpServer &server, const char *route) : _server(server), _route(route) {
_socket.onOpen((std::bind(&EventSocket::onWSOpen, this, std::placeholders::_1)));
_socket.onClose(std::bind(&EventSocket::onWSClose, this, std::placeholders::_1));
_socket.onFrame(std::bind(&EventSocket::onFrame, this, std::placeholders::_1, std::placeholders::_2));
}
void EventSocket::begin() { _server.on(_route, &_socket); }
void EventSocket::onWSOpen(PsychicWebSocketClient *client) {
ESP_LOGI("EventSocket", "ws[%s][%u] connect", client->remoteIP().toString().c_str(), client->socket());
}
@@ -171,6 +173,4 @@ void EventSocket::onEvent(String event, EventCallback callback) {
void EventSocket::onSubscribe(String event, SubscribeCallback callback) {
subscribe_callbacks[event].push_back(std::move(callback));
}
EventSocket socket;
}
+2 -4
View File
@@ -28,8 +28,7 @@ void printFeatureConfiguration() {
// Web services
ESP_LOGI("Features", "USE_MDNS: %s", USE_MDNS ? "enabled" : "disabled");
ESP_LOGI("Features", "EMBED_WWW: %s", EMBED_WWW ? "enabled" : "disabled");
ESP_LOGI("Features", "ENABLE_CORS: %s", ENABLE_CORS ? "enabled" : "disabled");
ESP_LOGI("Features", "EMBED_WEBAPP: %s", EMBED_WEBAPP ? "enabled" : "disabled");
ESP_LOGI("Features", "SERVE_CONFIG_FILES: %s", SERVE_CONFIG_FILES ? "enabled" : "disabled");
ESP_LOGI("Features", "KINEMATICS_VARIANT: %s", KINEMATICS_VARIANT_STR);
ESP_LOGI("Features", "==========================================================");
@@ -48,8 +47,7 @@ void features(JsonObject &root) {
root["servo"] = USE_PCA9685 ? true : false;
root["ws2812"] = USE_WS2812 ? true : false;
root["mdns"] = USE_MDNS ? true : false;
root["embed_www"] = EMBED_WWW ? true : false;
root["enable_cors"] = ENABLE_CORS ? true : false;
root["embed_www"] = EMBED_WEBAPP ? true : false;
root["serve_config_files"] = SERVE_CONFIG_FILES ? true : false;
root["firmware_version"] = APP_VERSION;
root["firmware_name"] = APP_NAME;
+4 -4
View File
@@ -40,10 +40,10 @@ esp_err_t handleEdit(PsychicRequest *request, JsonVariant &json) {
/* Helpers */
bool deleteFile(const char *filename) { return ESPFS.remove(filename); }
bool deleteFile(const char *filename) { return ESP_FS.remove(filename); }
String listFiles(const String &directory, bool isRoot) {
File root = ESPFS.open(directory.startsWith("/") ? directory : "/" + directory);
File root = ESP_FS.open(directory.startsWith("/") ? directory : "/" + directory);
if (!root.isDirectory()) return "{}";
File file = root.openNextFile();
@@ -95,7 +95,7 @@ esp_err_t uploadFile(PsychicRequest *request, const String &filename, uint64_t i
}
bool editFile(const char *filename, const char *content) {
File file = ESPFS.open(filename, FILE_WRITE);
File file = ESP_FS.open(filename, FILE_WRITE);
if (!file) return false;
file.print(content);
@@ -106,7 +106,7 @@ bool editFile(const char *filename, const char *content) {
esp_err_t mkdir(PsychicRequest *request, JsonVariant &json) {
const char *path = json["path"].as<const char *>();
ESP_LOGI(TAG, "Creating directory: %s", path);
return ESPFS.mkdir(path) ? request->reply(200) : request->reply(500);
return ESP_FS.mkdir(path) ? request->reply(200) : request->reply(500);
}
} // namespace FileSystem
-105
View File
@@ -1,105 +0,0 @@
/**
* ESP32 SvelteKit
*
* A simple, secure and extensible framework for IoT projects for ESP32 platforms
* with responsive Sveltekit front-end built with TailwindCSS and DaisyUI.
* https://github.com/theelims/ESP32-sveltekit
*
* Copyright (C) 2023 theelims
*
* All Rights Reserved. This software may be modified and distributed under
* the terms of the LGPL v3 license. See the LICENSE file for details.
**/
#include <firmware_download_service.h>
extern const uint8_t rootca_crt_bundle_start[] asm("_binary_src_certs_x509_crt_bundle_bin_start");
static int previousProgress = 0;
JsonVariant obj;
void update_started() {
obj["status"] = "preparing";
socket.emit(EVENT_DOWNLOAD_OTA, obj);
}
void update_progress(int currentBytes, int totalBytes) {
obj["status"] = "progress";
int progress = ((currentBytes * 100) / totalBytes);
if (progress > previousProgress) {
obj["progress"] = progress;
socket.emit(EVENT_DOWNLOAD_OTA, obj);
ESP_LOGV("Download OTA", "HTTP update process at %d of %d bytes... (%d %%)", currentBytes, totalBytes,
progress);
}
previousProgress = progress;
}
void update_finished() {
obj["status"] = "finished";
socket.emit(EVENT_DOWNLOAD_OTA, obj);
// delay to allow the event to be sent out
vTaskDelay(100 / portTICK_PERIOD_MS);
}
void updateTask(void *param) {
WiFiClientSecure client;
client.setCACertBundle(rootca_crt_bundle_start);
client.setTimeout(10);
httpUpdate.setFollowRedirects(HTTPC_FORCE_FOLLOW_REDIRECTS);
httpUpdate.rebootOnUpdate(true);
String url = *((String *)param);
// httpUpdate.onStart(update_started);
// httpUpdate.onProgress(update_progress);
// httpUpdate.onEnd(update_finished);
t_httpUpdate_return ret = httpUpdate.update(client, url.c_str());
switch (ret) {
case HTTP_UPDATE_FAILED:
obj["status"] = "error";
obj["error"] = httpUpdate.getLastErrorString().c_str();
socket.emit(EVENT_DOWNLOAD_OTA, obj);
ESP_LOGE("Download OTA", "HTTP Update failed with error (%d): %s", httpUpdate.getLastError(),
httpUpdate.getLastErrorString().c_str());
break;
case HTTP_UPDATE_NO_UPDATES:
obj["status"] = "error";
obj["error"] = "Update failed, has same firmware version";
socket.emit(EVENT_DOWNLOAD_OTA, obj);
ESP_LOGE("Download OTA", "HTTP Update failed, has same firmware version");
break;
case HTTP_UPDATE_OK: ESP_LOGI("Download OTA", "HTTP Update successful - Restarting"); break;
}
vTaskDelete(NULL);
}
DownloadFirmwareService::DownloadFirmwareService() {}
esp_err_t DownloadFirmwareService::handleDownloadUpdate(PsychicRequest *request, JsonVariant &json) {
if (!json.is<JsonObject>()) {
return request->reply(400);
}
String downloadURL = json["download_url"];
ESP_LOGI("Download OTA", "Starting OTA from: %s", downloadURL.c_str());
obj["status"] = "preparing";
obj["progress"] = 0;
obj["error"] = "";
socket.emit(EVENT_DOWNLOAD_OTA, obj);
const BaseType_t taskResult = g_taskManager.createTask(&updateTask, "Firmware download", OTA_TASK_STACK_SIZE,
&downloadURL, (configMAX_PRIORITIES - 1), NULL, 1);
if (taskResult != pdPASS) {
ESP_LOGE("Download OTA", "Couldn't create download OTA task");
return request->reply(500);
}
return request->reply(200);
}
-161
View File
@@ -1,161 +0,0 @@
#include <firmware_upload_service.h>
#include <esp_app_format.h>
#include <esp_ota_ops.h>
static const char *TAG = "FirmwareUploadService";
using namespace std::placeholders;
static char md5[33] = "\0";
static size_t fsize = 0;
static FileType fileType = ft_none;
FirmwareUploadService::FirmwareUploadService() {}
void FirmwareUploadService::begin() {
uploadHandler.onUpload(std::bind(&FirmwareUploadService::handleUpload, this, _1, _2, _3, _4, _5, _6));
uploadHandler.onRequest(std::bind(&FirmwareUploadService::uploadComplete, this, _1));
uploadHandler.onClose(std::bind(&FirmwareUploadService::handleEarlyDisconnect, this));
}
esp_err_t FirmwareUploadService::handleUpload(PsychicRequest *request, const String &filename, uint64_t index,
uint8_t *data, size_t len, bool final) {
// at init
if (!index) {
// check details of the file, to see if its a valid bin or json file
std::string fname(filename.c_str());
auto position = fname.find_last_of(".");
std::string extension = fname.substr(position + 1);
fsize = request->contentLength();
fileType = ft_none;
if ((extension == "bin") && (fsize > 1000000)) {
fileType = ft_firmware;
} else if (extension == "md5") {
fileType = ft_md5;
if (len == 32) {
memcpy(md5, data, 32);
md5[32] = '\0';
}
return ESP_OK;
} else {
md5[0] = '\0';
return handleError(request,
406); // Not Acceptable - unsupported file type
}
if (fileType == ft_firmware) {
// Check firmware header, 0xE9 magic offset 0 indicates esp bin, chip
// offset 12: esp32:0, S2:2, C3:5
#if CONFIG_IDF_TARGET_ESP32 // ESP32/PICO-D4
if (len > 12 && (data[0] != 0xE9 || data[12] != 0)) {
return handleError(request, 503); // service unavailable
}
#elif CONFIG_IDF_TARGET_ESP32S2
if (len > 12 && (data[0] != 0xE9 || data[12] != 2)) {
return handleError(request, 503); // service unavailable
}
#elif CONFIG_IDF_TARGET_ESP32C3
if (len > 12 && (data[0] != 0xE9 || data[12] != 5)) {
return handleError(request, 503); // service unavailable
}
#elif CONFIG_IDF_TARGET_ESP32S3
if (len > 12 && (data[0] != 0xE9 || data[12] != 9)) {
return handleError(request, 503); // service unavailable
}
#endif
// it's firmware - initialize the ArduinoOTA updater
if (Update.begin(fsize - sizeof(esp_image_header_t))) {
ESP_LOGI(TAG, "Starting update");
if (strlen(md5) == 32) {
Update.setMD5(md5);
md5[0] = '\0';
ESP_LOGI(TAG, "Setting MD5 hash");
}
} else {
return handleError(request, 507); // failed to begin, send an error
// response Insufficient Storage
}
}
}
// if we haven't delt with an error, continue with the firmware update
if (!request->_tempObject) {
if (Update.write(data, len) != len) {
handleError(request, 500);
} else {
JsonVariant obj;
obj["status"] = "progress";
obj["progress"] = (float)Update.progress() / (float)fsize * 100.f;
socket.emit("otastatus", obj);
delay(20);
}
if (final) {
if (!Update.end(true)) {
handleError(request, 500);
} else {
JsonVariant obj;
obj["status"] = "finished", obj["progress"] = 100;
socket.emit("otastatus", obj);
ESP_LOGI(TAG, "Finish writing update");
}
}
}
return ESP_OK;
}
esp_err_t FirmwareUploadService::uploadComplete(PsychicRequest *request) {
// if we completed uploading a md5 file create a JSON response
if (fileType == ft_md5) {
if (strlen(md5) == 32) {
PsychicJsonResponse response = PsychicJsonResponse(request, false);
JsonObject root = response.getRoot();
root["md5"] = md5;
return response.send();
}
return ESP_OK;
}
// if no error, send the success response
if (!request->_tempObject) {
ESP_LOGI(TAG, "Finish updating");
system_service::restart();
return ESP_OK;
}
// if updated has an error send 500 response and log on Serial
if (Update.hasError()) {
Update.printError(Serial);
Update.abort();
handleError(request, 500);
}
return ESP_OK;
}
esp_err_t FirmwareUploadService::handleError(PsychicRequest *request, int code) {
JsonVariant obj;
obj["status"] = "error", obj["error"] = Update.getError();
socket.emit("otastatus", obj);
// if we have had an error already, do nothing
if (request->_tempObject) {
return ESP_OK;
}
// send the error code to the client and record the error code in the temp
// object
request->_tempObject = new int(code);
return request->reply(code);
}
esp_err_t FirmwareUploadService::handleEarlyDisconnect() {
// if updated has not ended on connection close, abort it
if (!Update.end(true)) {
Update.printError(Serial);
Update.abort();
return ESP_OK;
}
return ESP_OK;
}
+138 -10
View File
@@ -1,26 +1,154 @@
#include <spot.h>
#include <Arduino.h>
#include <PsychicHttp.h>
#include <ESPmDNS.h>
#include <WiFi.h>
#include <Wire.h>
DRAM_ATTR Spot spot;
#include <filesystem.h>
#include <peripherals/peripherals.h>
#include <peripherals/servo_controller.h>
#include <peripherals/led_service.h>
#include <peripherals/camera_service.h>
#include <event_socket.h>
#include <features.h>
#include <motion.h>
#include <task_manager.h>
#include <wifi_service.h>
#include <ap_service.h>
#include <mdns_service.h>
#include <system_service.h>
void IRAM_ATTR SpotControlLoopEntry(void*) {
#include <www_mount.hpp>
// Communication
PsychicHttpServer server;
EventSocket socket {server, "/api/ws"};
// Core
Peripherals peripherals;
ServoController servoController;
MotionService motionService;
#if FT_ENABLED(USE_WS2812)
LEDService ledService;
#endif
#if FT_ENABLED(USE_CAMERA)
Camera::CameraService cameraService;
#endif
// Service
WiFiService wifiService;
APService apService;
void setupServer() {
server.config.max_uri_handlers = 5 + WWW_ASSETS_COUNT;
server.maxUploadSize = 1000000; // 1 MB;
server.listen(80);
server.serveStatic("/api/config/", ESP_FS, "/config/");
server.on("/api/features", feature_service::getFeatures);
#if USE_CAMERA
server.on("/api/camera/still", HTTP_GET,
[&](PsychicRequest *request) { return cameraService.cameraStill(request); });
server.on("/api/camera/stream", HTTP_GET,
[&](PsychicRequest *request) { return cameraService.cameraStream(request); });
server.on("/api/camera/settings", HTTP_GET,
[&](PsychicRequest *request) { return cameraService.endpoint.getState(request); });
server.on("/api/camera/settings", HTTP_POST, [&](PsychicRequest *request, JsonVariant &json) {
return cameraService.endpoint.handleStateUpdate(request, json);
});
#endif
#if EMBED_WEBAPP
mountStaticAssets(server);
#endif
server.on("/*", HTTP_OPTIONS, [](PsychicRequest *request) { // CORS handling
PsychicResponse response(request);
response.setCode(200);
return response.send();
});
DefaultHeaders::Instance().addHeader("Server", APP_NAME);
DefaultHeaders::Instance().addHeader("Access-Control-Allow-Origin", "*");
DefaultHeaders::Instance().addHeader("Access-Control-Allow-Headers", "Accept, Content-Type, Authorization");
DefaultHeaders::Instance().addHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
DefaultHeaders::Instance().addHeader("Access-Control-Max-Age", "86400");
}
#define ANGLES_EVENT "angles"
#define INPUT_EVENT "input"
#define MODE_EVENT "mode"
#define WALK_GAIT_EVENT "walk_gait"
void setupEventSocket() {
socket.onEvent(INPUT_EVENT, [&](JsonVariant &root, int originId) { motionService.handleInput(root, originId); });
socket.onEvent(MODE_EVENT, [&](JsonVariant &root, int originId) { motionService.handleMode(root, originId); });
socket.onEvent(WALK_GAIT_EVENT,
[&](JsonVariant &root, int originId) { motionService.handleWalkGait(root, originId); });
socket.onEvent(ANGLES_EVENT, [&](JsonVariant &root, int originId) { motionService.anglesEvent(root, originId); });
socket.onSubscribe(ANGLES_EVENT, std::bind(&MotionService::syncAngles, motionService, std::placeholders::_1,
std::placeholders::_2));
}
void IRAM_ATTR SpotControlLoopEntry(void *) {
ESP_LOGI("main", "Setup complete now runing tsk");
TickType_t xLastWakeTime = xTaskGetTickCount();
const TickType_t xFrequency = 5 / portTICK_PERIOD_MS;
peripherals.begin();
servoController.begin();
motionService.begin();
for (;;) {
spot.readSensors();
spot.planMotion();
spot.updateActuators();
spot.emitTelemetry();
CALLS_PER_SECOND(SpotControlLoopEntry);
peripherals.update();
motionService.update(&peripherals);
servoController.setAngles(motionService.getAngles());
motionService.isActive() ? servoController.activate() : servoController.deactivate();
servoController.update();
#if FT_ENABLED(USE_WS2812)
ledService.loop();
#endif
vTaskDelayUntil(&xLastWakeTime, xFrequency);
}
}
void IRAM_ATTR serviceLoopEntry(void *) {
ESP_LOGI("main", "Service control task starting");
wifiService.begin();
MDNS.begin(APP_NAME);
MDNS.setInstanceName(APP_NAME);
apService.begin();
setupServer();
socket.begin();
setupEventSocket();
ESP_LOGI("main", "Service control task started");
for (;;) {
wifiService.loop();
apService.loop();
EXECUTE_EVERY_N_MS(2000, system_service::emitMetrics());
vTaskDelay(100 / portTICK_PERIOD_MS);
}
}
void setup() {
Serial.begin(115200);
spot.initialize();
ESP_FS.begin();
g_taskManager.createTask(SpotControlLoopEntry, "Spot control task", 4096, nullptr, 5);
ESP_LOGI("main", "Booting robot");
feature_service::printFeatureConfiguration();
xTaskCreate(serviceLoopEntry, "Service task", 4096, nullptr, 2, nullptr);
xTaskCreatePinnedToCore(SpotControlLoopEntry, "Control task", 4096, nullptr, 5, nullptr, 1);
ESP_LOGI("main", "Finished booting");
}
void loop() { vTaskDelete(NULL); }
void loop() { vTaskDelete(nullptr); }
-195
View File
@@ -1,195 +0,0 @@
#include <spot.h>
static const char *TAG = "Spot";
Spot::Spot()
:
#if FT_ENABLED(USE_MOTION)
_motionService(&_servoController, &_peripherals)
#endif
{
}
void Spot::initialize() {
ESP_LOGI(TAG, "Running Firmware Version: %s", APP_VERSION);
feature_service::printFeatureConfiguration();
ESPFS.begin(true);
g_taskManager.begin();
#if FT_ENABLED(USE_WS2812)
_ledService.loop();
#endif
_wifiService.begin();
setupServer();
startServices();
ESP_LOGV(TAG, "Starting misc loop task");
g_taskManager.createTask(this->_loopImpl, "Spot misc", 4096, this, 2, NULL, APPLICATION_CORE);
}
void Spot::setupServer() {
_server.config.max_uri_handlers = _numberEndpoints;
_server.maxUploadSize = _maxFileUpload;
_server.listen(_port);
// WIFI
_server.on("/api/wifi/scan", HTTP_GET, _wifiService.handleScan);
_server.on("/api/wifi/networks", HTTP_GET,
[this](PsychicRequest *request) { return _wifiService.getNetworks(request); });
_server.on("/api/wifi/sta/status", HTTP_GET,
[this](PsychicRequest *request) { return _wifiService.getNetworkStatus(request); });
_server.on("/api/wifi/sta/settings", HTTP_GET,
[this](PsychicRequest *request) { return _wifiService.endpoint.getState(request); });
_server.on("/api/wifi/sta/settings", HTTP_POST, [this](PsychicRequest *request, JsonVariant &json) {
return _wifiService.endpoint.handleStateUpdate(request, json);
});
// AP
_server.on("/api/wifi/ap/status", HTTP_GET,
[this](PsychicRequest *request) { return _apService.getStatus(request); });
_server.on("/api/wifi/ap/settings", HTTP_GET,
[this](PsychicRequest *request) { return _apService.endpoint.getState(request); });
_server.on("/api/wifi/ap/settings", HTTP_POST, [this](PsychicRequest *request, JsonVariant &json) {
return _apService.endpoint.handleStateUpdate(request, json);
});
// CAMERA
#if USE_CAMERA
_server.on("/api/camera/still", HTTP_GET,
[this](PsychicRequest *request) { return _cameraService.cameraStill(request); });
_server.on("/api/camera/stream", HTTP_GET,
[this](PsychicRequest *request) { return _cameraService.cameraStream(request); });
_server.on("/api/camera/settings", HTTP_GET,
[this](PsychicRequest *request) { return _cameraService.endpoint.getState(request); });
_server.on("/api/camera/settings", HTTP_POST, [this](PsychicRequest *request, JsonVariant &json) {
return _cameraService.endpoint.handleStateUpdate(request, json);
});
#endif
// SYSTEM
_server.on("/api/system/reset", HTTP_POST, system_service::handleReset);
_server.on("/api/system/restart", HTTP_POST, system_service::handleRestart);
_server.on("/api/system/sleep", HTTP_POST, system_service::handleSleep);
_server.on("/api/system/status", HTTP_GET, system_service::getStatus);
_server.on("/api/system/metrics", HTTP_GET, system_service::getMetrics);
// FILESYSTEM
_server.on("/api/files", HTTP_GET, FileSystem::getFiles);
_server.on("/api/files/delete", HTTP_POST, FileSystem::handleDelete);
_server.on("/api/files/upload/*", HTTP_POST, FileSystem::uploadHandler);
_server.on("/api/files/edit", HTTP_POST, FileSystem::handleEdit);
_server.on("/api/files/mkdir", HTTP_POST, FileSystem::mkdir);
// SERVO
_server.on("/api/servo/config", HTTP_GET,
[this](PsychicRequest *request) { return _servoController.endpoint.getState(request); });
_server.on("/api/servo/config", HTTP_POST, [this](PsychicRequest *request, JsonVariant &json) {
return _servoController.endpoint.handleStateUpdate(request, json);
});
// PERIPHERALS
_server.on("/api/peripheral/settings", HTTP_GET,
[this](PsychicRequest *request) { return _peripherals.endpoint.getState(request); });
_server.on("/api/peripheral/settings", HTTP_POST, [this](PsychicRequest *request, JsonVariant &json) {
return _peripherals.endpoint.handleStateUpdate(request, json);
});
// MISC
_server.on("/api/ws/events", socket.getHandler());
_server.on("/api/features", feature_service::getFeatures);
#if FT_ENABLED(USE_UPLOAD_FIRMWARE)
_server.on("/api/firmware", HTTP_POST, _uploadFirmwareService.getHandler());
#endif
#if FT_ENABLED(USE_DOWNLOAD_FIRMWARE)
_server.on("/api/firmware/download", HTTP_POST, [this](PsychicRequest *r, JsonVariant &json) {
return _downloadFirmwareService.handleDownloadUpdate(r, json);
});
#endif
// MDNS
_server.on("/api/mdns/status", HTTP_GET,
[this](PsychicRequest *request) { return _mdnsService.getStatus(request); });
_server.on("/api/mdns/settings", HTTP_GET,
[this](PsychicRequest *request) { return _mdnsService.endpoint.getState(request); });
_server.on("/api/mdns/settings", HTTP_POST, [this](PsychicRequest *request, JsonVariant &json) {
return _mdnsService.endpoint.handleStateUpdate(request, json);
});
_server.on("/api/mdns/query", HTTP_POST, MDNSService::queryServices);
#ifdef EMBED_WWW
ESP_LOGV(TAG, "Registering routes from PROGMEM static resources");
WWWData::registerRoutes([&](const String &uri, const String &contentType, const uint8_t *content, size_t len) {
PsychicHttpRequestCallback requestHandler = [contentType, content, len](PsychicRequest *request) {
PsychicResponse response(request);
response.setCode(200);
response.setContentType(contentType.c_str());
response.addHeader("Content-Encoding", "gzip");
response.addHeader("Cache-Control", "public, immutable, max-age=31536000");
response.setContent(content, len);
return response.send();
};
PsychicWebHandler *handler = new PsychicWebHandler();
handler->onRequest(requestHandler);
_server.on(uri.c_str(), HTTP_GET, handler);
// Set default end-point for all non matching requests
// this is easier than using webServer.onNotFound()
if (uri.equals("/index.html")) {
_server.defaultEndpoint->setHandler(handler);
}
});
#else
// Serve static resources from /www/
ESP_LOGV(TAG, "Registering routes from FS /www/ static resources");
_server.serveStatic("/_app/", ESPFS, "/www/_app/");
_server.serveStatic("/favicon.png", ESPFS, "/www/favicon.png");
// Serving all other get requests with "/www/index.htm"
_server.onNotFound([](PsychicRequest *request) {
if (request->method() == HTTP_GET) {
PsychicFileResponse response(request, ESPFS, "/www/index.html", "text/html");
return response.send();
// String url = "http://" + request->host() + "/index.html";
// request->redirect(url.c_str());
}
});
#endif
#ifdef SERVE_CONFIG_FILES
_server.serveStatic("/api/config/", ESPFS, "/config/");
#endif
#if defined(ENABLE_CORS)
ESP_LOGV(TAG, "Enabling CORS headers");
DefaultHeaders::Instance().addHeader("Access-Control-Allow-Origin", "*");
DefaultHeaders::Instance().addHeader("Access-Control-Allow-Headers", "Accept, Content-Type, Authorization");
DefaultHeaders::Instance().addHeader("Access-Control-Allow-Credentials", "true");
#endif
DefaultHeaders::Instance().addHeader("Server", _appName);
}
void Spot::startServices() {
_apService.begin();
#if FT_ENABLED(USE_UPLOAD_FIRMWARE)
_uploadFirmwareService.begin();
#endif
_peripherals.begin();
_servoController.begin();
#if FT_ENABLED(USE_MOTION)
_motionService.begin();
#endif
#if FT_ENABLED(USE_CAMERA)
_cameraService.begin();
#endif
_mdnsService.begin();
}
void IRAM_ATTR Spot::loop() {
while (1) {
_wifiService.loop();
_apService.loop();
EXECUTE_EVERY_N_MS(2000, system_service::emitMetrics());
delay(20);
}
}
+12 -12
View File
@@ -38,12 +38,12 @@ esp_err_t getMetrics(PsychicRequest *request) {
void reset() {
ESP_LOGI(TAG, "Resetting device");
File root = ESPFS.open(FS_CONFIG_DIRECTORY);
File root = ESP_FS.open(FS_CONFIG_DIRECTORY);
File file;
while (file = root.openNextFile()) {
String path = file.path();
file.close();
ESPFS.remove(path);
ESP_FS.remove(path);
}
restart();
}
@@ -106,8 +106,8 @@ void status(JsonObject &root) {
root["arduino_version"] = ARDUINO_VERSION;
root["flash_chip_size"] = ESP.getFlashChipSize();
root["flash_chip_speed"] = ESP.getFlashChipSpeed();
root["fs_total"] = ESPFS.totalBytes();
root["fs_used"] = ESPFS.usedBytes();
root["fs_total"] = ESP_FS.totalBytes();
root["fs_used"] = ESP_FS.usedBytes();
root["core_temp"] = temperatureRead();
root["cpu_reset_reason"] = resetReason(esp_reset_reason());
root["uptime"] = millis() / 1000;
@@ -119,8 +119,8 @@ void metrics(JsonObject &root) {
root["total_heap"] = ESP.getHeapSize();
root["min_free_heap"] = ESP.getMinFreeHeap();
root["max_alloc_heap"] = ESP.getMaxAllocHeap();
root["fs_used"] = ESPFS.usedBytes();
root["fs_total"] = ESPFS.totalBytes();
root["fs_used"] = ESP_FS.usedBytes();
root["fs_total"] = ESP_FS.totalBytes();
root["core_temp"] = temperatureRead();
root["cpu0_usage"] = g_taskManager.getCpuUsage(0);
root["cpu1_usage"] = g_taskManager.getCpuUsage(1);
@@ -136,12 +136,12 @@ void metrics(JsonObject &root) {
}
void emitMetrics() {
if (!socket.hasSubscribers(EVENT_ANALYTICS)) return;
analyticsDoc.clear();
JsonObject root = analyticsDoc.to<JsonObject>();
system_service::metrics(root);
JsonVariant data = analyticsDoc.as<JsonVariant>();
socket.emit(EVENT_ANALYTICS, data);
// if (!socket.hasSubscribers(EVENT_ANALYTICS)) return;
// analyticsDoc.clear();
// JsonObject root = analyticsDoc.to<JsonObject>();
// system_service::metrics(root);
// JsonVariant data = analyticsDoc.as<JsonVariant>();
// socket.emit(EVENT_ANALYTICS, data);
}
const char *resetReason(esp_reset_reason_t reason) {
+35
View File
@@ -0,0 +1,35 @@
#include <Arduino.h>
#include "www_mount.hpp"
static esp_err_t web_send(PsychicRequest* req, const WebAsset& asset) {
PsychicResponse resp(req);
resp.setCode(200);
resp.setContentType(asset.mime);
if (asset.gz) resp.addHeader("Content-Encoding", "gzip");
if (WWW_OPT.add_vary) resp.addHeader("Vary", "Accept-Encoding");
char cc[64];
snprintf(cc, sizeof(cc), "public, immutable, max-age=%u", WWW_OPT.max_age);
resp.addHeader("Cache-Control", cc);
char et[34];
snprintf(et, sizeof(et), "\"%08x\"", asset.etag);
resp.addHeader("ETag", et);
resp.setContent(asset.data, asset.len);
return resp.send();
}
void mountStaticAssets(PsychicHttpServer& server) {
static uint8_t buf[sizeof(PsychicWebHandler) * WWW_ASSETS_COUNT];
for (size_t i = 0; i < WWW_ASSETS_COUNT; i++) {
const WebAsset* a = &WWW_ASSETS[i];
auto* handle = new (&buf[i * sizeof(PsychicWebHandler)]) PsychicWebHandler();
handle->onRequest([a](PsychicRequest* req) { return web_send(req, *a); });
server.on(a->uri, HTTP_GET, handle);
}
for (size_t i = 0; i < WWW_ASSETS_COUNT; i++) {
if (strcmp(WWW_ASSETS[i].uri, WWW_OPT.default_uri) == 0) {
server.defaultEndpoint->setHandler(
reinterpret_cast<PsychicWebHandler*>(&buf[i * sizeof(PsychicWebHandler)]));
break;
}
}
}