♻️ Major clean up of project structure
This commit is contained in:
@@ -35,7 +35,7 @@
|
|||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
const ws = $location ? $location : window.location.host
|
const ws = $location ? $location : window.location.host
|
||||||
socket.init(`ws://${ws}/api/ws/events`)
|
socket.init(`ws://${ws}/api/ws`)
|
||||||
|
|
||||||
addEventListeners()
|
addEventListeners()
|
||||||
|
|
||||||
|
|||||||
@@ -2,9 +2,8 @@
|
|||||||
build_flags =
|
build_flags =
|
||||||
-D BUILD_TARGET=\"$PIOENV\"
|
-D BUILD_TARGET=\"$PIOENV\"
|
||||||
-D APPLICATION_CORE=0
|
-D APPLICATION_CORE=0
|
||||||
-D EMBED_WWW
|
-D EMBED_WEBAPP=1
|
||||||
-D SERVE_CONFIG_FILES
|
-D SERVE_CONFIG_FILES
|
||||||
-D ENABLE_CORS
|
|
||||||
|
|
||||||
-D USE_MSGPACK=1 ; Use either msgpack or json
|
-D USE_MSGPACK=1 ; Use either msgpack or json
|
||||||
-D USE_JSON=0 ; Use either msgpack or json
|
-D USE_JSON=0 ; Use either msgpack or json
|
||||||
@@ -14,9 +14,9 @@ typedef std::function<void(const String &originId, bool sync)> SubscribeCallback
|
|||||||
|
|
||||||
class EventSocket {
|
class EventSocket {
|
||||||
public:
|
public:
|
||||||
EventSocket();
|
EventSocket(PsychicHttpServer &server, const char *route = "/api/ws");
|
||||||
|
|
||||||
PsychicWebSocketHandler *getHandler() { return &_socket; }
|
void begin();
|
||||||
|
|
||||||
bool hasSubscribers(const char *event);
|
bool hasSubscribers(const char *event);
|
||||||
|
|
||||||
@@ -28,6 +28,8 @@ class EventSocket {
|
|||||||
|
|
||||||
private:
|
private:
|
||||||
PsychicWebSocketHandler _socket;
|
PsychicWebSocketHandler _socket;
|
||||||
|
PsychicHttpServer &_server;
|
||||||
|
const char *_route;
|
||||||
|
|
||||||
std::map<String, std::list<int>> client_subscriptions;
|
std::map<String, std::list<int>> client_subscriptions;
|
||||||
std::map<String, std::list<EventCallback>> event_callbacks;
|
std::map<String, std::list<EventCallback>> event_callbacks;
|
||||||
@@ -41,6 +43,4 @@ class EventSocket {
|
|||||||
esp_err_t onFrame(PsychicWebSocketRequest *request, httpd_ws_frame *frame);
|
esp_err_t onFrame(PsychicWebSocketRequest *request, httpd_ws_frame *frame);
|
||||||
};
|
};
|
||||||
|
|
||||||
extern EventSocket socket;
|
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
#include <LittleFS.h>
|
#include <LittleFS.h>
|
||||||
|
|
||||||
#define ESPFS LittleFS
|
#define ESP_FS LittleFS
|
||||||
|
|
||||||
#define AP_SETTINGS_FILE "/config/apSettings.json"
|
#define AP_SETTINGS_FILE "/config/apSettings.json"
|
||||||
#define CAMERA_SETTINGS_FILE "/config/cameraSettings.json"
|
#define CAMERA_SETTINGS_FILE "/config/cameraSettings.json"
|
||||||
|
|||||||
@@ -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:
|
|
||||||
};
|
|
||||||
@@ -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
@@ -13,33 +13,13 @@
|
|||||||
#include <motion_states/rest_state.h>
|
#include <motion_states/rest_state.h>
|
||||||
#include <message_types.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 };
|
enum class MOTION_STATE { DEACTIVATED, IDLE, CALIBRATION, REST, STAND, WALK };
|
||||||
|
|
||||||
class MotionService {
|
class MotionService {
|
||||||
public:
|
public:
|
||||||
MotionService(ServoController *servoController, Peripherals *peripherals)
|
MotionService() {}
|
||||||
: _servoController(servoController), _peripherals(peripherals) {}
|
|
||||||
|
|
||||||
void begin() {
|
void begin() { body_state.updateFeet(KinConfig::default_feet_positions); }
|
||||||
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 anglesEvent(JsonVariant &root, int originId) {
|
void anglesEvent(JsonVariant &root, int originId) {
|
||||||
JsonArray array = root.as<JsonArray>();
|
JsonArray array = root.as<JsonArray>();
|
||||||
@@ -50,12 +30,14 @@ class MotionService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void setState(MotionState *newState) {
|
void setState(MotionState *newState) {
|
||||||
_servoController->activate();
|
|
||||||
if (state) {
|
if (state) {
|
||||||
state->end();
|
state->end();
|
||||||
}
|
}
|
||||||
state = newState;
|
state = newState;
|
||||||
if (state) state->begin();
|
if (state) {
|
||||||
|
_servoController->activate();
|
||||||
|
state->begin();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void handleInput(JsonVariant &root, int originId) {
|
void handleInput(JsonVariant &root, int originId) {
|
||||||
@@ -89,7 +71,7 @@ class MotionService {
|
|||||||
JsonDocument doc;
|
JsonDocument doc;
|
||||||
doc.set(static_cast<int>(mode));
|
doc.set(static_cast<int>(mode));
|
||||||
JsonVariant data = doc.as<JsonVariant>();
|
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) {
|
void emitAngles(const String &originId = "", bool sync = false) {
|
||||||
@@ -97,7 +79,7 @@ class MotionService {
|
|||||||
auto arr = doc.to<JsonArray>();
|
auto arr = doc.to<JsonArray>();
|
||||||
for (int i = 0; i < 12; i++) arr.add(angles[i]);
|
for (int i = 0; i < 12; i++) arr.add(angles[i]);
|
||||||
JsonVariant data = doc.as<JsonVariant>();
|
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) {
|
void syncAngles(const String &originId = "", bool sync = false) {
|
||||||
@@ -105,8 +87,7 @@ class MotionService {
|
|||||||
_servoController->setAngles(angles);
|
_servoController->setAngles(angles);
|
||||||
}
|
}
|
||||||
|
|
||||||
void handleGestures() {
|
void handleGestures(const gesture_t ges) {
|
||||||
const gesture_t ges = _peripherals->takeGesture();
|
|
||||||
if (ges != gesture_t::eGestureNone) {
|
if (ges != gesture_t::eGestureNone) {
|
||||||
ESP_LOGI("Motion", "Gesture: %d", ges);
|
ESP_LOGI("Motion", "Gesture: %d", ges);
|
||||||
switch (ges) {
|
switch (ges) {
|
||||||
@@ -120,13 +101,13 @@ class MotionService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bool updateMotion() {
|
bool update(Peripherals *peripherals) {
|
||||||
handleGestures();
|
handleGestures(peripherals->takeGesture());
|
||||||
if (!state) return false;
|
if (!state) return false;
|
||||||
unsigned long now = millis();
|
unsigned long now = millis();
|
||||||
float dt = (now - lastUpdate) / 1000.0f;
|
float dt = (now - lastUpdate) / 1000.0f;
|
||||||
lastUpdate = now;
|
lastUpdate = now;
|
||||||
state->updateImuOffsets(_peripherals->angleY(), _peripherals->angleX());
|
state->updateImuOffsets(peripherals->angleY(), peripherals->angleX());
|
||||||
state->step(body_state, dt);
|
state->step(body_state, dt);
|
||||||
kinematics.calculate_inverse_kinematics(body_state, new_angles);
|
kinematics.calculate_inverse_kinematics(body_state, new_angles);
|
||||||
|
|
||||||
@@ -147,9 +128,10 @@ class MotionService {
|
|||||||
|
|
||||||
float *getAngles() { return angles; }
|
float *getAngles() { return angles; }
|
||||||
|
|
||||||
|
inline bool isActive() { return state != nullptr; }
|
||||||
|
|
||||||
private:
|
private:
|
||||||
ServoController *_servoController;
|
ServoController *_servoController;
|
||||||
Peripherals *_peripherals;
|
|
||||||
Kinematics kinematics;
|
Kinematics kinematics;
|
||||||
|
|
||||||
CommandMsg command = {0, 0, 0, 0, 0, 0, 0};
|
CommandMsg command = {0, 0, 0, 0, 0, 0, 0};
|
||||||
|
|||||||
@@ -161,7 +161,7 @@ class GestureSensor {
|
|||||||
|
|
||||||
gesture_t getGesture() { return msg.gesture; }
|
gesture_t getGesture() { return msg.gesture; }
|
||||||
|
|
||||||
gesture_t takeGesture() {
|
gesture_t const takeGesture() {
|
||||||
const auto g = msg.gesture;
|
const auto g = msg.gesture;
|
||||||
msg.gesture = eGestureNone;
|
msg.gesture = eGestureNone;
|
||||||
return g;
|
return g;
|
||||||
|
|||||||
@@ -49,15 +49,15 @@ class Peripherals : public StatefulService<PeripheralsConfiguration> {
|
|||||||
_eventEndpoint.begin();
|
_eventEndpoint.begin();
|
||||||
_persistence.readFromFS();
|
_persistence.readFromFS();
|
||||||
|
|
||||||
socket.onEvent(EVENT_I2C_SCAN, [&](JsonVariant &root, int originId) {
|
// socket.onEvent(EVENT_I2C_SCAN, [&](JsonVariant &root, int originId) {
|
||||||
scanI2C();
|
// scanI2C();
|
||||||
emitI2C();
|
// emitI2C();
|
||||||
});
|
// });
|
||||||
|
|
||||||
socket.onSubscribe(EVENT_I2C_SCAN, [&](const String &originId, bool sync) {
|
// socket.onSubscribe(EVENT_I2C_SCAN, [&](const String &originId, bool sync) {
|
||||||
scanI2C();
|
// scanI2C();
|
||||||
emitI2C(originId, sync);
|
// emitI2C(originId, sync);
|
||||||
});
|
// });
|
||||||
|
|
||||||
updatePins();
|
updatePins();
|
||||||
|
|
||||||
@@ -79,6 +79,13 @@ class Peripherals : public StatefulService<PeripheralsConfiguration> {
|
|||||||
#endif
|
#endif
|
||||||
};
|
};
|
||||||
|
|
||||||
|
void update() {
|
||||||
|
readIMU();
|
||||||
|
readMag();
|
||||||
|
// _peripherals.readBMP();
|
||||||
|
EXECUTE_EVERY_N_MS(100, { readGesture(); });
|
||||||
|
}
|
||||||
|
|
||||||
void loop() {
|
void loop() {
|
||||||
EXECUTE_EVERY_N_MS(_updateInterval, {
|
EXECUTE_EVERY_N_MS(_updateInterval, {
|
||||||
beginTransaction();
|
beginTransaction();
|
||||||
@@ -112,7 +119,7 @@ class Peripherals : public StatefulService<PeripheralsConfiguration> {
|
|||||||
}
|
}
|
||||||
ESP_LOGI("Peripherals", "Emitting I2C scan results, %s %d", originId.c_str(), sync);
|
ESP_LOGI("Peripherals", "Emitting I2C scan results, %s %d", originId.c_str(), sync);
|
||||||
JsonVariant data = doc.as<JsonVariant>();
|
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) {
|
void scanI2C(uint8_t lower = 1, uint8_t higher = 127) {
|
||||||
@@ -197,7 +204,7 @@ class Peripherals : public StatefulService<PeripheralsConfiguration> {
|
|||||||
|
|
||||||
// float angleZ() { return _imu.getAngleZ(); }
|
// float angleZ() { return _imu.getAngleZ(); }
|
||||||
|
|
||||||
gesture_t takeGesture() {
|
gesture_t const takeGesture() {
|
||||||
return
|
return
|
||||||
#if FT_ENABLED(USE_PAJ7620U2)
|
#if FT_ENABLED(USE_PAJ7620U2)
|
||||||
_gesture.takeGesture();
|
_gesture.takeGesture();
|
||||||
@@ -225,7 +232,7 @@ class Peripherals : public StatefulService<PeripheralsConfiguration> {
|
|||||||
#endif
|
#endif
|
||||||
#if FT_ENABLED(USE_MPU6050 || USE_BNO055) || FT_ENABLED(USE_HMC5883)
|
#if FT_ENABLED(USE_MPU6050 || USE_BNO055) || FT_ENABLED(USE_HMC5883)
|
||||||
JsonVariant data = doc.as<JsonVariant>();
|
JsonVariant data = doc.as<JsonVariant>();
|
||||||
socket.emit(EVENT_IMU, data);
|
// socket.emit(EVENT_IMU, data);
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -235,7 +242,7 @@ class Peripherals : public StatefulService<PeripheralsConfiguration> {
|
|||||||
JsonArray root = doc.to<JsonArray>();
|
JsonArray root = doc.to<JsonArray>();
|
||||||
root[0] = _left_distance, root[1] = _right_distance;
|
root[0] = _left_distance, root[1] = _right_distance;
|
||||||
JsonVariant data = doc.as<JsonVariant>();
|
JsonVariant data = doc.as<JsonVariant>();
|
||||||
socket.emit("sonar", data);
|
// socket.emit("sonar", data);
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -32,16 +32,16 @@ class ServoController : public StatefulService<ServoSettings> {
|
|||||||
_persistence(ServoSettings::read, ServoSettings::update, this, SERVO_SETTINGS_FILE) {}
|
_persistence(ServoSettings::read, ServoSettings::update, this, SERVO_SETTINGS_FILE) {}
|
||||||
|
|
||||||
void begin() {
|
void begin() {
|
||||||
socket.onEvent(EVENT_SERVO_CONFIGURATION_SETTINGS,
|
// socket.onEvent(EVENT_SERVO_CONFIGURATION_SETTINGS,
|
||||||
[&](JsonVariant &root, int originId) { servoEvent(root, originId); });
|
// [&](JsonVariant &root, int originId) { servoEvent(root, originId); });
|
||||||
socket.onEvent(EVENT_SERVO_STATE, [&](JsonVariant &root, int originId) { stateUpdate(root, originId); });
|
// socket.onEvent(EVENT_SERVO_STATE, [&](JsonVariant &root, int originId) { stateUpdate(root, originId); });
|
||||||
_persistence.readFromFS();
|
_persistence.readFromFS();
|
||||||
|
|
||||||
initializePCA();
|
initializePCA();
|
||||||
socket.onEvent(EVENT_SERVO_STATE, [&](JsonVariant &root, int originId) {
|
// socket.onEvent(EVENT_SERVO_STATE, [&](JsonVariant &root, int originId) {
|
||||||
is_active = root["active"] | false;
|
// is_active = root["active"] | false;
|
||||||
is_active ? activate() : deactivate();
|
// is_active ? activate() : deactivate();
|
||||||
});
|
// });
|
||||||
}
|
}
|
||||||
|
|
||||||
void pcaWrite(int index, int value) {
|
void pcaWrite(int index, int value) {
|
||||||
@@ -110,7 +110,7 @@ class ServoController : public StatefulService<ServoSettings> {
|
|||||||
_pca.setMultiplePWM(pwms, 12);
|
_pca.setMultiplePWM(pwms, 12);
|
||||||
}
|
}
|
||||||
|
|
||||||
void updateServoState() {
|
void update() {
|
||||||
if (control_state == SERVO_CONTROL_STATE::ANGLE) calculatePWM();
|
if (control_state == SERVO_CONTROL_STATE::ANGLE) calculatePWM();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
|
||||||
@@ -5,7 +5,7 @@
|
|||||||
#include <PsychicHttp.h>
|
#include <PsychicHttp.h>
|
||||||
#include <WiFi.h>
|
#include <WiFi.h>
|
||||||
#include <task_manager.h>
|
#include <task_manager.h>
|
||||||
#include <event_socket.h>
|
// #include <event_socket.h>
|
||||||
#include <filesystem.h>
|
#include <filesystem.h>
|
||||||
#include <global.h>
|
#include <global.h>
|
||||||
|
|
||||||
|
|||||||
@@ -90,7 +90,7 @@ class FSPersistence {
|
|||||||
JsonStateReader<T> _stateReader;
|
JsonStateReader<T> _stateReader;
|
||||||
JsonStateUpdater<T> _stateUpdater;
|
JsonStateUpdater<T> _stateUpdater;
|
||||||
StatefulService<T> *_statefulService;
|
StatefulService<T> *_statefulService;
|
||||||
FS *_fs {&ESPFS};
|
FS *_fs {&ESP_FS};
|
||||||
const char *_filePath;
|
const char *_filePath;
|
||||||
size_t _bufferSize;
|
size_t _bufferSize;
|
||||||
HandlerId _updateHandlerId;
|
HandlerId _updateHandlerId;
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
#include <PsychicHttp.h>
|
#include <PsychicHttp.h>
|
||||||
|
|
||||||
#include <event_socket.h>
|
// #include <event_socket.h>
|
||||||
#include <template/stateful_service.h>
|
#include <template/stateful_service.h>
|
||||||
|
|
||||||
template <class T>
|
template <class T>
|
||||||
@@ -15,10 +15,10 @@ class EventEndpoint {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void begin() {
|
void begin() {
|
||||||
socket.onEvent(_event,
|
// socket.onEvent(_event,
|
||||||
std::bind(&EventEndpoint::updateState, this, std::placeholders::_1, std::placeholders::_2));
|
// std::bind(&EventEndpoint::updateState, this, std::placeholders::_1, std::placeholders::_2));
|
||||||
socket.onSubscribe(_event,
|
// socket.onSubscribe(_event,
|
||||||
std::bind(&EventEndpoint::syncState, this, std::placeholders::_1, std::placeholders::_2));
|
// std::bind(&EventEndpoint::syncState, this, std::placeholders::_1, std::placeholders::_2));
|
||||||
}
|
}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
@@ -35,6 +35,6 @@ class EventEndpoint {
|
|||||||
JsonDocument jsonDocument;
|
JsonDocument jsonDocument;
|
||||||
JsonVariant root = jsonDocument.to<JsonVariant>();
|
JsonVariant root = jsonDocument.to<JsonVariant>();
|
||||||
_statefulService->read(root, _stateReader);
|
_statefulService->read(root, _stateReader);
|
||||||
socket.emit(_event, root, originId.c_str(), sync);
|
// socket.emit(_event, root, originId.c_str(), sync);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
#pragma once
|
||||||
|
#include <PsychicHttp.h>
|
||||||
|
#include "WWWData.h"
|
||||||
|
|
||||||
|
void mountStaticAssets(PsychicHttpServer& s);
|
||||||
+112
-155
@@ -1,44 +1,49 @@
|
|||||||
# ESP32 SvelteKit --
|
from functools import lru_cache
|
||||||
#
|
|
||||||
# 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 pathlib import Path
|
from pathlib import Path
|
||||||
from shutil import copytree, rmtree, copyfileobj
|
from os.path import exists, getmtime, splitext
|
||||||
from os.path import exists, getmtime
|
|
||||||
import os
|
import os
|
||||||
import gzip
|
import gzip
|
||||||
import mimetypes
|
import mimetypes
|
||||||
import glob
|
import glob
|
||||||
from datetime import datetime
|
import zlib
|
||||||
|
|
||||||
Import("env")
|
Import("env")
|
||||||
|
|
||||||
project_dir = env["PROJECT_DIR"]
|
project_dir = env["PROJECT_DIR"]
|
||||||
buildFlags = env.ParseFlags(env["BUILD_FLAGS"])
|
buildFlags = env.ParseFlags(env["BUILD_FLAGS"])
|
||||||
|
|
||||||
interface_dir = project_dir + "/app"
|
interface_dir = f"{project_dir}/app"
|
||||||
output_file = project_dir + "/esp32/include/WWWData.h"
|
output_file = f"{project_dir}/esp32/include/WWWData.h"
|
||||||
source_www_dir = interface_dir + "/src"
|
source_www_dir = f"{interface_dir}/src"
|
||||||
build_dir = interface_dir + "/build"
|
build_dir = f"{interface_dir}/build"
|
||||||
filesystem_dir = project_dir + "/data/www"
|
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():
|
def get_files_to_exclude():
|
||||||
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")
|
print("Excluding Yertle files for SPOTMICRO_ESP32 build")
|
||||||
files_to_exclude.extend(["yertle.URDF", "URDF.zip", "URDF/"])
|
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")
|
print("Excluding Spot Micro files for SPOTMICRO_YERTLE build")
|
||||||
files_to_exclude.extend(["spot_micro.urdf.xacro", "stl.zip", "stl/"])
|
files_to_exclude.extend(["spot_micro.urdf.xacro", "stl.zip", "stl/"])
|
||||||
else:
|
else:
|
||||||
@@ -46,157 +51,109 @@ def get_files_to_exclude():
|
|||||||
return files_to_exclude
|
return files_to_exclude
|
||||||
|
|
||||||
|
|
||||||
def find_latest_timestamp_for_app():
|
def latest_ts():
|
||||||
return max((getmtime(f) for f in glob.glob(f"{source_www_dir}/**/*", recursive=True)))
|
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():
|
def needs_rebuild():
|
||||||
if not flag_exists("EMBED_WWW") or not exists(output_file):
|
if not exists(output_file):
|
||||||
return True
|
return True
|
||||||
last_source_change = find_latest_timestamp_for_app()
|
return getmtime(output_file) < latest_ts()
|
||||||
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
|
|
||||||
|
|
||||||
|
|
||||||
def gzip_file(file):
|
def pkg_mgr():
|
||||||
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"
|
|
||||||
if exists(os.path.join(interface_dir, "pnpm-lock.yaml")):
|
if exists(os.path.join(interface_dir, "pnpm-lock.yaml")):
|
||||||
return "pnpm"
|
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():
|
def build_web():
|
||||||
if package_manager := get_package_manager():
|
m = pkg_mgr()
|
||||||
print(f"Building interface with {package_manager}")
|
if not m:
|
||||||
|
raise Exception(
|
||||||
|
"No lock-file found. Please install dependencies for interface")
|
||||||
|
cwd = os.getcwd()
|
||||||
|
try:
|
||||||
os.chdir(interface_dir)
|
os.chdir(interface_dir)
|
||||||
env.Execute(f"{package_manager} install")
|
env.Execute(f"{m} install")
|
||||||
env.Execute(f"{package_manager} run build:embedded")
|
env.Execute(f"{m} run build:embedded")
|
||||||
os.chdir("..")
|
finally:
|
||||||
else:
|
os.chdir(cwd)
|
||||||
raise Exception("No lock-file found. Please install dependencies for interface (eg. npm install)")
|
|
||||||
|
|
||||||
|
|
||||||
def embed_webapp():
|
def encode_asset_data(path):
|
||||||
if flag_exists("EMBED_WWW"):
|
ext = splitext(path.name)[1].lower()
|
||||||
print("Converting interface to PROGMEM")
|
raw = path.read_bytes()
|
||||||
build_progmem()
|
if ext in already_compressed_ext:
|
||||||
return
|
return raw, 0, zlib.crc32(raw) & 0xFFFFFFFF
|
||||||
add_app_to_filesystem()
|
gz = gzip.compress(raw, mtime=0)
|
||||||
|
return gz, 1, zlib.crc32(gz) & 0xFFFFFFFF
|
||||||
|
|
||||||
|
|
||||||
def build_progmem():
|
def write_header():
|
||||||
mimetypes.init()
|
exclude = get_files_to_exclude()
|
||||||
with open(output_file, "w") as progmem:
|
assets = []
|
||||||
progmem.write("#include <functional>\n")
|
for p in sorted(Path(build_dir).rglob("*.*"), key=lambda x: x.relative_to(build_dir).as_posix()):
|
||||||
progmem.write("#include <Arduino.h>\n")
|
rel_path = p.relative_to(build_dir).as_posix()
|
||||||
|
if any(rel_path == ex or rel_path.startswith(ex.rstrip("/")) for ex in exclude):
|
||||||
assetMap = {}
|
|
||||||
|
|
||||||
files_to_exclude = get_files_to_exclude()
|
|
||||||
|
|
||||||
for idx, path in enumerate(Path(build_dir).rglob("*.*")):
|
|
||||||
asset_path = path.relative_to(build_dir).as_posix()
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
if should_exclude:
|
|
||||||
continue
|
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))
|
||||||
|
|
||||||
asset_mime = mimetypes.guess_type(asset_path)[0] or "application/octet-stream"
|
offsets, cursor = [], 0
|
||||||
print(f"Converting {asset_path}")
|
for _, _, data, _, _ in assets:
|
||||||
|
offsets.append(cursor)
|
||||||
|
cursor += len(data)
|
||||||
|
|
||||||
asset_var = f"ESP_SVELTEKIT_DATA_{idx}"
|
with open(output_file, "w", newline="\n") as f:
|
||||||
progmem.write(f"// {asset_path}\n")
|
f.write("#pragma once\n")
|
||||||
progmem.write(f"const uint8_t {asset_var}[] PROGMEM = {{\n\t")
|
f.write("#include <Arduino.h>\n\n")
|
||||||
file_data = gzip.compress(path.read_bytes())
|
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 i, byte in enumerate(file_data):
|
f.write("static const uint8_t WWW_BLOB[] PROGMEM = {\n")
|
||||||
if i and not (i % 16):
|
col = 0
|
||||||
progmem.write("\n\t")
|
for _, _, data, _, _ in assets:
|
||||||
progmem.write(f"0x{byte:02X},")
|
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")
|
||||||
|
|
||||||
progmem.write("\n};\n\n")
|
for i, (uri, _, _, _, _) in enumerate(assets):
|
||||||
assetMap[asset_path] = {
|
f.write(f'static const char WWW_URI_{i}[] PROGMEM = "{uri}";\n')
|
||||||
"name": asset_var,
|
for i, (_, mime, _, _, _) in enumerate(assets):
|
||||||
"mime": asset_mime,
|
f.write(f'static const char WWW_MIME_{i}[] PROGMEM = "{mime}";\n')
|
||||||
"size": len(file_data),
|
f.write("\n")
|
||||||
}
|
|
||||||
|
|
||||||
progmem.write(
|
f.write("static const WebAsset WWW_ASSETS[] PROGMEM = {\n")
|
||||||
"typedef std::function<void(const String& uri, const String& contentType, const uint8_t * content, size_t len)> RouteRegistrationHandler;\n\n"
|
for i, (_, _, data, gz_flag, etag) in enumerate(assets):
|
||||||
)
|
f.write(
|
||||||
progmem.write("class WWWData {\n")
|
f"\t{{WWW_URI_{i}, WWW_MIME_{i}, WWW_BLOB+{offsets[i]}, {len(data)}, 0x{etag:08X}, {gz_flag}}},\n")
|
||||||
progmem.write("\tpublic:\n")
|
f.write("};\n\n")
|
||||||
progmem.write("\t\tstatic void registerRoutes(RouteRegistrationHandler handler) {\n")
|
|
||||||
|
|
||||||
for asset_path, asset in assetMap.items():
|
f.write(f"static const size_t WWW_ASSETS_COUNT = {len(assets)};\n")
|
||||||
progmem.write(f'\t\t\thandler("/{asset_path}", "{asset["mime"]}", {asset["name"]}, {asset["size"]});\n')
|
default_uri = "/index.html" if any(u == "/index.html" for u,
|
||||||
|
_, _, _, _ in assets) else (assets[0][0] if assets else "/")
|
||||||
progmem.write("\t\t}\n")
|
f.write(
|
||||||
progmem.write("};\n\n")
|
f'static const WebOptions WWW_OPT = {{ "{default_uri}", 31536000u, 1 }};\n')
|
||||||
|
|
||||||
|
|
||||||
def add_app_to_filesystem():
|
if get_flag("EMBED_WEBAPP") == "1" and needs_rebuild():
|
||||||
build_path = Path(build_dir)
|
print("Building web app")
|
||||||
www_path = Path(filesystem_dir)
|
build_web()
|
||||||
if www_path.exists() and www_path.is_dir():
|
write_header()
|
||||||
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()
|
|
||||||
|
|||||||
@@ -2,12 +2,14 @@
|
|||||||
|
|
||||||
SemaphoreHandle_t clientSubscriptionsMutex = xSemaphoreCreateMutex();
|
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.onOpen((std::bind(&EventSocket::onWSOpen, this, std::placeholders::_1)));
|
||||||
_socket.onClose(std::bind(&EventSocket::onWSClose, 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));
|
_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) {
|
void EventSocket::onWSOpen(PsychicWebSocketClient *client) {
|
||||||
ESP_LOGI("EventSocket", "ws[%s][%u] connect", client->remoteIP().toString().c_str(), client->socket());
|
ESP_LOGI("EventSocket", "ws[%s][%u] connect", client->remoteIP().toString().c_str(), client->socket());
|
||||||
}
|
}
|
||||||
@@ -172,5 +174,3 @@ void EventSocket::onEvent(String event, EventCallback callback) {
|
|||||||
void EventSocket::onSubscribe(String event, SubscribeCallback callback) {
|
void EventSocket::onSubscribe(String event, SubscribeCallback callback) {
|
||||||
subscribe_callbacks[event].push_back(std::move(callback));
|
subscribe_callbacks[event].push_back(std::move(callback));
|
||||||
}
|
}
|
||||||
|
|
||||||
EventSocket socket;
|
|
||||||
@@ -28,8 +28,7 @@ void printFeatureConfiguration() {
|
|||||||
|
|
||||||
// Web services
|
// Web services
|
||||||
ESP_LOGI("Features", "USE_MDNS: %s", USE_MDNS ? "enabled" : "disabled");
|
ESP_LOGI("Features", "USE_MDNS: %s", USE_MDNS ? "enabled" : "disabled");
|
||||||
ESP_LOGI("Features", "EMBED_WWW: %s", EMBED_WWW ? "enabled" : "disabled");
|
ESP_LOGI("Features", "EMBED_WEBAPP: %s", EMBED_WEBAPP ? "enabled" : "disabled");
|
||||||
ESP_LOGI("Features", "ENABLE_CORS: %s", ENABLE_CORS ? "enabled" : "disabled");
|
|
||||||
ESP_LOGI("Features", "SERVE_CONFIG_FILES: %s", SERVE_CONFIG_FILES ? "enabled" : "disabled");
|
ESP_LOGI("Features", "SERVE_CONFIG_FILES: %s", SERVE_CONFIG_FILES ? "enabled" : "disabled");
|
||||||
ESP_LOGI("Features", "KINEMATICS_VARIANT: %s", KINEMATICS_VARIANT_STR);
|
ESP_LOGI("Features", "KINEMATICS_VARIANT: %s", KINEMATICS_VARIANT_STR);
|
||||||
ESP_LOGI("Features", "==========================================================");
|
ESP_LOGI("Features", "==========================================================");
|
||||||
@@ -48,8 +47,7 @@ void features(JsonObject &root) {
|
|||||||
root["servo"] = USE_PCA9685 ? true : false;
|
root["servo"] = USE_PCA9685 ? true : false;
|
||||||
root["ws2812"] = USE_WS2812 ? true : false;
|
root["ws2812"] = USE_WS2812 ? true : false;
|
||||||
root["mdns"] = USE_MDNS ? true : false;
|
root["mdns"] = USE_MDNS ? true : false;
|
||||||
root["embed_www"] = EMBED_WWW ? true : false;
|
root["embed_www"] = EMBED_WEBAPP ? true : false;
|
||||||
root["enable_cors"] = ENABLE_CORS ? true : false;
|
|
||||||
root["serve_config_files"] = SERVE_CONFIG_FILES ? true : false;
|
root["serve_config_files"] = SERVE_CONFIG_FILES ? true : false;
|
||||||
root["firmware_version"] = APP_VERSION;
|
root["firmware_version"] = APP_VERSION;
|
||||||
root["firmware_name"] = APP_NAME;
|
root["firmware_name"] = APP_NAME;
|
||||||
|
|||||||
@@ -40,10 +40,10 @@ esp_err_t handleEdit(PsychicRequest *request, JsonVariant &json) {
|
|||||||
|
|
||||||
/* Helpers */
|
/* 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) {
|
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 "{}";
|
if (!root.isDirectory()) return "{}";
|
||||||
|
|
||||||
File file = root.openNextFile();
|
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) {
|
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;
|
if (!file) return false;
|
||||||
|
|
||||||
file.print(content);
|
file.print(content);
|
||||||
@@ -106,7 +106,7 @@ bool editFile(const char *filename, const char *content) {
|
|||||||
esp_err_t mkdir(PsychicRequest *request, JsonVariant &json) {
|
esp_err_t mkdir(PsychicRequest *request, JsonVariant &json) {
|
||||||
const char *path = json["path"].as<const char *>();
|
const char *path = json["path"].as<const char *>();
|
||||||
ESP_LOGI(TAG, "Creating directory: %s", path);
|
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
|
} // namespace FileSystem
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
@@ -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
@@ -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");
|
ESP_LOGI("main", "Setup complete now runing tsk");
|
||||||
TickType_t xLastWakeTime = xTaskGetTickCount();
|
TickType_t xLastWakeTime = xTaskGetTickCount();
|
||||||
const TickType_t xFrequency = 5 / portTICK_PERIOD_MS;
|
const TickType_t xFrequency = 5 / portTICK_PERIOD_MS;
|
||||||
|
|
||||||
|
peripherals.begin();
|
||||||
|
servoController.begin();
|
||||||
|
motionService.begin();
|
||||||
|
|
||||||
for (;;) {
|
for (;;) {
|
||||||
spot.readSensors();
|
CALLS_PER_SECOND(SpotControlLoopEntry);
|
||||||
spot.planMotion();
|
peripherals.update();
|
||||||
spot.updateActuators();
|
motionService.update(&peripherals);
|
||||||
spot.emitTelemetry();
|
servoController.setAngles(motionService.getAngles());
|
||||||
|
motionService.isActive() ? servoController.activate() : servoController.deactivate();
|
||||||
|
servoController.update();
|
||||||
|
#if FT_ENABLED(USE_WS2812)
|
||||||
|
ledService.loop();
|
||||||
|
#endif
|
||||||
vTaskDelayUntil(&xLastWakeTime, xFrequency);
|
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() {
|
void setup() {
|
||||||
Serial.begin(115200);
|
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); }
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -38,12 +38,12 @@ esp_err_t getMetrics(PsychicRequest *request) {
|
|||||||
|
|
||||||
void reset() {
|
void reset() {
|
||||||
ESP_LOGI(TAG, "Resetting device");
|
ESP_LOGI(TAG, "Resetting device");
|
||||||
File root = ESPFS.open(FS_CONFIG_DIRECTORY);
|
File root = ESP_FS.open(FS_CONFIG_DIRECTORY);
|
||||||
File file;
|
File file;
|
||||||
while (file = root.openNextFile()) {
|
while (file = root.openNextFile()) {
|
||||||
String path = file.path();
|
String path = file.path();
|
||||||
file.close();
|
file.close();
|
||||||
ESPFS.remove(path);
|
ESP_FS.remove(path);
|
||||||
}
|
}
|
||||||
restart();
|
restart();
|
||||||
}
|
}
|
||||||
@@ -106,8 +106,8 @@ void status(JsonObject &root) {
|
|||||||
root["arduino_version"] = ARDUINO_VERSION;
|
root["arduino_version"] = ARDUINO_VERSION;
|
||||||
root["flash_chip_size"] = ESP.getFlashChipSize();
|
root["flash_chip_size"] = ESP.getFlashChipSize();
|
||||||
root["flash_chip_speed"] = ESP.getFlashChipSpeed();
|
root["flash_chip_speed"] = ESP.getFlashChipSpeed();
|
||||||
root["fs_total"] = ESPFS.totalBytes();
|
root["fs_total"] = ESP_FS.totalBytes();
|
||||||
root["fs_used"] = ESPFS.usedBytes();
|
root["fs_used"] = ESP_FS.usedBytes();
|
||||||
root["core_temp"] = temperatureRead();
|
root["core_temp"] = temperatureRead();
|
||||||
root["cpu_reset_reason"] = resetReason(esp_reset_reason());
|
root["cpu_reset_reason"] = resetReason(esp_reset_reason());
|
||||||
root["uptime"] = millis() / 1000;
|
root["uptime"] = millis() / 1000;
|
||||||
@@ -119,8 +119,8 @@ void metrics(JsonObject &root) {
|
|||||||
root["total_heap"] = ESP.getHeapSize();
|
root["total_heap"] = ESP.getHeapSize();
|
||||||
root["min_free_heap"] = ESP.getMinFreeHeap();
|
root["min_free_heap"] = ESP.getMinFreeHeap();
|
||||||
root["max_alloc_heap"] = ESP.getMaxAllocHeap();
|
root["max_alloc_heap"] = ESP.getMaxAllocHeap();
|
||||||
root["fs_used"] = ESPFS.usedBytes();
|
root["fs_used"] = ESP_FS.usedBytes();
|
||||||
root["fs_total"] = ESPFS.totalBytes();
|
root["fs_total"] = ESP_FS.totalBytes();
|
||||||
root["core_temp"] = temperatureRead();
|
root["core_temp"] = temperatureRead();
|
||||||
root["cpu0_usage"] = g_taskManager.getCpuUsage(0);
|
root["cpu0_usage"] = g_taskManager.getCpuUsage(0);
|
||||||
root["cpu1_usage"] = g_taskManager.getCpuUsage(1);
|
root["cpu1_usage"] = g_taskManager.getCpuUsage(1);
|
||||||
@@ -136,12 +136,12 @@ void metrics(JsonObject &root) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void emitMetrics() {
|
void emitMetrics() {
|
||||||
if (!socket.hasSubscribers(EVENT_ANALYTICS)) return;
|
// if (!socket.hasSubscribers(EVENT_ANALYTICS)) return;
|
||||||
analyticsDoc.clear();
|
// analyticsDoc.clear();
|
||||||
JsonObject root = analyticsDoc.to<JsonObject>();
|
// JsonObject root = analyticsDoc.to<JsonObject>();
|
||||||
system_service::metrics(root);
|
// system_service::metrics(root);
|
||||||
JsonVariant data = analyticsDoc.as<JsonVariant>();
|
// JsonVariant data = analyticsDoc.as<JsonVariant>();
|
||||||
socket.emit(EVENT_ANALYTICS, data);
|
// socket.emit(EVENT_ANALYTICS, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
const char *resetReason(esp_reset_reason_t reason) {
|
const char *resetReason(esp_reset_reason_t reason) {
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user