Merge pull request #1 from cjkas/scz/opti

Scz/opti
This commit is contained in:
cjkas 2026-03-24 20:25:56 +01:00 committed by GitHub
commit 6a28589d29
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
53 changed files with 3253 additions and 3640 deletions

View file

@ -2,150 +2,44 @@ name: ESPSomfy-RTS
on: [push, pull_request]
env:
ARDUINO_BOARD_MANAGER_ADDITIONAL_URLS: "https://raw.githubusercontent.com/espressif/arduino-esp32/gh-pages/package_esp32_dev_index.json"
ARDUINO_CLI_VERSION: "0.x"
ARDUINO_ESP32_VERSION: "2.0.10"
ARDUINO_JSON_VERSION: "6.21.3"
ESPTOOL_VERSION: "4.6"
LITTLEFS_VERSION: "v2.5.1"
MKLITTLEFS_VERSION: "3.1.0"
PUB_SUB_CLIENT_VERSION: "2.8.0"
PYTHON_VERSION: "3.10"
SMARTRC_CC1101_VERSION: "2.5.7"
WEB_SOCKET_VERSION: "2.4.0"
jobs:
littlefs:
name: LittleFS
runs-on: ubuntu-latest
steps:
- name: Check out code
uses: actions/checkout@v3
- name: Checkout mklittlefs
uses: actions/checkout@v3
with:
repository: earlephilhower/mklittlefs
path: mklittlefs
ref: ${{ env.MKLITTLEFS_VERSION }}
- name: Checkout LittleFS
uses: actions/checkout@v3
with:
repository: littlefs-project/littlefs
path: mklittlefs/littlefs
ref: ${{ env.LITTLEFS_VERSION }}
- name: Build mklittlefs
run: |
make -C mklittlefs
- name: Create LittleFS
run: |
./mklittlefs/mklittlefs --create data --size 1441792 SomfyController.littlefs.bin
- name: Upload binaries
uses: actions/upload-artifact@v3
with:
name: LittleFS
path: SomfyController.littlefs.bin
retention-days: 5
arduino:
build:
name: ${{ matrix.name }}
needs: [littlefs]
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
include:
- board: esp32
addr_bootloader: 0x1000
chip: ESP32
fqbn: esp32:esp32:esp32
- env: esp32dev
name: ESP32
- board: lolin_c3_mini
addr_bootloader: 0x0
chip: ESP32-C3
fqbn: esp32:esp32:lolin_c3_mini
name: LOLIN-C3-mini
- board: lolin_s2_mini
addr_bootloader: 0x1000
chip: ESP32-S2
fqbn: esp32:esp32:lolin_s2_mini
name: LOLIN-S2-mini
- board: lolin_s3_mini
addr_bootloader: 0x0
chip: ESP32-S3
fqbn: esp32:esp32:lolin_s3_mini
name: LOLIN-S3-mini
steps:
- name: Check out code
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
path: SomfyController
python-version: "3.12"
- name: Get LittleFS
uses: actions/download-artifact@v3
with:
name: LittleFS
- name: Install Python ${{ env.PYTHON_VERSION }}
uses: actions/setup-python@v4
with:
python-version: ${{ env.PYTHON_VERSION }}
- name: Upgrade pip
run: |
python -m pip install --upgrade pip
pip --version
- name: Install ESPTool
run: |
pip install esptool==${{ env.ESPTOOL_VERSION }}
- name: Install Arduino CLI
uses: arduino/setup-arduino-cli@v1
with:
version: ${{ env.ARDUINO_CLI_VERSION }}
- name: Configure Arduino CLI
run: |
arduino-cli core update-index
arduino-cli core install esp32:esp32@${{ env.ARDUINO_ESP32_VERSION }}
- name: Configure Arduino Libraries
run: |
arduino-cli lib install ArduinoJson@${{ env.ARDUINO_JSON_VERSION }}
arduino-cli lib install PubSubClient@${{ env.PUB_SUB_CLIENT_VERSION }}
arduino-cli lib install SmartRC-CC1101-Driver-Lib@${{ env.SMARTRC_CC1101_VERSION }}
arduino-cli lib install WebSockets@${{ env.WEB_SOCKET_VERSION }}
- name: Install PlatformIO
run: pip install platformio
- name: Build ${{ matrix.name }}
run: |
mkdir -p build
arduino-cli compile --clean --output-dir build --fqbn ${{ matrix.fqbn }} --warnings default ./SomfyController
run: pio run -e ${{ matrix.env }}
- name: ${{ matrix.name }} Image
run: |
python -m esptool --chip ${{ matrix.chip }} \
merge_bin -o build/SomfyController.onboard.bin \
${{ matrix.addr_bootloader }} build/SomfyController.ino.bootloader.bin \
0x8000 build/SomfyController.ino.partitions.bin \
0x10000 build/SomfyController.ino.bin \
0x290000 SomfyController.littlefs.bin
- name: Build LittleFS image
run: pio run -e ${{ matrix.env }} -t buildfs
- name: Upload ${{ matrix.name }}
uses: actions/upload-artifact@v3
- name: Upload firmware
uses: actions/upload-artifact@v4
with:
name: ${{ matrix.name }}
path: |
build/SomfyController.ino.bin
build/SomfyController.ino.bootloader.bin
build/SomfyController.ino.partitions.bin
build/SomfyController.onboard.bin
.pio/build/${{ matrix.env }}/firmware.bin
.pio/build/${{ matrix.env }}/firmware.elf
.pio/build/${{ matrix.env }}/partitions.bin
.pio/build/${{ matrix.env }}/bootloader.bin
.pio/build/${{ matrix.env }}/littlefs.bin
retention-days: 5

View file

@ -4,24 +4,23 @@ on:
release:
types: [published]
env:
ARDUINO_BOARD_MANAGER_ADDITIONAL_URLS: "https://raw.githubusercontent.com/espressif/arduino-esp32/gh-pages/package_esp32_index.json"
ARDUINO_CLI_VERSION: "0.x"
ARDUINO_ESP32_VERSION: "2.0.17"
ARDUINO_JSON_VERSION: "6.21.5"
ESPTOOL_VERSION: "4.7"
LITTLEFS_VERSION: "v2.5.1"
MKLITTLEFS_VERSION: "3.1.0"
PUB_SUB_CLIENT_VERSION: "2.8.0"
PYTHON_VERSION: "3.10"
SMARTRC_CC1101_VERSION: "2.5.7"
WEB_SOCKET_VERSION: "2.4.0"
jobs:
littlefs:
name: LittleFS
build:
permissions: write-all
name: ${{ matrix.name }}
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
include:
- env: esp32dev
name: ESP32
chip: ESP32
addr_bootloader: "0x1000"
fwname: SomfyController.esp32.bin
obname: SomfyController.onboard.esp32.bin
steps:
- name: Get Release
id: get_release
@ -32,34 +31,31 @@ jobs:
- name: Check out code
uses: actions/checkout@v4
- name: Checkout mklittlefs
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
repository: earlephilhower/mklittlefs
path: mklittlefs
ref: ${{ env.MKLITTLEFS_VERSION }}
python-version: "3.12"
- name: Checkout LittleFS
uses: actions/checkout@v4
with:
repository: littlefs-project/littlefs
path: mklittlefs/littlefs
ref: ${{ env.LITTLEFS_VERSION }}
- name: Install PlatformIO
run: pip install platformio
- name: Build mklittlefs
- name: Build firmware
run: pio run -e ${{ matrix.env }}
- name: Build LittleFS image
run: pio run -e ${{ matrix.env }} -t buildfs
- name: Create onboard image
run: |
make -C mklittlefs
python -m esptool --chip ${{ matrix.chip }} \
merge_bin -o ${{ matrix.obname }} \
${{ matrix.addr_bootloader }} .pio/build/${{ matrix.env }}/bootloader.bin \
0x8000 .pio/build/${{ matrix.env }}/partitions.bin \
0x10000 .pio/build/${{ matrix.env }}/firmware.bin \
0x310000 .pio/build/${{ matrix.env }}/littlefs.bin
- name: Create LittleFS
run: |
./mklittlefs/mklittlefs --create data --size 1441792 SomfyController.littlefs.bin
- name: Upload binaries
uses: actions/upload-artifact@v4
with:
name: LittleFS
path: SomfyController.littlefs.bin
retention-days: 5
- name: Compress onboard image
run: zip ${{ matrix.obname }}.zip ${{ matrix.obname }}
- name: Upload LittleFS
uses: shogo82148/actions-upload-release-asset@v1.7.5
@ -67,129 +63,20 @@ jobs:
github_token: ${{ github.token }}
upload_url: ${{ steps.get_release.outputs.upload_url }}
asset_name: SomfyController.littlefs.bin
asset_path: SomfyController.littlefs.bin
asset_path: .pio/build/${{ matrix.env }}/littlefs.bin
overwrite: true
arduino:
permissions: write-all
name: ${{ matrix.name }}
needs: [littlefs]
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
include:
- board: esp32
addr_bootloader: 0x1000
chip: ESP32
fqbn: esp32:esp32:esp32wrover:PartitionScheme=default,FlashMode=qio,FlashFreq=80,UploadSpeed=921600,DebugLevel=none,EraseFlash=none
# esp32:esp32:esp32wrover:PartitionScheme=default,FlashMode=qio,FlashFreq=80,UploadSpeed=921600,DebugLevel=none,EraseFlash=none
name: ESP32
obname: SomfyController.onboard.esp32.bin
fwname: SomfyController.ino.esp32.bin
- board: esp32c3
addr_bootloader: 0x0
chip: ESP32-C3
fqbn: esp32:esp32:esp32c3:JTAGAdapter=default,CDCOnBoot=cdc,PartitionScheme=default,CPUFreq=160,FlashMode=qio,FlashFreq=80,FlashSize=4M,UploadSpeed=921600,DebugLevel=none,EraseFlash=none
# esp32:esp32:esp32c3:JTAGAdapter=default,CDCOnBoot=default,PartitionScheme=default,CPUFreq=160,FlashMode=qio,FlashFreq=80,FlashSize=4M,UploadSpeed=921600,DebugLevel=none,EraseFlash=none
name: ESP32C3
obname: SomfyController.onboard.esp32c3.bin
fwname: SomfyController.ino.esp32c3.bin
- board: esp32s2
addr_bootloader: 0x1000
chip: ESP32-S2
fqbn: esp32:esp32:esp32s2:JTAGAdapter=default,CDCOnBoot=cdc,MSCOnBoot=default,DFUOnBoot=default,UploadMode=default,PSRAM=disabled,PartitionScheme=default,CPUFreq=240,FlashMode=qio,FlashFreq=80,FlashSize=4M,UploadSpeed=921600,DebugLevel=none,EraseFlash=none
# esp32:esp32:esp32s2:JTAGAdapter=default,CDCOnBoot=default,MSCOnBoot=default,DFUOnBoot=default,UploadMode=default,PSRAM=disabled,PartitionScheme=default,CPUFreq=240,FlashMode=qio,FlashFreq=80,FlashSize=4M,UploadSpeed=921600,DebugLevel=none,EraseFlash=none
name: ESP32S2
obname: SomfyController.onboard.esp32s2.bin
fwname: SomfyController.ino.esp32s2.bin
- board: esp32s3
addr_bootloader: 0x0
chip: ESP32-S3
fqbn: esp32:esp32:esp32s3:JTAGAdapter=default,PSRAM=disabled,FlashMode=qio,FlashSize=4M,LoopCore=1,EventsCore=1,USBMode=hwcdc,CDCOnBoot=cdc,MSCOnBoot=default,DFUOnBoot=default,UploadMode=default,PartitionScheme=default,CPUFreq=240,UploadSpeed=921600,DebugLevel=none,EraseFlash=none
# esp32:esp32:esp32s3:JTAGAdapter=default,PSRAM=disabled,FlashMode=qio,FlashSize=4M,LoopCore=1,EventsCore=1,USBMode=hwcdc,CDCOnBoot=cdc,MSCOnBoot=default,DFUOnBoot=default,UploadMode=default,PartitionScheme=default,CPUFreq=240,UploadSpeed=921600,DebugLevel=none,EraseFlash=none
name: ESP32S3
fwname: SomfyController.ino.esp32s3.bin
obname: SomfyController.onboard.esp32s3.bin
steps:
- name: Get Release
id: get_release
uses: bruceadams/get-release@v1.3.2
env:
GITHUB_TOKEN: ${{ github.token }}
- name: Check out code
uses: actions/checkout@v4
with:
path: SomfyController
- name: Get LittleFS
uses: actions/download-artifact@v4
with:
name: LittleFS
- name: Install Python ${{ env.PYTHON_VERSION }}
uses: actions/setup-python@v5
with:
python-version: ${{ env.PYTHON_VERSION }}
- name: Upgrade pip
run: |
python -m pip install --upgrade pip
pip --version
- name: Install ESPTool
run: |
pip install esptool==${{ env.ESPTOOL_VERSION }}
- name: Install Arduino CLI
uses: arduino/setup-arduino-cli@v1
with:
version: ${{ env.ARDUINO_CLI_VERSION }}
- name: Configure Arduino CLI
run: |
arduino-cli core update-index
arduino-cli core install esp32:esp32@${{ env.ARDUINO_ESP32_VERSION }}
- name: Configure Arduino Libraries
run: |
arduino-cli lib install ArduinoJson@${{ env.ARDUINO_JSON_VERSION }}
arduino-cli lib install PubSubClient@${{ env.PUB_SUB_CLIENT_VERSION }}
arduino-cli lib install SmartRC-CC1101-Driver-Lib@${{ env.SMARTRC_CC1101_VERSION }}
arduino-cli lib install WebSockets@${{ env.WEB_SOCKET_VERSION }}
- name: Build ${{ matrix.name }}
run: |
mkdir -p build${{ matrix.name }}
arduino-cli compile --clean --output-dir build${{ matrix.name }} --fqbn ${{ matrix.fqbn }} --warnings none ./SomfyController
- name: ${{ matrix.name }} Image
run: |
python -m esptool --chip ${{ matrix.chip }} \
merge_bin -o ${{ matrix.obname }} \
${{ matrix.addr_bootloader }} build${{ matrix.name }}/SomfyController.ino.bootloader.bin \
0x8000 build${{ matrix.name }}/SomfyController.ino.partitions.bin \
0x10000 build${{ matrix.name }}/SomfyController.ino.bin \
0x290000 SomfyController.littlefs.bin
- name: Upload Firmware ${{ matrix.name }}
- name: Upload firmware
uses: shogo82148/actions-upload-release-asset@v1.7.5
with:
github_token: ${{ github.token }}
upload_url: ${{ steps.get_release.outputs.upload_url }}
asset_name: ${{ matrix.fwname }}
asset_path: build${{ matrix.name }}/SomfyController.ino.bin
asset_path: .pio/build/${{ matrix.env }}/firmware.bin
overwrite: true
- name: ${{ matrix.name }} Compress Onboard Image
run: |
zip ${{ matrix.obname }}.zip ./${{ matrix.obname }}
- name: Upload Onboard ${{ matrix.name }}
- name: Upload onboard image
uses: shogo82148/actions-upload-release-asset@v1.7.5
# env:
# GITHUB_TOKEN: ${{ github.token }}
with:
github_token: ${{ github.token }}
upload_url: ${{ steps.get_release.outputs.upload_url }}
@ -197,4 +84,3 @@ jobs:
asset_path: ${{ matrix.obname }}.zip
overwrite: true
asset_content_type: application/zip

