144
.github/workflows/ci.yaml
vendored
|
|
@ -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
|
||||
|
|
|
|||
192
.github/workflows/release.yaml
vendored
|
|
@ -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
|
|
@ -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/
|
||||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
203
Sockets.cpp
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
50
Web.h
|
|
@ -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
|
||||
|
Before Width: | Height: | Size: 6.5 KiB After Width: | Height: | Size: 6.5 KiB |
1
data-src/appversion
Normal file
|
|
@ -0,0 +1 @@
|
|||
2.4.8
|
||||
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 24 KiB |
|
|
@ -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">
|
||||
|
|
@ -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(' ');
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
@ -1 +0,0 @@
|
|||
2.4.7
|
||||
6
huge_app.csv
Normal 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,
|
||||
|
37
include/README
Normal 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
|
|
@ -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
|
|
@ -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,
|
||||
|
290
minify.py
Normal 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
|
|
@ -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
|
||||
|
||||
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
|
@ -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;
|
||||
|
|
@ -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();
|
||||
|
|
@ -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();
|
||||
|
|
@ -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);
|
||||
|
|
@ -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);
|
||||
221
src/Sockets.cpp
Normal 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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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;
|
||||
|
|
@ -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();
|
||||
|
|
@ -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
|
||||
|
||||
}
|
||||
|
|
@ -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) {
|
||||
|
|
@ -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
40
src/Web.h
Normal 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
|
|
@ -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
|
||||