9
.gitignore vendored
View file

@ -7,4 +7,11 @@ debug.cfg
SomfyController.ino.XIAO_ESP32S3.bin
SomfyController.ino.esp32c3.bin
SomfyController.ino.esp32s2.bin
.vscode/settings.json
.vscode/
.pio/
.claude/
data/
build/
coredump_report.txt
coredump.bin
logs/

View file

@ -82,3 +82,9 @@ Configuration of the Transceiver is done with the ELECHOUSE_CC1101 library which
pio pkg exec -p tool-esptoolpy -- esptool.py --port COM9 read_flash 0x3F0000 0x10000 coredump.bin
C:\Users\oem\.platformio\packages\framework-espidf\export.ps1
esp-coredump info_corefile --core coredump.bin --core-format=raw --gdb C:\Users\oem\.platformio\packages\toolchain-xtensa-esp32\bin\xtensa-esp32-elf-gdb.exe .pio\build\esp32dev\firmware.elf > coredump_report.txt

View file

@ -1,203 +0,0 @@
#include <Arduino.h>
#include <ArduinoJson.h>
#include <WebSocketsServer.h>
#include <esp_task_wdt.h>
#include "Sockets.h"
#include "ConfigSettings.h"
#include "Somfy.h"
#include "Network.h"
#include "GitOTA.h"
extern ConfigSettings settings;
extern Network net;
extern SomfyShadeController somfy;
extern SocketEmitter sockEmit;
extern GitUpdater git;
WebSocketsServer sockServer = WebSocketsServer(8080);
#define MAX_SOCK_RESPONSE 2048
static char g_response[MAX_SOCK_RESPONSE];
bool room_t::isJoined(uint8_t num) {
for(uint8_t i = 0; i < sizeof(this->clients); i++) {
if(this->clients[i] == num) return true;
}
return false;
}
bool room_t::join(uint8_t num) {
if(this->isJoined(num)) return true;
for(uint8_t i = 0; i < sizeof(this->clients); i++) {
if(this->clients[i] == 255) {
this->clients[i] = num;
return true;
}
}
return false;
}
bool room_t::leave(uint8_t num) {
if(!this->isJoined(num)) return false;
for(uint8_t i = 0; i < sizeof(this->clients); i++) {
if(this->clients[i] == num) this->clients[i] = 255;
}
return true;
}
void room_t::clear() {
memset(this->clients, 255, sizeof(this->clients));
}
uint8_t room_t::activeClients() {
uint8_t n = 0;
for(uint8_t i = 0; i < sizeof(this->clients); i++) {
if(this->clients[i] != 255) n++;
}
return n;
}
/*********************************************************************
* ClientSocketEvent class members
********************************************************************/
/*
void ClientSocketEvent::prepareMessage(const char *evt, const char *payload) {
if(strlen(payload) + 5 >= sizeof(this->msg)) Serial.printf("Socket buffer overflow %d > 2048\n", strlen(payload) + 5 + strlen(evt));
snprintf(this->msg, sizeof(this->msg), "42[%s,%s]", evt, payload);
}
void ClientSocketEvent::prepareMessage(const char *evt, JsonDocument &doc) {
memset(this->msg, 0x00, sizeof(this->msg));
snprintf(this->msg, sizeof(this->msg), "42[%s,", evt);
serializeJson(doc, &this->msg[strlen(this->msg)], sizeof(this->msg) - strlen(this->msg) - 2);
strcat(this->msg, "]");
}
*/
/*********************************************************************
* SocketEmitter class members
********************************************************************/
void SocketEmitter::startup() {
}
void SocketEmitter::begin() {
sockServer.begin();
sockServer.enableHeartbeat(20000, 10000, 3);
sockServer.onEvent(this->wsEvent);
Serial.println("Socket Server Started...");
//settings.printAvailHeap();
}
void SocketEmitter::loop() {
this->initClients();
sockServer.loop();
}
JsonSockEvent *SocketEmitter::beginEmit(const char *evt) {
this->json.beginEvent(&sockServer, evt, g_response, sizeof(g_response));
return &this->json;
}
void SocketEmitter::endEmit(uint8_t num) { this->json.endEvent(num); sockServer.loop(); }
void SocketEmitter::endEmitRoom(uint8_t room) {
if(room < SOCK_MAX_ROOMS) {
room_t *r = &this->rooms[room];
for(uint8_t i = 0; i < sizeof(r->clients); i++) {
if(r->clients[i] != 255) this->json.endEvent(r->clients[i]);
}
}
}
uint8_t SocketEmitter::activeClients(uint8_t room) {
if(room < SOCK_MAX_ROOMS) return this->rooms[room].activeClients();
return 0;
}
void SocketEmitter::initClients() {
for(uint8_t i = 0; i < sizeof(this->newClients); i++) {
uint8_t num = this->newClients[i];
if(num != 255) {
if(sockServer.clientIsConnected(num)) {
Serial.printf("Initializing Socket Client %u\n", num);
esp_task_wdt_reset();
settings.emitSockets(num);
somfy.emitState(num);
git.emitUpdateCheck(num);
net.emitSockets(num);
esp_task_wdt_reset();
}
this->newClients[i] = 255;
}
}
}
void SocketEmitter::delayInit(uint8_t num) {
for(uint8_t i=0; i < sizeof(this->newClients); i++) {
if(this->newClients[i] == num) break;
else if(this->newClients[i] == 255) {
this->newClients[i] = num;
break;
}
}
}
void SocketEmitter::end() {
sockServer.close();
for(uint8_t i = 0; i < SOCK_MAX_ROOMS; i++)
this->rooms[i].clear();
}
void SocketEmitter::disconnect() { sockServer.disconnect(); }
void SocketEmitter::wsEvent(uint8_t num, WStype_t type, uint8_t *payload, size_t length) {
switch(type) {
case WStype_ERROR:
if(length > 0)
Serial.printf("Socket Error: %s\n", payload);
else
Serial.println("Socket Error: \n");
break;
case WStype_DISCONNECTED:
if(length > 0)
Serial.printf("Socket [%u] Disconnected!\n [%s]", num, payload);
else
Serial.printf("Socket [%u] Disconnected!\n", num);
for(uint8_t i = 0; i < SOCK_MAX_ROOMS; i++) {
sockEmit.rooms[i].leave(num);
}
break;
case WStype_CONNECTED:
{
IPAddress ip = sockServer.remoteIP(num);
Serial.printf("Socket [%u] Connected from %d.%d.%d.%d url: %s\n", num, ip[0], ip[1], ip[2], ip[3], payload);
// Send all the current shade settings to the client.
sockServer.sendTXT(num, "Connected");
//sockServer.loop();
sockEmit.delayInit(num);
}
break;
case WStype_TEXT:
if(strncmp((char *)payload, "join:", 5) == 0) {
// In this instance the client wants to join a room. Let's do some
// work to get the ordinal of the room that the client wants to join.
uint8_t roomNum = atoi((char *)&payload[5]);
Serial.printf("Client %u joining room %u\n", num, roomNum);
if(roomNum < SOCK_MAX_ROOMS) sockEmit.rooms[roomNum].join(num);
}
else if(strncmp((char *)payload, "leave:", 6) == 0) {
uint8_t roomNum = atoi((char *)&payload[6]);
Serial.printf("Client %u leaving room %u\n", num, roomNum);
if(roomNum < SOCK_MAX_ROOMS) sockEmit.rooms[roomNum].leave(num);
}
else {
Serial.printf("Socket [%u] text: %s\n", num, payload);
}
// send message to client
// webSocket.sendTXT(num, "message here");
// send data to all connected clients
// sockServer.broadcastTXT("message here");
break;
case WStype_BIN:
Serial.printf("[%u] get binary length: %u\n", num, length);
//hexdump(payload, length);
// send message to client
// sockServer.sendBIN(num, payload, length);
break;
case WStype_PONG:
//Serial.printf("Pong from %u\n", num);
break;
case WStype_PING:
//Serial.printf("Ping from %u\n", num);
break;
default:
break;
}
}

2733
Web.cpp

File diff suppressed because it is too large Load diff

50
Web.h
View file

@ -1,50 +0,0 @@
#include <WebServer.h>
#include "Somfy.h"
#ifndef webserver_h
#define webserver_h
class Web {
public:
bool uploadSuccess = false;
void sendCORSHeaders(WebServer &server);
void sendCacheHeaders(uint32_t seconds=604800);
void startup();
void handleLogin(WebServer &server);
void handleLogout(WebServer &server);
void handleStreamFile(WebServer &server, const char *filename, const char *encoding);
void handleController(WebServer &server);
void handleLoginContext(WebServer &server);
void handleGetRepeaters(WebServer &server);
void handleGetRooms(WebServer &server);
void handleGetShades(WebServer &server);
void handleGetGroups(WebServer &server);
void handleShadeCommand(WebServer &server);
void handleRepeatCommand(WebServer &server);
void handleGroupCommand(WebServer &server);
void handleTiltCommand(WebServer &server);
void handleDiscovery(WebServer &server);
void handleNotFound(WebServer &server);
void handleRoom(WebServer &server);
void handleShade(WebServer &server);
void handleGroup(WebServer &server);
void handleSetPositions(WebServer &server);
void handleSetSensor(WebServer &server);
void handleDownloadFirmware(WebServer &server);
void handleBackup(WebServer &server, bool attach = false);
void handleReboot(WebServer &server);
void handleDeserializationError(WebServer &server, DeserializationError &err);
void begin();
void loop();
void end();
// Web Handlers
bool createAPIToken(const IPAddress ipAddress, char *token);
bool createAPIToken(const char *payload, char *token);
bool createAPIPinToken(const IPAddress ipAddress, const char *pin, char *token);
bool createAPIPasswordToken(const IPAddress ipAddress, const char *username, const char *password, char *token);
bool isAuthenticated(WebServer &server, bool cfg = false);
//void chunkRoomsResponse(WebServer &server, const char *elem = nullptr);
//void chunkShadesResponse(WebServer &server, const char *elem = nullptr);
//void chunkGroupsResponse(WebServer &server, const char *elem = nullptr);
//void chunkGroupResponse(WebServer &server, SomfyGroup *, const char *prefix = nullptr);
};
#endif

View file

Before

Width:  |  Height:  |  Size: 6.5 KiB

After

Width:  |  Height:  |  Size: 6.5 KiB

Before After
Before After

1
data-src/appversion Normal file
View file

@ -0,0 +1 @@
2.4.8

View file

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 24 KiB

Before After
Before After

View file

@ -8,9 +8,9 @@
<meta name="apple-mobile-web-app-title" content="ESPSomfy RTS App">
<meta name="apple-mobile-web-app-status-bar-style" content="black">
<link rel="stylesheet" href="main.css?v=2.4.7c" type="text/css" />
<link rel="stylesheet" href="widgets.css?v=2.4.7c" type="text/css" />
<link rel="stylesheet" href="icons.css?v=2.4.7c" type="text/css" />
<link rel="stylesheet" href="main.css?v=2.4.8c" type="text/css" />
<link rel="stylesheet" href="widgets.css?v=2.4.8c" type="text/css" />
<link rel="stylesheet" href="icons.css?v=2.4.8c" type="text/css" />
<link rel="icon" type="image/png" href="favicon.png" />
<!-- iPad retina icon -->
@ -114,7 +114,7 @@
rel="apple-touch-startup-image">
<script type="text/javascript" src="index.js?v=2.4.7c"></script>
<script type="text/javascript" src="index.js?v=2.4.8c"></script>
</head>
<body>
<div id="divContainer" class="container main" data-auth="false">
@ -246,6 +246,8 @@
<span id="spanMaxMemory" style="text-align:right;width:120px;"></span>
<span style="text-align:right;display:inline-block;color:#00bcd4;">Min: </span>
<span id="spanMinMemory" style="text-align:right;width:120px;"></span>
<span style="text-align:right;display:inline-block;color:#00bcd4;">Uptime: </span>
<span id="spanUptime" style="text-align:right;width:120px;"></span>
</div>
</div>
<div class="button-container">

View file

@ -1270,7 +1270,7 @@ var security = new Security();
class General {
initialized = false;
appVersion = 'v2.4.7';
appVersion = 'v2.4.8';
reloadApp = false;
init() {
if (this.initialized) return;
@ -4335,7 +4335,7 @@ class Firmware {
if (typeof overlay !== 'undefined') overlay.remove();
reject({ htmlError: status, service: 'GET /backup' });
};
xhr.open('GET', baseUrl.length > 0 ? `${baseUrl}/backup` : '/backup', true);
xhr.open('GET', baseUrl.length > 0 ? `${baseUrl}/backup?attach=true` : '/backup?attach=true', true);
xhr.send();
});
}
@ -4385,6 +4385,20 @@ class Firmware {
if (sp) sp.innerHTML = mem.max.fmt('#,##0');
sp = document.getElementById('spanMinMemory');
if (sp) sp.innerHTML = mem.min.fmt('#,##0');
sp = document.getElementById('spanUptime');
if (sp) {
let t = Math.floor(mem.uptime / 1000);
let d = Math.floor(t / 86400); t %= 86400;
let h = Math.floor(t / 3600); t %= 3600;
let m = Math.floor(t / 60);
let s = t % 60;
let parts = [];
if (d > 0) parts.push(d + 'd');
if (h > 0) parts.push(h + 'h');
if (m > 0) parts.push(m + 'm');
if (s > 0 || parts.length === 0) parts.push(s + 's');
sp.innerHTML = parts.join(' ');
}
}

View file

@ -1 +0,0 @@
2.4.7

6
huge_app.csv Normal file
View file

@ -0,0 +1,6 @@
# Name, Type, SubType, Offset, Size, Flags
nvs, data, nvs, 0x9000, 0x5000,
otadata, data, ota, 0xe000, 0x2000,
app0, app, ota_0, 0x10000, 0x300000,
spiffs, data, spiffs, 0x310000,0xE0000,
coredump, data, coredump,0x3F0000,0x10000,
1 # Name Type SubType Offset Size Flags
2 nvs data nvs 0x9000 0x5000
3 otadata data ota 0xe000 0x2000
4 app0 app ota_0 0x10000 0x300000
5 spiffs data spiffs 0x310000 0xE0000
6 coredump data coredump 0x3F0000 0x10000

37
include/README Normal file
View file

@ -0,0 +1,37 @@
This directory is intended for project header files.
A header file is a file containing C declarations and macro definitions
to be shared between several project source files. You request the use of a
header file in your project source file (C, C++, etc) located in `src` folder
by including it, with the C preprocessing directive `#include'.
```src/main.c
#include "header.h"
int main (void)
{
...
}
```
Including a header file produces the same results as copying the header file
into each source file that needs it. Such copying would be time-consuming
and error-prone. With a header file, the related declarations appear
in only one place. If they need to be changed, they can be changed in one
place, and programs that include the header file will automatically use the
new version when next recompiled. The header file eliminates the labor of
finding and changing all the copies as well as the risk that a failure to
find one copy will result in inconsistencies within a program.
In C, the convention is to give header files names that end with `.h'.
Read more about using header files in official GCC documentation:
* Include Syntax
* Include Operation
* Once-Only Headers
* Computed Includes
https://gcc.gnu.org/onlinedocs/cpp/Header-Files.html

46
lib/README Normal file
View file

@ -0,0 +1,46 @@
This directory is intended for project specific (private) libraries.
PlatformIO will compile them to static libraries and link into the executable file.
The source code of each library should be placed in a separate directory
("lib/your_library_name/[Code]").
For example, see the structure of the following example libraries `Foo` and `Bar`:
|--lib
| |
| |--Bar
| | |--docs
| | |--examples
| | |--src
| | |- Bar.c
| | |- Bar.h
| | |- library.json (optional. for custom build options, etc) https://docs.platformio.org/page/librarymanager/config.html
| |
| |--Foo
| | |- Foo.c
| | |- Foo.h
| |
| |- README --> THIS FILE
|
|- platformio.ini
|--src
|- main.c
Example contents of `src/main.c` using Foo and Bar:
```
#include <Foo.h>
#include <Bar.h>
int main (void)
{
...
}
```
The PlatformIO Library Dependency Finder will find automatically dependent
libraries by scanning project source files.
More information about PlatformIO Library Dependency Finder
- https://docs.platformio.org/page/librarymanager/ldf.html

7
min_spiffs.csv Normal file
View file

@ -0,0 +1,7 @@
# Name, Type, SubType, Offset, Size, Flags
nvs, data, nvs, 0x9000, 0x5000,
otadata, data, ota, 0xe000, 0x2000,
app0, app, ota_0, 0x10000, 0x1E0000,
app1, app, ota_1, 0x1F0000,0x1E0000,
spiffs, data, spiffs, 0x3D0000,0x20000,
coredump, data, coredump,0x3F0000,0x10000,
1 # Name Type SubType Offset Size Flags
2 nvs data nvs 0x9000 0x5000
3 otadata data ota 0xe000 0x2000
4 app0 app ota_0 0x10000 0x1E0000
5 app1 app ota_1 0x1F0000 0x1E0000
6 spiffs data spiffs 0x3D0000 0x20000
7 coredump data coredump 0x3F0000 0x10000

290
minify.py Normal file
View file

@ -0,0 +1,290 @@
"""
PlatformIO pre-build script: minify & gzip web assets.
Copies files from data-src/ data/
HTML whitespace & comment removal, then gzip
CSS whitespace & comment removal, then gzip
JS whitespace & comment removal, then gzip
PNG copied as-is (optipng if available)
Other copied as-is
Usage in platformio.ini
-----------------------
extra_scripts = pre:minify.py
Directory layout
----------------
project/
data-src/ original, human-readable assets
index.html
css/
js/
img/
data/ auto-generated (gitignore this!)
minify.py
platformio.ini
The script runs automatically before LittleFS / SPIFFS image is built.
It also runs once at the start of every build so the data/ dir is always fresh.
Dependencies: none (pure Python 3.7).
Optional: optipng (for PNG optimisation)
"""
Import("env") # PlatformIO macro gives us the build environment
import gzip
import os
import re
import shutil
import subprocess
import sys
# ──────────────────────────────────────────────
# Config
# ──────────────────────────────────────────────
SRC_DIR_NAME = "data-src"
DST_DIR_NAME = "data"
# Extensions that will be minified + gzipped
MINIFY_AND_GZIP = {".html", ".htm", ".css", ".js", ".json", ".svg", ".xml"}
# Extensions that optipng can optimise
PNG_EXTENSIONS = {".png"}
# Everything else is copied verbatim
# ──────────────────────────────────────────────
def _project_dir():
return env.subst("$PROJECT_DIR")
def _src_dir():
return os.path.join(_project_dir(), SRC_DIR_NAME)
def _dst_dir():
return os.path.join(_project_dir(), DST_DIR_NAME)
# ──────────────────────────────────────────────
# Minifiers (pure Python, no npm needed)
# ──────────────────────────────────────────────
def minify_html(text: str) -> str:
"""Simple but effective HTML minifier."""
# Remove HTML comments (but keep IE conditional comments)
text = re.sub(r"<!--(?!\[if).*?-->", "", text, flags=re.DOTALL)
# Collapse whitespace between tags
text = re.sub(r">\s+<", "> <", text)
# Collapse runs of whitespace into a single space
text = re.sub(r"\s{2,}", " ", text)
# Remove whitespace around = in attributes
text = re.sub(r'\s*=\s*', '=', text)
# Strip leading/trailing whitespace per line, rejoin
lines = [line.strip() for line in text.splitlines() if line.strip()]
return " ".join(lines)
def minify_css(text: str) -> str:
"""Remove comments, collapse whitespace in CSS."""
# Remove comments
text = re.sub(r"/\*.*?\*/", "", text, flags=re.DOTALL)
# Remove whitespace around : ; { } ,
text = re.sub(r"\s*([:{};,])\s*", r"\1", text)
# Collapse remaining whitespace
text = re.sub(r"\s{2,}", " ", text)
# Strip leading/trailing
return text.strip()
# def minify_js(text: str) -> str:
# """
# Light JS minifier removes comments and collapses whitespace.
# For heavy minification install terser and the script will use it
# automatically (see _try_terser below).
# """
# # Remove single-line comments (careful with URLs :// )
# text = re.sub(r"(?<!:)//(?!/)[^\n]*", "", text)
# # Remove multi-line comments
# text = re.sub(r"/\*.*?\*/", "", text, flags=re.DOTALL)
# # Collapse whitespace
# text = re.sub(r"\s{2,}", " ", text)
# lines = [line.strip() for line in text.splitlines() if line.strip()]
# return "\n".join(lines)
def minify_json(text: str) -> str:
"""Compact JSON by removing unnecessary whitespace."""
import json
try:
data = json.loads(text)
return json.dumps(data, separators=(",", ":"), ensure_ascii=False)
except json.JSONDecodeError:
return text
def minify_svg(text: str) -> str:
"""Minimal SVG minifier comments + whitespace."""
text = re.sub(r"<!--.*?-->", "", text, flags=re.DOTALL)
text = re.sub(r">\s+<", "><", text)
text = re.sub(r"\s{2,}", " ", text)
return text.strip()
MINIFIERS = {
# ".html": minify_html,
# ".htm": minify_html,
# ".css": minify_css,
# ".js": minify_js,
# ".json": minify_json,
# ".svg": minify_svg,
# ".xml": minify_svg, # same approach works for generic XML
}
# ──────────────────────────────────────────────
# Optional external tools (used when available)
# ──────────────────────────────────────────────
def _has_command(cmd: str) -> bool:
return shutil.which(cmd) is not None
def _try_terser(src_path: str, dst_path: str) -> bool:
"""Use terser for JS if installed (npm i -g terser)."""
if not _has_command("terser"):
return False
try:
subprocess.run(
["terser", src_path, "-o", dst_path, "--compress", "--mangle"],
check=True, capture_output=True,
)
return True
except subprocess.CalledProcessError:
return False
def _try_optipng(path: str) -> None:
"""Optimise PNG in-place if optipng is available."""
if _has_command("optipng"):
try:
subprocess.run(
["optipng", "-quiet", "-o2", path],
check=True, capture_output=True,
)
except subprocess.CalledProcessError:
pass
# ──────────────────────────────────────────────
# Core logic
# ──────────────────────────────────────────────
def process_file(src_path: str, dst_path: str) -> dict:
"""
Process a single file: minify, gzip or copy.
Returns a small stats dict.
"""
ext = os.path.splitext(src_path)[1].lower()
original_size = os.path.getsize(src_path)
stats = {"src": src_path, "original": original_size, "final": 0, "action": "copy"}
os.makedirs(os.path.dirname(dst_path), exist_ok=True)
# ── Text assets: minify + gzip ──────────
if ext in MINIFY_AND_GZIP:
# Try terser for JS first
if ext == ".js" and _try_terser(src_path, dst_path + ".tmp"):
with open(dst_path + ".tmp", "rb") as f:
minified = f.read()
os.remove(dst_path + ".tmp")
stats["action"] = "terser+gzip"
else:
with open(src_path, "r", encoding="utf-8", errors="ignore") as f:
content = f.read()
minifier = MINIFIERS.get(ext)
if minifier:
content = minifier(content)
stats["action"] = "minify+gzip"
else:
stats["action"] = "gzip"
minified = content.encode("utf-8")
# Write gzipped version
gz_path = dst_path + ".gz"
with gzip.open(gz_path, "wb", compresslevel=9) as gz:
gz.write(minified)
stats["final"] = os.path.getsize(gz_path)
return stats
# ── PNG: copy + optional optipng ────────
if ext in PNG_EXTENSIONS:
shutil.copy2(src_path, dst_path)
_try_optipng(dst_path)
stats["final"] = os.path.getsize(dst_path)
stats["action"] = "optipng" if _has_command("optipng") else "copy"
return stats
# ── Everything else: plain copy ─────────
shutil.copy2(src_path, dst_path)
stats["final"] = os.path.getsize(dst_path)
return stats
def minify_all():
src_dir = _src_dir()
dst_dir = _dst_dir()
if not os.path.isdir(src_dir):
print(f"[minify] WARNING: '{SRC_DIR_NAME}/' not found skipping.")
return
print(f"[minify] {SRC_DIR_NAME}/ → {DST_DIR_NAME}/")
# Clean destination
if os.path.exists(dst_dir):
shutil.rmtree(dst_dir)
os.makedirs(dst_dir, exist_ok=True)
total_original = 0
total_final = 0
file_count = 0
for root, dirs, files in os.walk(src_dir):
for fname in sorted(files):
# Skip hidden files and editor temp files
if fname.startswith(".") or fname.endswith("~"):
continue
src_path = os.path.join(root, fname)
rel_path = os.path.relpath(src_path, src_dir)
dst_path = os.path.join(dst_dir, rel_path)
stats = process_file(src_path, dst_path)
pct = (1 - stats["final"] / stats["original"]) * 100 if stats["original"] > 0 else 0
print(
f" {rel_path:<40s} "
f"{stats['original']:>8,d}{stats['final']:>8,d} B "
f"({pct:+.0f}%) [{stats['action']}]"
)
total_original += stats["original"]
total_final += stats["final"]
file_count += 1
print(f"[minify] {file_count} files processed")
print(
f"[minify] Total: {total_original:,d}{total_final:,d} bytes "
f"(saved {total_original - total_final:,d} bytes, "
f"{(1 - total_final / total_original) * 100:.0f}%)"
)
# ──────────────────────────────────────────────
# PlatformIO hooks
# ──────────────────────────────────────────────
# Run at the start of every build
minify_all()

39
platformio.ini Normal file
View file

@ -0,0 +1,39 @@
; PlatformIO Project Configuration File
; https://docs.platformio.org/page/projectconf.html
[platformio]
default_envs = esp32dev
; Shared settings for all environments
[env]
platform = espressif32
framework = arduino
lib_deps =
bblanchon/ArduinoJson@^7.2.2
lsatan/SmartRC-CC1101-Driver-Lib@^2.5.7
knolleary/PubSubClient@^2.8
esp32async/ESPAsyncWebServer@^3.10.3
esp32async/AsyncTCP@^3.4.10
extra_scripts = pre:minify.py
board_build.partitions = huge_app.csv
board_build.filesystem = littlefs
build_flags =
-DCONFIG_ESP_COREDUMP_ENABLE_TO_FLASH=1
-DCONFIG_ESP_COREDUMP_DATA_FORMAT_ELF=1
-DCONFIG_ESP_COREDUMP_CHECKSUM_CRC32=1
-DCONFIG_ESP_TASK_WDT_PANIC=1
-DCONFIG_ESP_COREDUMP_DECODE_INFO=1
monitor_speed = 115200
monitor_filters =
time
esp32_exception_decoder
log2file
[env:esp32dev]
board = esp32dev
[env:esp32devdbg]
board = esp32dev
build_type = debug

View file

@ -22,10 +22,15 @@ bool ConfigFile::begin(const char* filename, bool readOnly) {
this->_opened = true;
return true;
}
bool ConfigFile::beginRAM(String *buf) {
_ramBuf = buf;
_opened = true;
return true;
}
void ConfigFile::end() {
if(this->isOpen()) {
if(!this->readOnly) this->file.flush();
this->file.close();
if(_ramBuf) { _ramBuf = nullptr; }
else { if(!this->readOnly) this->file.flush(); this->file.close(); }
}
this->_opened = false;
}
@ -187,10 +192,16 @@ bool ConfigFile::readVarString(char *buff, size_t len) {
bool ConfigFile::writeString(const char *val, size_t len, const char tok) {
if(!this->isOpen()) return false;
int slen = strlen(val);
if(_ramBuf) {
if(slen > 0) _ramBuf->concat(val);
while(slen < (int)len - 1) { _ramBuf->concat(' '); slen++; }
if(tok != CFG_TOK_NONE) _ramBuf->concat(tok);
return true;
}
if(slen > 0)
if(this->file.write((uint8_t *)val, slen) != slen) return false;
// Now we need to pad the end of the string so that it is of a fixed length.
while(slen < len - 1) {
while(slen < (int)len - 1) {
this->file.write(' ');
slen++;
}
@ -202,6 +213,13 @@ bool ConfigFile::writeString(const char *val, size_t len, const char tok) {
bool ConfigFile::writeVarString(const char *val, const char tok) {
if(!this->isOpen()) return false;
int slen = strlen(val);
if(_ramBuf) {
_ramBuf->concat((char)CFG_TOK_QUOTE);
if(slen > 0) _ramBuf->concat(val);
_ramBuf->concat((char)CFG_TOK_QUOTE);
if(tok != CFG_TOK_NONE) _ramBuf->concat(tok);
return true;
}
this->writeChar(CFG_TOK_QUOTE);
if(slen > 0) if(this->file.write((uint8_t *)val, slen) != slen) return false;
this->writeChar(CFG_TOK_QUOTE);
@ -210,6 +228,7 @@ bool ConfigFile::writeVarString(const char *val, const char tok) {
}
bool ConfigFile::writeChar(const char val) {
if(!this->isOpen()) return false;
if(_ramBuf) { _ramBuf->concat(val); return true; }
if(this->file.write(static_cast<uint8_t>(val)) == 1) return true;
return false;
}

View file

@ -31,12 +31,14 @@ struct config_header_t {
class ConfigFile {
protected:
File file;
String *_ramBuf = nullptr;
bool readOnly = false;
bool begin(const char *filename, bool readOnly = false);
uint32_t startRecPos = 0;
bool _opened = false;
public:
config_header_t header;
bool beginRAM(String *buf);
void end();
bool isOpen();
bool seekRecordByIndex(uint16_t ndx);

View file

@ -85,13 +85,6 @@ bool appver_t::toJSON(JsonObject &obj) {
obj["suffix"] = this->suffix;
return true;
}
void appver_t::toJSON(JsonResponse &json) {
json.addElem("name", this->name);
json.addElem("major", this->major);
json.addElem("minor", this->minor);
json.addElem("build", this->build);
json.addElem("suffix", this->suffix);
}
void appver_t::toJSON(JsonSockEvent *json) {
json->addElem("name", this->name);
json->addElem("major", this->major);
@ -249,14 +242,6 @@ bool ConfigSettings::toJSON(JsonObject &obj) {
obj["checkForUpdate"] = this->checkForUpdate;
return true;
}
void ConfigSettings::toJSON(JsonResponse &json) {
json.addElem("ssdpBroadcast", this->ssdpBroadcast);
json.addElem("hostname", this->hostname);
json.addElem("connType", static_cast<uint8_t>(this->connType));
json.addElem("chipModel", this->chipModel);
json.addElem("checkForUpdate", this->checkForUpdate);
}
bool ConfigSettings::requiresAuth() { return this->Security.type != security_types::None; }
bool ConfigSettings::fromJSON(JsonObject &obj) {
if(obj.containsKey("ssdpBroadcast")) this->ssdpBroadcast = obj["ssdpBroadcast"];
@ -308,18 +293,6 @@ bool MQTTSettings::begin() {
this->load();
return true;
}
void MQTTSettings::toJSON(JsonResponse &json) {
json.addElem("enabled", this->enabled);
json.addElem("pubDisco", this->pubDisco);
json.addElem("protocol", this->protocol);
json.addElem("hostname", this->hostname);
json.addElem("port", (uint32_t)this->port);
json.addElem("username", this->username);
json.addElem("password", this->password);
json.addElem("rootTopic", this->rootTopic);
json.addElem("discoTopic", this->discoTopic);
}
bool MQTTSettings::toJSON(JsonObject &obj) {
obj["enabled"] = this->enabled;
obj["pubDisco"] = this->pubDisco;
@ -418,11 +391,6 @@ bool NTPSettings::fromJSON(JsonObject &obj) {
this->parseValueString(obj, "posixZone", this->posixZone, sizeof(this->posixZone));
return true;
}
void NTPSettings::toJSON(JsonResponse &json) {
json.addElem("ntpServer", this->ntpServer);
json.addElem("posixZone", this->posixZone);
}
bool NTPSettings::toJSON(JsonObject &obj) {
obj["ntpServer"] = this->ntpServer;
obj["posixZone"] = this->posixZone;
@ -459,16 +427,6 @@ bool IPSettings::toJSON(JsonObject &obj) {
obj["dns2"] = this->dns2 == ipEmpty ? "" : this->dns2.toString();
return true;
}
void IPSettings::toJSON(JsonResponse &json) {
IPAddress ipEmpty(0,0,0,0);
json.addElem("dhcp", this->dhcp);
json.addElem("ip", this->ip.toString().c_str());
json.addElem("gateway", this->gateway.toString().c_str());
json.addElem("subnet", this->subnet.toString().c_str());
json.addElem("dns1", this->dns1.toString().c_str());
json.addElem("dns2", this->dns2.toString().c_str());
}
bool IPSettings::save() {
pref.begin("IP");
pref.clear();
@ -529,14 +487,6 @@ bool SecuritySettings::toJSON(JsonObject &obj) {
obj["permissions"] = this->permissions;
return true;
}
void SecuritySettings::toJSON(JsonResponse &json) {
json.addElem("type", static_cast<uint8_t>(this->type));
json.addElem("username", this->username);
json.addElem("password", this->password);
json.addElem("pin", this->pin);
json.addElem("permissions", this->permissions);
}
bool SecuritySettings::save() {
pref.begin("SEC");
pref.clear();
@ -590,13 +540,6 @@ bool WifiSettings::toJSON(JsonObject &obj) {
obj["hidden"] = this->hidden;
return true;
}
void WifiSettings::toJSON(JsonResponse &json) {
json.addElem("ssid", this->ssid);
json.addElem("passphrase", this->passphrase);
json.addElem("roaming", this->roaming);
json.addElem("hidden", this->hidden);
}
bool WifiSettings::save() {
pref.begin("WIFI");
pref.clear();
@ -697,16 +640,6 @@ bool EthernetSettings::toJSON(JsonObject &obj) {
obj["MDIOPin"] = this->MDIOPin;
return true;
}
void EthernetSettings::toJSON(JsonResponse &json) {
json.addElem("boardType", this->boardType);
json.addElem("phyAddress", this->phyAddress);
json.addElem("CLKMode", static_cast<uint8_t>(this->CLKMode));
json.addElem("phyType", static_cast<uint8_t>(this->phyType));
json.addElem("PWRPin", this->PWRPin);
json.addElem("MDCPin", this->MDCPin);
json.addElem("MDIOPin", this->MDIOPin);
}
bool EthernetSettings::usesPin(uint8_t pin) {
if((this->CLKMode == 0 || this->CLKMode == 1) && pin == 0) return true;
else if(this->CLKMode == 2 && pin == 16) return true;

View file

@ -3,7 +3,7 @@
#ifndef configsettings_h
#define configsettings_h
#include "WResp.h"
#define FW_VERSION "v2.4.7"
#define FW_VERSION "v2.4.8"
enum class conn_types_t : byte {
unset = 0x00,
wifi = 0x01,
@ -34,7 +34,7 @@ struct appver_t {
char suffix[4] = "";
void parse(const char *ver);
bool toJSON(JsonObject &obj);
void toJSON(JsonResponse &json);
void toJSON(JsonSockEvent *json);
int8_t compare(appver_t &ver);
void copy(appver_t &ver);
@ -46,7 +46,7 @@ class BaseSettings {
bool loadFile(const char* filename);
bool fromJSON(JsonObject &obj);
bool toJSON(JsonObject &obj);
void toJSON(JsonResponse &json);
bool parseIPAddress(JsonObject &obj, const char *prop, IPAddress *);
bool parseValueString(JsonObject &obj, const char *prop, char *dest, size_t size);
int parseValueInt(JsonObject &obj, const char *prop, int defVal);
@ -61,7 +61,7 @@ class NTPSettings: BaseSettings {
char posixZone[64] = "";
bool fromJSON(JsonObject &obj);
bool toJSON(JsonObject &obj);
void toJSON(JsonResponse &json);
bool apply();
bool begin();
bool save();
@ -79,7 +79,7 @@ class WifiSettings: BaseSettings {
bool begin();
bool fromJSON(JsonObject &obj);
bool toJSON(JsonObject &obj);
void toJSON(JsonResponse &json);
String mapEncryptionType(int type);
bool ssidExists(const char *ssid);
void printNetworks();
@ -102,7 +102,7 @@ class EthernetSettings: BaseSettings {
bool begin();
bool fromJSON(JsonObject &obj);
bool toJSON(JsonObject &obj);
void toJSON(JsonResponse &json);
bool load();
bool save();
void print();
@ -120,7 +120,7 @@ class IPSettings: BaseSettings {
bool begin();
bool fromJSON(JsonObject &obj);
bool toJSON(JsonObject &obj);
void toJSON(JsonResponse &json);
bool load();
bool save();
void print();
@ -145,7 +145,7 @@ class SecuritySettings: BaseSettings {
bool load();
void print();
bool toJSON(JsonObject &obj);
void toJSON(JsonResponse &json);
bool fromJSON(JsonObject &obj);
};
class MQTTSettings: BaseSettings {
@ -163,7 +163,7 @@ class MQTTSettings: BaseSettings {
bool save();
bool load();
bool toJSON(JsonObject &obj);
void toJSON(JsonResponse &json);
bool fromJSON(JsonObject &obj);
};
class ConfigSettings: BaseSettings {
@ -187,7 +187,7 @@ class ConfigSettings: BaseSettings {
bool requiresAuth();
bool fromJSON(JsonObject &obj);
bool toJSON(JsonObject &obj);
void toJSON(JsonResponse &json);
bool begin();
bool save();
bool load();

View file

@ -72,22 +72,6 @@ void GitRelease::setAssetProperty(const char *key, const char *val) {
}
}
}
void GitRelease::toJSON(JsonResponse &json) {
Timestamp ts;
char buff[20];
sprintf(buff, "%llu", this->id);
json.addElem("id", buff);
json.addElem("name", this->name);
json.addElem("date", ts.getISOTime(this->releaseDate));
json.addElem("draft", this->draft);
json.addElem("preRelease", this->preRelease);
json.addElem("main", this->main);
json.addElem("hasFS", this->hasFS);
json.addElem("hwVersions", this->hwVersions);
json.beginObject("version");
this->version.toJSON(json);
json.endObject();
}
#define ERR_CLIENT_OFFSET -50
int16_t GitRepo::getReleases(uint8_t num) {
@ -230,22 +214,6 @@ int16_t GitRepo::getReleases(uint8_t num) {
settings.printAvailHeap();
return 0;
}
void GitRepo::toJSON(JsonResponse &json) {
json.beginObject("fwVersion");
settings.fwVersion.toJSON(json);
json.endObject();
json.beginObject("appVersion");
settings.appVersion.toJSON(json);
json.endObject();
json.beginArray("releases");
for(uint8_t i = 0; i < GIT_MAX_RELEASES + 1; i++) {
if(this->releases[i].id == 0) continue;
json.beginObject();
this->releases[i].toJSON(json);
json.endObject();
}
json.endArray();
}
#define UPDATE_ERR_OFFSET 20
#define ERR_DOWNLOAD_HTTP -40
#define ERR_DOWNLOAD_BUFFER -41
@ -310,23 +278,6 @@ void GitUpdater::setCurrentRelease(GitRepo &repo) {
}
this->emitUpdateCheck();
}
void GitUpdater::toJSON(JsonResponse &json) {
json.addElem("available", this->updateAvailable);
json.addElem("status", this->status);
json.addElem("error", (int32_t)this->error);
json.addElem("cancelled", this->cancelled);
json.addElem("checkForUpdate", settings.checkForUpdate);
json.addElem("inetAvailable", this->inetAvailable);
json.beginObject("fwVersion");
settings.fwVersion.toJSON(json);
json.endObject();
json.beginObject("appVersion");
settings.appVersion.toJSON(json);
json.endObject();
json.beginObject("latest");
this->latest.toJSON(json);
json.endObject();
}
void GitUpdater::emitUpdateCheck(uint8_t num) {
JsonSockEvent *json = sockEmit.beginEmit("fwStatus");
json->beginObject();

View file

@ -28,13 +28,13 @@ class GitRelease {
appver_t version;
void setReleaseProperty(const char *key, const char *val);
void setAssetProperty(const char *key, const char *val);
void toJSON(JsonResponse &json);
};
class GitRepo {
public:
int16_t getReleases(uint8_t num = GIT_MAX_RELEASES);
GitRelease releases[GIT_MAX_RELEASES + 1];
void toJSON(JsonResponse &json);
};
class GitUpdater {
public:
@ -58,7 +58,7 @@ class GitUpdater {
void setFirmwareFile();
void setCurrentRelease(GitRepo &repo);
void loop();
void toJSON(JsonResponse &json);
bool recoverFilesystem();
int checkInternet();
void emitUpdateCheck(uint8_t num=255);

View file

View file

@ -691,6 +691,7 @@ void Network::emitHeap(uint8_t num) {
json->addElem("free", freeHeap);
json->addElem("min", minHeap);
json->addElem("total", ESP.getHeapSize());
json->addElem("uptime", (uint64_t)millis());
json->endObject();
if(num == 255 && bTimeEmit && bValEmit) {
sockEmit.endEmit(num);

View file

221
src/Sockets.cpp Normal file
View file

@ -0,0 +1,221 @@
#include <Arduino.h>
#include <ArduinoJson.h>
#include <esp_task_wdt.h>
#include "Sockets.h"
#include "ConfigSettings.h"
#include "Somfy.h"
#include "Network.h"
#include "GitOTA.h"
extern ConfigSettings settings;
extern Network net;
extern SomfyShadeController somfy;
extern SocketEmitter sockEmit;
extern GitUpdater git;
AsyncWebServer wsServer(8080);
AsyncWebSocket ws("/");
#define MAX_WS_CLIENTS 5
static uint32_t clientMap[MAX_WS_CLIENTS] = {0,0,0,0,0};
static uint8_t mapClientId(uint32_t asyncId) {
for(uint8_t i = 0; i < MAX_WS_CLIENTS; i++)
if(clientMap[i] == asyncId) return i;
return 255;
}
static uint32_t getAsyncId(uint8_t slot) {
if(slot < MAX_WS_CLIENTS) return clientMap[slot];
return 0;
}
static uint8_t addClient(uint32_t asyncId) {
for(uint8_t i = 0; i < MAX_WS_CLIENTS; i++) {
if(clientMap[i] == 0) { clientMap[i] = asyncId; return i; }
}
return 255;
}
static void removeClient(uint32_t asyncId) {
for(uint8_t i = 0; i < MAX_WS_CLIENTS; i++)
if(clientMap[i] == asyncId) clientMap[i] = 0;
}
#define MAX_SOCK_RESPONSE 2048
static char g_response[MAX_SOCK_RESPONSE];
bool room_t::isJoined(uint8_t num) {
for(uint8_t i = 0; i < sizeof(this->clients); i++) {
if(this->clients[i] == num) return true;
}
return false;
}
bool room_t::join(uint8_t num) {
if(this->isJoined(num)) return true;
for(uint8_t i = 0; i < sizeof(this->clients); i++) {
if(this->clients[i] == 255) {
this->clients[i] = num;
return true;
}
}
return false;
}
bool room_t::leave(uint8_t num) {
if(!this->isJoined(num)) return false;
for(uint8_t i = 0; i < sizeof(this->clients); i++) {
if(this->clients[i] == num) this->clients[i] = 255;
}
return true;
}
void room_t::clear() {
memset(this->clients, 255, sizeof(this->clients));
}
uint8_t room_t::activeClients() {
uint8_t n = 0;
for(uint8_t i = 0; i < sizeof(this->clients); i++) {
if(this->clients[i] != 255) n++;
}
return n;
}
/*********************************************************************
* SocketEmitter class members
********************************************************************/
void SocketEmitter::startup() {
}
void SocketEmitter::begin() {
ws.onEvent(SocketEmitter::wsEvent);
wsServer.addHandler(&ws);
wsServer.begin();
Serial.println("Socket Server Started...");
}
void SocketEmitter::loop() {
ws.cleanupClients();
this->initClients();
}
JsonSockEvent *SocketEmitter::beginEmit(const char *evt) {
this->json.beginEvent(&ws, evt, g_response, sizeof(g_response));
return &this->json;
}
void SocketEmitter::endEmit(uint8_t num) {
if(num == 255) {
this->json.endEvent(0);
} else {
uint32_t asyncId = getAsyncId(num);
this->json.endEvent(asyncId);
}
esp_task_wdt_reset();
}
void SocketEmitter::endEmitRoom(uint8_t room) {
if(room < SOCK_MAX_ROOMS) {
room_t *r = &this->rooms[room];
for(uint8_t i = 0; i < sizeof(r->clients); i++) {
if(r->clients[i] != 255) {
uint32_t asyncId = getAsyncId(r->clients[i]);
if(asyncId != 0) this->json.endEvent(asyncId);
}
}
}
}
uint8_t SocketEmitter::activeClients(uint8_t room) {
if(room < SOCK_MAX_ROOMS) return this->rooms[room].activeClients();
return 0;
}
void SocketEmitter::initClients() {
for(uint8_t i = 0; i < sizeof(this->newClients); i++) {
uint8_t slot = this->newClients[i];
if(slot != 255) {
uint32_t asyncId = getAsyncId(slot);
if(asyncId != 0 && ws.hasClient(asyncId)) {
Serial.printf("Initializing Socket Client %u (asyncId=%lu)\n", slot, asyncId);
esp_task_wdt_reset();
settings.emitSockets(slot);
if(!ws.hasClient(asyncId)) { this->newClients[i] = 255; continue; }
somfy.emitState(slot);
if(!ws.hasClient(asyncId)) { this->newClients[i] = 255; continue; }
git.emitUpdateCheck(slot);
if(!ws.hasClient(asyncId)) { this->newClients[i] = 255; continue; }
net.emitSockets(slot);
esp_task_wdt_reset();
}
this->newClients[i] = 255;
}
}
}
void SocketEmitter::delayInit(uint8_t num) {
for(uint8_t i=0; i < sizeof(this->newClients); i++) {
if(this->newClients[i] == num) break;
else if(this->newClients[i] == 255) {
this->newClients[i] = num;
break;
}
}
}
void SocketEmitter::end() {
ws.closeAll();
wsServer.end();
for(uint8_t i = 0; i < SOCK_MAX_ROOMS; i++)
this->rooms[i].clear();
memset(clientMap, 0, sizeof(clientMap));
}
void SocketEmitter::disconnect() {
ws.closeAll();
memset(clientMap, 0, sizeof(clientMap));
}
void SocketEmitter::wsEvent(AsyncWebSocket *server, AsyncWebSocketClient *client, AwsEventType type, void *arg, uint8_t *data, size_t len) {
uint32_t asyncId = client->id();
switch(type) {
case WS_EVT_CONNECT:
{
uint8_t slot = addClient(asyncId);
if(slot == 255) {
Serial.printf("Socket: No free client slots, closing %lu\n", asyncId);
client->close();
return;
}
IPAddress ip = client->remoteIP();
Serial.printf("Socket [%lu] Connected from %d.%d.%d.%d (slot %u)\n", asyncId, ip[0], ip[1], ip[2], ip[3], slot);
client->text("Connected");
client->setCloseClientOnQueueFull(false);
sockEmit.delayInit(slot);
}
break;
case WS_EVT_DISCONNECT:
{
uint8_t slot = mapClientId(asyncId);
Serial.printf("Socket [%lu] Disconnected (slot %u)\n", asyncId, slot);
if(slot != 255) {
for(uint8_t i = 0; i < SOCK_MAX_ROOMS; i++)
sockEmit.rooms[i].leave(slot);
}
removeClient(asyncId);
}
break;
case WS_EVT_DATA:
{
AwsFrameInfo *info = (AwsFrameInfo*)arg;
if(info->final && info->index == 0 && info->len == len && info->opcode == WS_TEXT) {
uint8_t slot = mapClientId(asyncId);
data[len] = 0;
if(strncmp((char*)data, "join:", 5) == 0) {
uint8_t roomNum = atoi((char*)&data[5]);
Serial.printf("Client %u joining room %u\n", slot, roomNum);
if(roomNum < SOCK_MAX_ROOMS && slot != 255) sockEmit.rooms[roomNum].join(slot);
}
else if(strncmp((char*)data, "leave:", 6) == 0) {
uint8_t roomNum = atoi((char*)&data[6]);
Serial.printf("Client %u leaving room %u\n", slot, roomNum);
if(roomNum < SOCK_MAX_ROOMS && slot != 255) sockEmit.rooms[roomNum].leave(slot);
}
else {
Serial.printf("Socket [%lu] text: %s\n", asyncId, data);
}
}
}
break;
case WS_EVT_ERROR:
Serial.printf("Socket [%lu] Error\n", asyncId);
break;
case WS_EVT_PONG:
break;
}
}

View file

@ -1,4 +1,4 @@
#include <WebSocketsServer.h>
#include <ESPAsyncWebServer.h>
#include "WResp.h"
#ifndef sockets_h
#define sockets_h
@ -21,7 +21,6 @@ class SocketEmitter {
void delayInit(uint8_t num);
public:
JsonSockEvent json;
//ClientSocketEvent evt;
room_t rooms[SOCK_MAX_ROOMS];
uint8_t activeClients(uint8_t room);
void initClients();
@ -33,6 +32,6 @@ class SocketEmitter {
JsonSockEvent * beginEmit(const char *evt);
void endEmit(uint8_t num = 255);
void endEmitRoom(uint8_t num);
static void wsEvent(uint8_t num, WStype_t type, uint8_t *payload, size_t length);
static void wsEvent(AsyncWebSocket *server, AsyncWebSocketClient *client, AwsEventType type, void *arg, uint8_t *data, size_t len);
};
#endif

View file

@ -1,7 +1,6 @@
#include <Preferences.h>
#include <ELECHOUSE_CC1101_SRC_DRV.h>
#include <SPI.h>
#include <WebServer.h>
#include <esp_task_wdt.h>
#include "Utils.h"
#include "ConfigSettings.h"
@ -156,9 +155,9 @@ void somfy_frame_t::decodeFrame(byte* frame) {
this->proto = radio_proto::RTV;
this->cmd = (somfy_commands)(this->encKey - 148);
}
else if(this->encKey > 133) {
else if(this->encKey >= 133) {
this->proto = radio_proto::RTW;
this->cmd = (somfy_commands)(this->encKey - 133);
this->cmd = this->encKey == 133 ? somfy_commands::My : (somfy_commands)(this->encKey - 133);
}
}
else this->proto = radio_proto::RTS;
@ -633,8 +632,10 @@ void SomfyShadeController::commit() {
void SomfyShadeController::writeBackup() {
if(git.lockFS) return;
esp_task_wdt_reset(); // Make sure we don't reset inadvertently.
this->backupData = "";
this->backupData.reserve(16384);
ShadeConfigFile file;
file.begin("/controller.backup", false);
file.beginRAM(&this->backupData);
file.backup(this);
file.end();
}
@ -1445,9 +1446,9 @@ void SomfyRoom::unpublish() {
}
void SomfyShade::publishState() {
if(mqtt.connected()) {
this->publish("position", this->transformPosition(this->currentPos), true);
this->publish("direction", this->direction, true);
this->publish("target", this->transformPosition(this->target), true);
this->publish("position", (uint8_t)50, true);
this->publish("direction", (int8_t)0, true);
this->publish("target", (uint8_t)50, true);
this->publish("lastRollingCode", this->lastRollingCode);
this->publish("mypos", this->transformPosition(this->myPos), true);
this->publish("myTiltPos", this->transformPosition(this->myTiltPos), true);
@ -1794,7 +1795,7 @@ bool SomfyGroup::publish(const char *topic, bool val, bool retain) {
float SomfyShade::p_currentPos(float pos) {
float old = this->currentPos;
this->currentPos = pos;
if(floor(old) != floor(pos)) this->publish("position", this->transformPosition(static_cast<uint8_t>(floor(this->currentPos))));
if(floor(old) != floor(pos)) this->publish("position", (uint8_t)50);
return old;
}
float SomfyShade::p_currentTiltPos(float pos) {
@ -2888,9 +2889,21 @@ void SomfyShade::moveToMyPosition() {
}
void SomfyShade::sendCommand(somfy_commands cmd) { this->sendCommand(cmd, this->repeats); }
void SomfyShade::sendCommand(somfy_commands cmd, uint8_t repeat, uint8_t stepSize) {
Serial.print("Send command start\n");
// This sendCommand function will always be called externally. sendCommand at the remote level
// is expected to be called internally when the motor needs commanded.
if(this->bitLength == 0) this->bitLength = somfy.transceiver.config.type;
// If same direction command sent while already moving, stop the state tracking.
// The real motor stops on its own when it receives the same direction again.
if((cmd == somfy_commands::Up && this->direction == -1) ||
(cmd == somfy_commands::Down && this->direction == 1)) {
Serial.println("Same command as dir");
SomfyRemote::sendCommand(cmd, repeat);
this->p_target(this->currentPos);
this->p_tiltTarget(this->currentTiltPos);
this->setMovement(0);
return;
}
if(cmd == somfy_commands::Up) {
if(this->tiltType == tilt_types::euromode) {
// In euromode we need to long press for 2 seconds on the
@ -2930,9 +2943,13 @@ void SomfyShade::sendCommand(somfy_commands cmd, uint8_t repeat, uint8_t stepSiz
else if(cmd == somfy_commands::My) {
if(this->isToggle() || this->shadeType == shade_types::drycontact)
SomfyRemote::sendCommand(cmd, repeat);
else if(this->shadeType == shade_types::drycontact2) return;
else if(this->shadeType == shade_types::drycontact2){
Serial.print("Send command start 1\n");
return;
}
else if(this->isIdle()) {
this->moveToMyPosition();
Serial.print("Send command end 2\n");
return;
}
else {
@ -2951,6 +2968,7 @@ void SomfyShade::sendCommand(somfy_commands cmd, uint8_t repeat, uint8_t stepSiz
else {
SomfyRemote::sendCommand(cmd, repeat, stepSize);
}
Serial.print("Send command end\n");
}
void SomfyGroup::sendCommand(somfy_commands cmd) { this->sendCommand(cmd, this->repeats); }
void SomfyGroup::sendCommand(somfy_commands cmd, uint8_t repeat, uint8_t stepSize) {
@ -3309,72 +3327,6 @@ int8_t SomfyShade::fromJSON(JsonObject &obj) {
}
return err;
}
void SomfyShade::toJSONRef(JsonResponse &json) {
json.addElem("shadeId", this->getShadeId());
json.addElem("roomId", this->roomId);
json.addElem("name", this->name);
json.addElem("remoteAddress", (uint32_t)this->m_remoteAddress);
json.addElem("paired", this->paired);
json.addElem("shadeType", static_cast<uint8_t>(this->shadeType));
json.addElem("flipCommands", this->flipCommands);
json.addElem("flipPosition", this->flipCommands);
json.addElem("bitLength", this->bitLength);
json.addElem("proto", static_cast<uint8_t>(this->proto));
json.addElem("flags", this->flags);
json.addElem("sunSensor", this->hasSunSensor());
json.addElem("hasLight", this->hasLight());
json.addElem("repeats", this->repeats);
//SomfyRemote::toJSON(json);
}
void SomfyShade::toJSON(JsonResponse &json) {
json.addElem("shadeId", this->getShadeId());
json.addElem("roomId", this->roomId);
json.addElem("name", this->name);
json.addElem("remoteAddress", (uint32_t)this->m_remoteAddress);
json.addElem("upTime", (uint32_t)this->upTime);
json.addElem("downTime", (uint32_t)this->downTime);
json.addElem("paired", this->paired);
json.addElem("lastRollingCode", (uint32_t)this->lastRollingCode);
json.addElem("position", this->transformPosition(this->currentPos));
json.addElem("tiltType", static_cast<uint8_t>(this->tiltType));
json.addElem("tiltPosition", this->transformPosition(this->currentTiltPos));
json.addElem("tiltDirection", this->tiltDirection);
json.addElem("tiltTime", (uint32_t)this->tiltTime);
json.addElem("stepSize", (uint32_t)this->stepSize);
json.addElem("tiltTarget", this->transformPosition(this->tiltTarget));
json.addElem("target", this->transformPosition(this->target));
json.addElem("myPos", this->transformPosition(this->myPos));
json.addElem("myTiltPos", this->transformPosition(this->myTiltPos));
json.addElem("direction", this->direction);
json.addElem("shadeType", static_cast<uint8_t>(this->shadeType));
json.addElem("bitLength", this->bitLength);
json.addElem("proto", static_cast<uint8_t>(this->proto));
json.addElem("flags", this->flags);
json.addElem("flipCommands", this->flipCommands);
json.addElem("flipPosition", this->flipPosition);
json.addElem("inGroup", this->isInGroup());
json.addElem("sunSensor", this->hasSunSensor());
json.addElem("light", this->hasLight());
json.addElem("repeats", this->repeats);
json.addElem("sortOrder", this->sortOrder);
json.addElem("gpioUp", this->gpioUp);
json.addElem("gpioDown", this->gpioDown);
json.addElem("gpioMy", this->gpioMy);
json.addElem("gpioLLTrigger", ((this->gpioFlags & (uint8_t)gpio_flags_t::LowLevelTrigger) == 0) ? false : true);
json.addElem("simMy", this->simMy());
json.beginArray("linkedRemotes");
for(uint8_t i = 0; i < SOMFY_MAX_LINKED_REMOTES; i++) {
SomfyLinkedRemote &lremote = this->linkedRemotes[i];
if(lremote.getRemoteAddress() != 0) {
json.beginObject();
lremote.toJSON(json);
json.endObject();
}
}
json.endArray();
}
/*
bool SomfyShade::toJSON(JsonObject &obj) {
//Serial.print("Serializing Shade:");
@ -3442,12 +3394,6 @@ bool SomfyRoom::toJSON(JsonObject &obj) {
return true;
}
*/
void SomfyRoom::toJSON(JsonResponse &json) {
json.addElem("roomId", this->roomId);
json.addElem("name", this->name);
json.addElem("sortOrder", this->sortOrder);
}
bool SomfyGroup::fromJSON(JsonObject &obj) {
if(obj.containsKey("name")) strlcpy(this->name, obj["name"], sizeof(this->name));
if(obj.containsKey("roomId")) this->roomId = obj["roomId"];
@ -3469,50 +3415,6 @@ bool SomfyGroup::fromJSON(JsonObject &obj) {
}
return true;
}
void SomfyGroup::toJSON(JsonResponse &json) {
this->updateFlags();
json.addElem("groupId", this->getGroupId());
json.addElem("roomId", this->roomId);
json.addElem("name", this->name);
json.addElem("remoteAddress", (uint32_t)this->m_remoteAddress);
json.addElem("lastRollingCode", (uint32_t)this->lastRollingCode);
json.addElem("bitLength", this->bitLength);
json.addElem("proto", static_cast<uint8_t>(this->proto));
json.addElem("sunSensor", this->hasSunSensor());
json.addElem("flipCommands", this->flipCommands);
json.addElem("flags", this->flags);
json.addElem("repeats", this->repeats);
json.addElem("sortOrder", this->sortOrder);
json.beginArray("linkedShades");
for(uint8_t i = 0; i < SOMFY_MAX_GROUPED_SHADES; i++) {
uint8_t shadeId = this->linkedShades[i];
if(shadeId > 0 && shadeId < 255) {
SomfyShade *shade = somfy.getShadeById(shadeId);
if(shade) {
json.beginObject();
shade->toJSONRef(json);
json.endObject();
}
}
}
json.endArray();
}
void SomfyGroup::toJSONRef(JsonResponse &json) {
this->updateFlags();
json.addElem("groupId", this->getGroupId());
json.addElem("roomId", this->roomId);
json.addElem("name", this->name);
json.addElem("remoteAddress", (uint32_t)this->m_remoteAddress);
json.addElem("lastRollingCode", (uint32_t)this->lastRollingCode);
json.addElem("bitLength", this->bitLength);
json.addElem("proto", static_cast<uint8_t>(this->proto));
json.addElem("sunSensor", this->hasSunSensor());
json.addElem("flipCommands", this->flipCommands);
json.addElem("flags", this->flags);
json.addElem("repeats", this->repeats);
json.addElem("sortOrder", this->sortOrder);
}
/*
bool SomfyGroup::toJSON(JsonObject &obj) {
this->updateFlags();
@ -3544,10 +3446,6 @@ bool SomfyGroup::toJSON(JsonObject &obj) {
}
*/
void SomfyRemote::toJSON(JsonResponse &json) {
json.addElem("remoteAddress", (uint32_t)this->getRemoteAddress());
json.addElem("lastRollingCode", (uint32_t)this->lastRollingCode);
}
/*
bool SomfyRemote::toJSON(JsonObject &obj) {
//obj["remotePrefId"] = this->getRemotePrefId();
@ -3571,6 +3469,7 @@ void SomfyShadeController::emitState(uint8_t num) {
for(uint8_t i = 0; i < SOMFY_MAX_SHADES; i++) {
SomfyShade *shade = &this->shades[i];
if(shade->getShadeId() == 255) continue;
esp_task_wdt_reset();
shade->emitState(num);
}
}
@ -4111,26 +4010,6 @@ uint16_t SomfyRemote::setRollingCode(uint16_t code) {
}
return code;
}
void SomfyShadeController::toJSONRooms(JsonResponse &json) {
for(uint8_t i = 0; i < SOMFY_MAX_ROOMS; i++) {
SomfyRoom *room = &this->rooms[i];
if(room->roomId != 0) {
json.beginObject();
room->toJSON(json);
json.endObject();
}
}
}
void SomfyShadeController::toJSONShades(JsonResponse &json) {
for(uint8_t i = 0; i < SOMFY_MAX_SHADES; i++) {
SomfyShade &shade = this->shades[i];
if(shade.getShadeId() != 255) {
json.beginObject();
shade.toJSON(json);
json.endObject();
}
}
}
/*
bool SomfyShadeController::toJSON(DynamicJsonDocument &doc) {
@ -4185,21 +4064,6 @@ bool SomfyShadeController::toJSONGroups(JsonArray &arr) {
return true;
}
*/
void SomfyShadeController::toJSONGroups(JsonResponse &json) {
for(uint8_t i = 0; i < SOMFY_MAX_GROUPS; i++) {
SomfyGroup &group = this->groups[i];
if(group.getGroupId() != 255) {
json.beginObject();
group.toJSON(json);
json.endObject();
}
}
}
void SomfyShadeController::toJSONRepeaters(JsonResponse &json) {
for(uint8_t i = 0; i < SOMFY_MAX_REPEATERS; i++) {
if(somfy.repeaters[i] != 0) json.addElem((uint32_t)somfy.repeaters[i]);
}
}
void SomfyShadeController::loop() {
this->transceiver.loop();
for(uint8_t i = 0; i < SOMFY_MAX_SHADES; i++) {
@ -4683,11 +4547,6 @@ void Transceiver::disableReceive(void) {
interruptPin = 0;
}
void Transceiver::toJSON(JsonResponse& json) {
json.beginObject("config");
this->config.toJSON(json);
json.endObject();
}
/*
bool Transceiver::toJSON(JsonObject& obj) {
//Serial.println("Setting Transceiver Json");
@ -4766,22 +4625,6 @@ void transceiver_config_t::fromJSON(JsonObject& obj) {
*/
Serial.printf("SCK:%u MISO:%u MOSI:%u CSN:%u RX:%u TX:%u\n", this->SCKPin, this->MISOPin, this->MOSIPin, this->CSNPin, this->RXPin, this->TXPin);
}
void transceiver_config_t::toJSON(JsonResponse &json) {
json.addElem("type", this->type);
json.addElem("TXPin", this->TXPin);
json.addElem("RXPin", this->RXPin);
json.addElem("SCKPin", this->SCKPin);
json.addElem("MOSIPin", this->MOSIPin);
json.addElem("MISOPin", this->MISOPin);
json.addElem("CSNPin", this->CSNPin);
json.addElem("rxBandwidth", this->rxBandwidth); // float
json.addElem("frequency", this->frequency); // float
json.addElem("deviation", this->deviation); // float
json.addElem("txPower", this->txPower);
json.addElem("proto", static_cast<uint8_t>(this->proto));
json.addElem("enabled", this->enabled);
json.addElem("radioInit", this->radioInit);
}
/*
void transceiver_config_t::toJSON(JsonObject& obj) {
obj["type"] = this->type;

View file

@ -30,10 +30,10 @@ enum class radio_proto : byte { // Ordinal byte 0-255
};
enum class somfy_commands : byte {
Unknown0 = 0x0,
My = 0x1,
Up = 0x2,
MyUp = 0x3,
Down = 0x4,
My = 0x2,//DOWN
Up = 0x1,
MyUp = 0x4, //up
Down = 0x3,
MyDown = 0x5,
UpDown = 0x6,
MyUpDown = 0x7,
@ -209,7 +209,6 @@ class SomfyRoom {
void clear();
bool save();
bool fromJSON(JsonObject &obj);
void toJSON(JsonResponse &json);
void emitState(const char *evt = "roomState");
void emitState(uint8_t num, const char *evt = "roomState");
void publish();
@ -239,7 +238,6 @@ class SomfyRemote {
uint8_t repeats = 1;
virtual bool isLastCommand(somfy_commands cmd);
char *getRemotePrefId() {return m_remotePrefId;}
virtual void toJSON(JsonResponse &json);
virtual void setRemoteAddress(uint32_t address);
virtual uint32_t getRemoteAddress();
virtual uint16_t getNextRollingCode();
@ -304,9 +302,7 @@ class SomfyShade : public SomfyRemote {
SomfyLinkedRemote linkedRemotes[SOMFY_MAX_LINKED_REMOTES];
bool paired = false;
int8_t validateJSON(JsonObject &obj);
void toJSONRef(JsonResponse &json);
int8_t fromJSON(JsonObject &obj);
void toJSON(JsonResponse &json) override;
char name[21] = "";
void setShadeId(uint8_t id) { shadeId = id; }
@ -393,9 +389,6 @@ class SomfyGroup : public SomfyRemote {
void clear();
bool fromJSON(JsonObject &obj);
//bool toJSON(JsonObject &obj);
void toJSON(JsonResponse &json);
void toJSONRef(JsonResponse &json);
bool linkShade(uint8_t shadeId);
bool unlinkShade(uint8_t shadeId);
bool hasShadeId(uint8_t shadeId);
@ -485,7 +478,6 @@ struct transceiver_config_t {
*/
void fromJSON(JsonObject& obj);
//void toJSON(JsonObject& obj);
void toJSON(JsonResponse& json);
void save();
void load();
void apply();
@ -500,7 +492,6 @@ class Transceiver {
transceiver_config_t config;
bool printBuffer = false;
//bool toJSON(JsonObject& obj);
void toJSON(JsonResponse& json);
bool fromJSON(JsonObject& obj);
bool save();
bool begin();
@ -557,10 +548,6 @@ class SomfyShadeController {
SomfyGroup groups[SOMFY_MAX_GROUPS];
bool linkRepeater(uint32_t address);
bool unlinkRepeater(uint32_t address);
void toJSONShades(JsonResponse &json);
void toJSONRooms(JsonResponse &json);
void toJSONGroups(JsonResponse &json);
void toJSONRepeaters(JsonResponse &json);
uint8_t repeaterCount();
uint8_t roomCount();
uint8_t shadeCount();
@ -578,6 +565,7 @@ class SomfyShadeController {
void processWaitingFrame();
void commit();
void writeBackup();
String backupData;
bool loadShadesFile(const char *filename);
#ifdef USE_NVS
bool loadLegacy();

View file

@ -1,3 +1,5 @@
#define LOG_LOCAL_LEVEL ESP_LOG_INFO
#include "esp_log.h"
#include <WiFi.h>
#include <LittleFS.h>
#include <esp_task_wdt.h>
@ -9,6 +11,7 @@
#include "Somfy.h"
#include "MQTT.h"
#include "GitOTA.h"
#include "esp_core_dump.h"
ConfigSettings settings;
Web webServer;
@ -20,11 +23,55 @@ MQTTClass mqtt;
GitUpdater git;
uint32_t oldheap = 0;
void listDir(const char *dirname, uint8_t levels) {
Serial.printf("Listing: %s\n", dirname);
File root = LittleFS.open(dirname);
if (!root || !root.isDirectory()) {
Serial.println("Failed to open directory");
return;
}
File file = root.openNextFile();
while (file) {
if (file.isDirectory()) {
Serial.printf(" DIR : %s\n", file.name());
if (levels) listDir(file.path(), levels - 1);
} else {
Serial.printf(" FILE: %-30s %d bytes\n", file.name(), file.size());
}
file = root.openNextFile();
}
}
void setup() {
Serial.begin(115200);
Serial.println();
Serial.println("Startup/Boot....");
esp_core_dump_summary_t summary;
if (esp_core_dump_get_summary(&summary) == ESP_OK) {
Serial.println("*** Previous crash coredump found ***");
Serial.printf(" Task: %s\n", summary.exc_task);
Serial.printf(" PC: 0x%08x\n", summary.exc_pc);
Serial.printf(" Cause: %d\n", summary.ex_info.exc_cause);
Serial.printf(" Backtrace:");
for (int i = 0; i < summary.exc_bt_info.depth; i++) {
Serial.printf(" 0x%08x", summary.exc_bt_info.bt[i]);
}
Serial.println();
}
Serial.println("Mounting File System...");
if (LittleFS.begin()) {
Serial.printf("\nTotal: %d bytes\n", LittleFS.totalBytes());
Serial.printf("Used: %d bytes\n", LittleFS.usedBytes());
Serial.printf("Free: %d bytes\n", LittleFS.totalBytes() - LittleFS.usedBytes());
Serial.println();
listDir("/", 3);
} else {
Serial.println("LittleFS mount failed!");
}
if(LittleFS.begin()) Serial.println("File system mounted successfully");
else Serial.println("Error mounting file system");
settings.begin();
@ -37,7 +84,7 @@ void setup() {
net.setup();
somfy.begin();
//git.checkForUpdate();
esp_task_wdt_init(7, true); //enable panic so ESP32 restarts
esp_task_wdt_init(15, true); //enable panic so ESP32 restarts
esp_task_wdt_add(NULL); //add current thread to WDT watch
}

View file

@ -1,5 +1,5 @@
#include "WResp.h"
void JsonSockEvent::beginEvent(WebSocketsServer *server, const char *evt, char *buff, size_t buffSize) {
void JsonSockEvent::beginEvent(AsyncWebSocket *server, const char *evt, char *buff, size_t buffSize) {
this->server = server;
this->buff = buff;
this->buffSize = buffSize;
@ -15,10 +15,10 @@ void JsonSockEvent::closeEvent() {
this->_nocomma = true;
this->_closed = true;
}
void JsonSockEvent::endEvent(uint8_t num) {
void JsonSockEvent::endEvent(uint32_t clientId) {
this->closeEvent();
if(num == 255) this->server->broadcastTXT(this->buff);
else this->server->sendTXT(num, this->buff);
if(clientId == 0) this->server->textAll(this->buff);
else this->server->text(clientId, this->buff);
}
void JsonSockEvent::_safecat(const char *val, bool escape) {
size_t len = (escape ? this->calcEscapedLength(val) : strlen(val)) + strlen(this->buff);
@ -33,30 +33,32 @@ void JsonSockEvent::_safecat(const char *val, bool escape) {
else strcat(this->buff, val);
if(escape) strcat(this->buff, "\"");
}
void JsonResponse::beginResponse(WebServer *server, char *buff, size_t buffSize) {
this->server = server;
void AsyncJsonResp::beginResponse(AsyncWebServerRequest *request, char *buff, size_t buffSize) {
this->_request = request;
this->buff = buff;
this->buffSize = buffSize;
this->buff[0] = 0x00;
this->_nocomma = true;
server->setContentLength(CONTENT_LENGTH_UNKNOWN);
this->_headersSent = false;
this->_stream = request->beginResponseStream("application/json");
}
void JsonResponse::endResponse() {
if(strlen(buff)) this->send();
server->sendContent("", 0);
void AsyncJsonResp::endResponse() {
if(strlen(this->buff)) this->flush();
if(this->_request && this->_stream) {
this->_request->send(this->_stream);
}
void JsonResponse::send() {
if(!this->_headersSent) server->send_P(200, "application/json", this->buff);
else server->sendContent(this->buff);
//Serial.printf("Sent %d bytes %d\n", strlen(this->buff), this->buffSize);
}
void AsyncJsonResp::flush() {
if(this->_stream && strlen(this->buff) > 0) {
this->_stream->print(this->buff);
this->buff[0] = 0x00;
this->_headersSent = true;
}
void JsonResponse::_safecat(const char *val, bool escape) {
}
void AsyncJsonResp::_safecat(const char *val, bool escape) {
size_t len = (escape ? this->calcEscapedLength(val) : strlen(val)) + strlen(this->buff);
if(escape) len += 2;
if(len >= this->buffSize) {
this->send();
this->flush();
}
if(escape) strcat(this->buff, "\"");
if(escape) this->escapeString(val, &this->buff[strlen(this->buff)]);
@ -130,8 +132,9 @@ void JsonFormatter::addElem(const char *name, uint32_t nval) { sprintf(this->_nu
void JsonFormatter::addElem(const char *name, int16_t nval) { sprintf(this->_numbuff, "%d", nval); this->_appendNumber(name); }
void JsonFormatter::addElem(const char *name, uint16_t nval) { sprintf(this->_numbuff, "%u", nval); this->_appendNumber(name); }
void JsonFormatter::addElem(const char *name, int64_t lval) { sprintf(this->_numbuff, "%lld", (long long)lval); this->_appendNumber(name); }
void JsonFormatter::addElem(const char *name, uint64_t lval) { sprintf(this->_numbuff, "%llu", (unsigned long long)lval); this->_appendNumber(name); }
*/
void JsonFormatter::addElem(const char *name, uint64_t lval) { sprintf(this->_numbuff, "%llu", (unsigned long long)lval); this->_appendNumber(name); }
void JsonFormatter::addElem(const char *name, bool bval) { strcpy(this->_numbuff, bval ? "true" : "false"); this->_appendNumber(name); }
void JsonFormatter::_safecat(const char *val, bool escape) {

View file

@ -1,5 +1,4 @@
#include <WebServer.h>
#include <WebSocketsServer.h>
#include <ESPAsyncWebServer.h>
#include "Somfy.h"
#ifndef wresp_h
#define wresp_h
@ -51,24 +50,26 @@ class JsonFormatter {
void addElem(const char* name, uint32_t lval);
void addElem(const char* name, bool bval);
void addElem(const char *name, const char *val);
void addElem(const char* name, uint64_t lval);
};
class JsonResponse : public JsonFormatter {
class AsyncJsonResp : public JsonFormatter {
protected:
void _safecat(const char *val, bool escape = false) override;
AsyncWebServerRequest *_request = nullptr;
AsyncResponseStream *_stream = nullptr;
public:
WebServer *server;
void beginResponse(WebServer *server, char *buff, size_t buffSize);
void beginResponse(AsyncWebServerRequest *request, char *buff, size_t buffSize);
void endResponse();
void send();
void flush();
};
class JsonSockEvent : public JsonFormatter {
protected:
bool _closed = false;
void _safecat(const char *val, bool escape = false) override;
public:
WebSocketsServer *server = nullptr;
void beginEvent(WebSocketsServer *server, const char *evt, char *buff, size_t buffSize);
void endEvent(uint8_t clientNum = 255);
AsyncWebSocket *server = nullptr;
void beginEvent(AsyncWebSocket *server, const char *evt, char *buff, size_t buffSize);
void endEvent(uint32_t clientId = 0);
void closeEvent();
};
#endif

2306
src/Web.cpp Normal file

File diff suppressed because it is too large Load diff

40
src/Web.h Normal file
View file

@ -0,0 +1,40 @@
#include <ESPAsyncWebServer.h>
#include <AsyncJson.h>
#include "Somfy.h"
#ifndef webserver_h
#define webserver_h
class Web {
public:
bool uploadSuccess = false;
void startup();
void begin();
void loop();
// Auth helpers
bool createAPIToken(const IPAddress ipAddress, char *token);
bool createAPIToken(const char *payload, char *token);
bool createAPIPinToken(const IPAddress ipAddress, const char *pin, char *token);
bool createAPIPasswordToken(const IPAddress ipAddress, const char *username, const char *password, char *token);
bool isAuthenticated(AsyncWebServerRequest *request, bool cfg = false);
// Async API handlers
void handleDiscovery(AsyncWebServerRequest *request);
void handleGetRooms(AsyncWebServerRequest *request);
void handleGetShades(AsyncWebServerRequest *request);
void handleGetGroups(AsyncWebServerRequest *request);
void handleController(AsyncWebServerRequest *request);
void handleRoom(AsyncWebServerRequest *request);
void handleShade(AsyncWebServerRequest *request);
void handleGroup(AsyncWebServerRequest *request);
void handleLogin(AsyncWebServerRequest *request, JsonVariant &json);
void handleShadeCommand(AsyncWebServerRequest *request, JsonVariant &json);
void handleGroupCommand(AsyncWebServerRequest *request, JsonVariant &json);
void handleTiltCommand(AsyncWebServerRequest *request, JsonVariant &json);
void handleRepeatCommand(AsyncWebServerRequest *request, JsonVariant &json);
void handleSetPositions(AsyncWebServerRequest *request, JsonVariant &json);
void handleSetSensor(AsyncWebServerRequest *request, JsonVariant &json);
void handleDownloadFirmware(AsyncWebServerRequest *request);
void handleBackup(AsyncWebServerRequest *request);
void handleReboot(AsyncWebServerRequest *request);
void handleNotFound(AsyncWebServerRequest *request);
};
#endif

11
test/README Normal file
View file

@ -0,0 +1,11 @@
This directory is intended for PlatformIO Test Runner and project tests.
Unit Testing is a software testing method by which individual units of
source code, sets of one or more MCU program modules together with associated
control data, usage procedures, and operating procedures, are tested to
determine whether they are fit for use. Unit testing finds problems early
in the development cycle.
More information about PlatformIO Unit Testing:
- https://docs.platformio.org/en/latest/advanced/unit-testing/index.html