mirror of
https://github.com/rstrouse/ESPSomfy-RTS.git
synced 2026-10-31 13:52:15 +01:00
Merge origin/main into test/config-records
Both sides added a helper at the end of test/e2e/test_settings.py (boot_time here, header on main); both are kept. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
commit
afbfac5181
20 changed files with 149 additions and 40 deletions
23
.github/workflows/build.yaml
vendored
23
.github/workflows/build.yaml
vendored
|
|
@ -60,10 +60,9 @@ jobs:
|
|||
# fwname: firmware-only binary for OTA updates
|
||||
# obname: onboard image (bootloader + partitions + firmware + littlefs merged)
|
||||
# for flashing a new chip via USB/serial
|
||||
# addr_bootloader: chip-dependent (ESP32: 0x1000, S3/C6: 0x0)
|
||||
# No C3: on ESP-IDF 5.5 its image no longer fits the 1.5 MB OTA slot
|
||||
# (docs/adr/0003-pioarduino-idf55.md). C3 boards stay on 3.0.x.
|
||||
# addr_fs: must match spiffs/littlefs offset in esp32_3MB.csv
|
||||
# addr_bootloader: chip-dependent (ESP32: 0x1000, S3/C3/C6: 0x0, C5: 0x2000)
|
||||
# addr_fs: must match the spiffs offset in the env's partition table
|
||||
# (0x310000 in both esp32_3MB.csv and esp32_huge_app.csv)
|
||||
include:
|
||||
- env: esp32dev
|
||||
name: ESP32
|
||||
|
|
@ -81,6 +80,22 @@ jobs:
|
|||
fwname: SomfyController.ino.esp32s3.bin
|
||||
fsname: SomfyController.littlefs.esp32s3.bin
|
||||
obname: SomfyController.onboard.esp32s3.bin
|
||||
- env: esp32c3
|
||||
name: ESP32-C3
|
||||
chip: ESP32-C3
|
||||
addr_bootloader: "0x0"
|
||||
addr_fs: "0x310000"
|
||||
fwname: SomfyController.ino.esp32c3.bin
|
||||
fsname: SomfyController.littlefs.esp32c3.bin
|
||||
obname: SomfyController.onboard.esp32c3.bin
|
||||
- env: esp32c5
|
||||
name: ESP32-C5
|
||||
chip: ESP32-C5
|
||||
addr_bootloader: "0x2000"
|
||||
addr_fs: "0x310000"
|
||||
fwname: SomfyController.ino.esp32c5.bin
|
||||
fsname: SomfyController.littlefs.esp32c5.bin
|
||||
obname: SomfyController.onboard.esp32c5.bin
|
||||
- env: esp32c6
|
||||
name: ESP32-C6
|
||||
chip: ESP32-C6
|
||||
|
|
|
|||
|
|
@ -2719,6 +2719,7 @@ class Somfy {
|
|||
{ name: 's2', maxPins: 46, inputs: [0, 19, 20, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 45], outputs: [0, 19, 20, 26, 27, 28, 29, 30, 31, 32, 45, 46]},
|
||||
{ name: 's3', maxPins: 48, inputs: [19, 20, 22, 23, 24, 25, 27, 28, 29, 30, 31, 32], outputs: [19, 20, 22, 23, 24, 25, 27, 28, 29, 30, 31, 32] },
|
||||
{ name: 'c3', maxPins: 21, inputs: [11, 12, 13, 14, 15, 16, 17, 18, 19, 20], outputs: [11, 12, 13, 14, 15, 16, 17, 21] },
|
||||
{ name: 'c5', maxPins: 28, inputs: [13, 14, 15, 16, 17, 18, 19, 20, 21, 22], outputs: [13, 14, 15, 16, 17, 18, 19, 20, 21, 22] },
|
||||
{ name: 'c6', maxPins: 23, inputs: [], outputs: [] }
|
||||
];
|
||||
|
||||
|
|
@ -4169,9 +4170,14 @@ class Firmware {
|
|||
div.style.display = '';
|
||||
}
|
||||
updateFirmware() {
|
||||
let chip = document.getElementById('divContainer').getAttribute('data-chipmodel') || '';
|
||||
// Firmware from before the field leaves it out: undefined, not false.
|
||||
if (proto.model.header.hasOtaSlot === false) {
|
||||
ui.infoMessage(`This device has no OTA slot, so its firmware is flashed over USB with SomfyController.onboard.esp32${chip}.bin, from the release's SomfyController.onboard.esp32${chip}.bin.zip. That image holds the web UI and an empty configuration too, so take a backup first and restore it afterwards.`);
|
||||
return;
|
||||
}
|
||||
let div = this.createFileUploader('/updateFirmware');
|
||||
let inst = div.querySelector('div[id=divInstText]');
|
||||
let chip = document.getElementById('divContainer').getAttribute('data-chipmodel') || '';
|
||||
let html = `<div style="font-size:14px;margin-bottom:20px;">Select a binary file [SomfyController.ino.esp32${chip}.bin] containing the device firmware then press the Upload File button.</div>`;
|
||||
if (this.isMobile()) {
|
||||
html += `<div style="width:100%;color:red;text-align:center;font-weight:bold;"><span style="margin-top:7px;width:100%;background:yellow;padding:3px;display:inline-block;border-radius:5px;background:white;">WARNING<span></div>`;
|
||||
|
|
@ -4302,18 +4308,15 @@ class Firmware {
|
|||
security.tokenRefused();
|
||||
return;
|
||||
}
|
||||
if (service !== '/restore' && (xhr.status < 200 || xhr.status >= 300)) {
|
||||
ui.errorMessage(el, `The upload failed (${xhr.status}).`);
|
||||
if (xhr.status < 200 || xhr.status >= 300) {
|
||||
// The device's reason, when it sent one.
|
||||
let desc = `The upload failed (${xhr.status}).`;
|
||||
try { desc = JSON.parse(xhr.responseText).desc || desc; } catch (e) { }
|
||||
ui.errorMessage(el, desc);
|
||||
return;
|
||||
}
|
||||
switch (service) {
|
||||
case '/restore':
|
||||
if (xhr.status < 200 || xhr.status >= 300) {
|
||||
let desc = 'The restore failed.';
|
||||
try { desc = JSON.parse(xhr.responseText).desc || desc; } catch (e) { }
|
||||
ui.errorMessage(el, desc);
|
||||
break;
|
||||
}
|
||||
(async () => {
|
||||
await somfy.init();
|
||||
if (document.getElementById('divUploadFile')) document.getElementById('divUploadFile').remove();
|
||||
|
|
|
|||
|
|
@ -140,7 +140,7 @@ ProtoModel.ITEMS = [
|
|||
// have it.
|
||||
ProtoModel.FIELDS = {
|
||||
header: ['maxRooms', 'maxShades', 'maxGroups', 'maxGroupedShades', 'maxLinkedRemotes', 'startingAddress', 'bitLength',
|
||||
'proto', 'serverId', 'hostname', 'model', 'chipModel', 'fw', 'connType'],
|
||||
'proto', 'serverId', 'hostname', 'model', 'chipModel', 'hasOtaSlot', 'fw', 'connType'],
|
||||
room: ['roomId', 'name', 'sortOrder'],
|
||||
shadeState: ['shadeId', 'position', 'target', 'direction', 'myPos', 'tiltPosition', 'tiltTarget', 'tiltDirection',
|
||||
'myTiltPos', 'flags', 'lastRollingCode'],
|
||||
|
|
|
|||
|
|
@ -53,7 +53,7 @@ A number is never reused. A retired type stays reserved.
|
|||
- `F`: fields, as `[key, name, kind, default]`, kind being `b` bool, `i` integer, `n` number, `s` string, `l` list, `o` object
|
||||
- `E`: error codes, as `[number, name]`
|
||||
- `Q`: feeds, as `[number, name]`
|
||||
3. Then the device: `header` (limits, starting address, what a new shade gets, server id, hostname, model, chip, firmware, connection type), `rooms`, `shades`, `groups`, `repeaters` and `status` (memory, and WiFi or Ethernet). A frame is at most 4 KB, an item is never split across frames, and several sections can share one.
|
||||
3. Then the device: `header` (limits, starting address, what a new shade gets, server id, hostname, model, chip, whether it has an OTA slot, firmware, connection type), `rooms`, `shades`, `groups`, `repeaters` and `status` (memory, and WiFi or Ethernet). A frame is at most 4 KB, an item is never split across frames, and several sections can share one.
|
||||
4. The client sends `next` after each `sync` frame, and the device answers with the following one. With no `next` for 30 s the device closes the socket.
|
||||
5. `syncEnd` follows the last `sync` frame.
|
||||
|
||||
|
|
|
|||
|
|
@ -12,7 +12,10 @@ We moved every firmware env from `espressif32 @ 6.13.0` (Arduino 2.0.17 on ESP-I
|
|||
|
||||
- **Firmware size.** The ESP32 image grows from 1,382,640 bytes (v3.0.14) to 1,555,472 at `CORE_DEBUG_LEVEL=2`, leaving 17 KB of the 1.5 MB OTA slot. At level 3 it doesn't fit, so release envs build at level 2 (warnings and errors) and debug envs stay at 3. PlatformIO's "Flash:" figure counts ELF sections, not the image, and reads about 45 KB low, so CI checks `firmware.bin` against the app partition and fails under 8 KB free (`.github/size_check.py`).
|
||||
- **No more C3 builds.** Its image grows by 214 KB to 1,651,216 bytes, 76.5 KB over the OTA slot even at level 2. C3 boards stay on the 3.0.x releases.
|
||||
|
||||
> **Amended by [ADR 0006](0006-two-security-modes-eleven-http-routes.md) (4.0.0).** The C3 builds again. Without the old API (ports 8080 and 8081), its image is 1,498,512 bytes at level 2 and leaves 72.6 KB of the OTA slot. It runs on chip revision v0.3 or later only: the framework's C3 libraries are built with `CONFIG_ESP32C3_REV_MIN_3`. Its default radio pins moved off GPIO 12-17, which hold the SPI flash.
|
||||
- **The C5 is updated over USB.** New in 4.0.0. At level 2 its image left 3.4 KB of the OTA slot, under CI's 8 KB, so like the C6 it is released as a debug build with one 3 MB app slot (`esp32_huge_app.csv`), which no OTA can write. The framework's C5 libraries run on chip revision v1.0 or later only (`CONFIG_ESP32C5_REV_MIN_FULL=100`). Its WiFi has 5 GHz too, so the passive scans for the configured network (`src/ESPNetwork.cpp`, 300 ms a channel) cover about 38 channels instead of 13: about 11 s instead of 4 s, depending on the country's channels.
|
||||
- **Ethernet PHY ids are ours.** Arduino 3.x renumbered `eth_phy_type_t` (`ETH_PHY_GENERIC` is 0). NVS, backups and the web UI keep the Arduino 2.x numbers as `eth_phy_id_t` (`src/EthPhy.h`), translated by name when Ethernet starts. Never cast a stored id to the framework enum.
|
||||
- **Ethernet only on the original ESP32.** Arduino 3.x has the RMII MAC API only where `CONFIG_ETH_USE_ESP32_EMAC` is set. S3 and C6 build without Ethernet.
|
||||
- **Ethernet only on the original ESP32.** Arduino 3.x has the RMII MAC API only where `CONFIG_ETH_USE_ESP32_EMAC` is set. S3, C3, C5 and C6 build without Ethernet.
|
||||
- **Updates roll back until proven.** We override the core's `verifyRollbackLater()` hook to return true, and `loop()` marks the new firmware valid once it has run 60 s and the network is up. A reset before then boots the previous firmware. The LittleFS image an update wrote stays, so the old firmware runs with the new web UI.
|
||||
- **Tooling.** pioarduino needs its own Core fork (`pip install pioarduino`, the pioarduino IDE extension in VS Code), which it installs into `~/.platformio/penv` in place of PlatformIO Core.
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@ extra_scripts =
|
|||
board_build.filesystem = littlefs
|
||||
; Each env adds a CORE_DEBUG_LEVEL: 2 (warnings) for release builds, which
|
||||
; only fit the 1.5 MB OTA slot without the info logs, 3 for debug builds.
|
||||
; The C5 and C6 release their debug build, which has a 3 MB slot.
|
||||
build_flags =
|
||||
-DCONFIG_ESP_COREDUMP_ENABLE_TO_FLASH=1
|
||||
-DCONFIG_ESP_COREDUMP_DATA_FORMAT_ELF=1
|
||||
|
|
@ -95,6 +96,25 @@ build_flags =
|
|||
${dualcore.build_flags}
|
||||
-DCORE_DEBUG_LEVEL=2
|
||||
|
||||
[env:esp32c3]
|
||||
extends = esp32
|
||||
board = esp32-c3-devkitm-1
|
||||
board_build.partitions = esp32_3MB.csv
|
||||
build_flags =
|
||||
${esp32.build_flags}
|
||||
-DCORE_DEBUG_LEVEL=2
|
||||
|
||||
; Released like the C6 below. At level 2 its image left 3.4 KB of the
|
||||
; 1.5 MB OTA slot, under CI's 8 KB.
|
||||
[env:esp32c5]
|
||||
extends = esp32
|
||||
board = esp32-c5-devkitc-1
|
||||
build_type = debug
|
||||
board_build.partitions = esp32_huge_app.csv
|
||||
build_flags =
|
||||
${esp32.build_flags}
|
||||
-DCORE_DEBUG_LEVEL=3
|
||||
|
||||
; Released as a debug build with one 3 MB app slot, so C6 boards update over
|
||||
; USB, not OTA.
|
||||
[env:esp32c6]
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@
|
|||
#include "Utils.h"
|
||||
#include "Secrets.h"
|
||||
#include "esp_chip_info.h"
|
||||
#include "esp_ota_ops.h"
|
||||
#include "esp_log.h"
|
||||
|
||||
static const char *TAG = "Config";
|
||||
|
|
@ -134,6 +135,11 @@ bool ConfigSettings::begin() {
|
|||
case esp_chip_model_t::CHIP_ESP32C3:
|
||||
strcpy(this->chipModel, "c3");
|
||||
break;
|
||||
#ifdef CONFIG_IDF_TARGET_ESP32C5
|
||||
case esp_chip_model_t::CHIP_ESP32C5:
|
||||
strcpy(this->chipModel, "c5");
|
||||
break;
|
||||
#endif
|
||||
#ifdef CONFIG_IDF_TARGET_ESP32C6
|
||||
case esp_chip_model_t::CHIP_ESP32C6:
|
||||
strcpy(this->chipModel, "c6");
|
||||
|
|
@ -144,6 +150,7 @@ bool ConfigSettings::begin() {
|
|||
break;
|
||||
}
|
||||
ESP_LOGD(TAG, "Chip Model ESP32-%s", this->chipModel);
|
||||
this->hasOtaSlot = esp_ota_get_next_update_partition(nullptr) != nullptr;
|
||||
this->fwVersion.parse(FW_VERSION);
|
||||
uint64_t mac = ESP.getEfuseMac();
|
||||
for(int i=0; i<17; i=i+8) {
|
||||
|
|
|
|||
|
|
@ -163,6 +163,9 @@ class ConfigSettings: BaseSettings {
|
|||
char serverId[10] = "";
|
||||
char hostname[32] = "ESPSomfyRTS";
|
||||
char chipModel[10] = "ESP32";
|
||||
// Whether the partition table has a second app slot for /updateFirmware
|
||||
// to write. Builds with one 3 MB slot (C5, C6, debug) are flashed over USB.
|
||||
bool hasOtaSlot = true;
|
||||
conn_types_t connType = conn_types_t::unset;
|
||||
appver_t fwVersion;
|
||||
appver_t appVersion;
|
||||
|
|
|
|||
|
|
@ -5,8 +5,8 @@
|
|||
#include <sdkconfig.h>
|
||||
#endif
|
||||
|
||||
// Ethernet needs the RMII MAC, which only the original ESP32 has. The C3, S3
|
||||
// and C6 build without it.
|
||||
// Ethernet needs the RMII MAC, which only the original ESP32 has. The C3, C5,
|
||||
// S3 and C6 build without it.
|
||||
#if defined(CONFIG_ETH_USE_ESP32_EMAC) && CONFIG_ETH_USE_ESP32_EMAC
|
||||
#define HAS_ETHERNET 1
|
||||
#else
|
||||
|
|
|
|||
|
|
@ -169,6 +169,7 @@
|
|||
FIELD(hostname, "hn", 's', 0) \
|
||||
FIELD(model, "md", 's', 0) \
|
||||
FIELD(chipModel, "cp", 's', 0) \
|
||||
FIELD(hasOtaSlot, "os", 'b', 1) \
|
||||
FIELD(connType, "ct", 's', 0) \
|
||||
/* memory, wifi and ethernet */ \
|
||||
FIELD(freeHeap, "fr", 'i', 0) \
|
||||
|
|
|
|||
|
|
@ -474,13 +474,28 @@ void transceiver_config_t::load() {
|
|||
this->CSNPin = 34;
|
||||
break;
|
||||
case esp_chip_model_t::CHIP_ESP32C3:
|
||||
this->TXPin = 13;
|
||||
this->RXPin = 12;
|
||||
this->MOSIPin = 16;
|
||||
this->MISOPin = 17;
|
||||
this->SCKPin = 15;
|
||||
this->CSNPin = 14;
|
||||
// The framework's default SPI pins (variants/esp32c3). 12-17 hold the
|
||||
// SPI flash, and 2, 8 and 9 are strapping pins.
|
||||
this->TXPin = 10;
|
||||
this->RXPin = 3;
|
||||
this->MOSIPin = 6;
|
||||
this->MISOPin = 5;
|
||||
this->SCKPin = 4;
|
||||
this->CSNPin = 7;
|
||||
break;
|
||||
#ifdef CONFIG_IDF_TARGET_ESP32C5
|
||||
case esp_chip_model_t::CHIP_ESP32C5:
|
||||
// The framework's default SPI pins (variants/esp32c5). TX and RX take
|
||||
// 4 and 5, the LP UART's pins, which nothing here uses; 2, 3, 7 and
|
||||
// 25-28 are strapping pins and 11-22 hold UART0, USB and the flash.
|
||||
this->TXPin = 4;
|
||||
this->RXPin = 5;
|
||||
this->MOSIPin = 8;
|
||||
this->MISOPin = 9;
|
||||
this->SCKPin = 10;
|
||||
this->CSNPin = 6;
|
||||
break;
|
||||
#endif
|
||||
#ifdef CONFIG_IDF_TARGET_ESP32C6
|
||||
case esp_chip_model_t::CHIP_ESP32C6:
|
||||
// Pinout applicable for the ESP32-C6-WROOM-1 module (ESP32-C6-DevKitC-1-N4)
|
||||
|
|
|
|||
|
|
@ -85,7 +85,7 @@ const char *wifiAuthModeName(uint8_t authMode);
|
|||
bool otaProven(bool networkUp, unsigned long uptimeMs, unsigned long minUptimeMs);
|
||||
// A release asset's name: "SomfyController.<kind>.esp32<chipModel>.bin", kind
|
||||
// "ino" for the firmware or "littlefs" for the web UI image, chipModel as in
|
||||
// ConfigSettings ("" for the ESP32, "s3", "c3", "c6").
|
||||
// ConfigSettings ("" for the ESP32, "s3", "c3", "c5", "c6").
|
||||
void releaseAssetName(char *buf, size_t size, const char *kind, const char *chipModel);
|
||||
// A coredump backtrace as " 0x%08x" per frame. bt holds maxFrames entries and
|
||||
// depth is how many the dump claims are valid.
|
||||
|
|
|
|||
|
|
@ -164,15 +164,21 @@ void registerHttpRoutes(AsyncWebServer &server) {
|
|||
// updateFirmware - file upload
|
||||
server.on(AsyncURIMatcher::exact("/updateFirmware"), HTTP_POST,
|
||||
[](AsyncWebServerRequest *request) {
|
||||
// Nothing was written or stopped, so there is nothing to reboot for.
|
||||
if(!settings.hasOtaSlot) {
|
||||
request->send(400, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"This device has no OTA slot, so its firmware is flashed over USB.\"}"));
|
||||
return;
|
||||
}
|
||||
// errorString() is one of the updater's fixed messages, none with a quote.
|
||||
if(Update.hasError())
|
||||
request->send(500, _encoding_json, "{\"status\":\"ERROR\",\"desc\":\"Error updating firmware: \"}");
|
||||
request->send(500, _encoding_json, String("{\"status\":\"ERROR\",\"desc\":\"Error updating firmware: ") + Update.errorString() + "\"}");
|
||||
else
|
||||
request->send(200, _encoding_json, "{\"status\":\"SUCCESS\",\"desc\":\"Successfully updated firmware\"}");
|
||||
rebootDelay.reboot = true;
|
||||
rebootDelay.rebootTime = millis() + 500;
|
||||
},
|
||||
[](AsyncWebServerRequest *request, String filename, size_t index, uint8_t *data, size_t len, bool final) {
|
||||
if(!uploadAllowed(request, index)) return;
|
||||
if(!uploadAllowed(request, index) || !settings.hasOtaSlot) return;
|
||||
if(index == 0) {
|
||||
webServer.uploadSuccess = false;
|
||||
ESP_LOGI(TAG, "Update: %s", filename.c_str());
|
||||
|
|
@ -207,7 +213,7 @@ void registerHttpRoutes(AsyncWebServer &server) {
|
|||
server.on(AsyncURIMatcher::exact("/updateApplication"), HTTP_POST,
|
||||
[](AsyncWebServerRequest *request) {
|
||||
if(Update.hasError())
|
||||
request->send(500, _encoding_json, "{\"status\":\"ERROR\",\"desc\":\"Error updating application: \"}");
|
||||
request->send(500, _encoding_json, String("{\"status\":\"ERROR\",\"desc\":\"Error updating application: ") + Update.errorString() + "\"}");
|
||||
else
|
||||
request->send(200, _encoding_json, "{\"status\":\"SUCCESS\",\"desc\":\"Successfully updated application\"}");
|
||||
rebootDelay.reboot = true;
|
||||
|
|
|
|||
|
|
@ -77,6 +77,7 @@ static void writeHeader(ProtoWriter &w) {
|
|||
w.add(proto_field_t::hostname, settings.hostname);
|
||||
w.add(proto_field_t::model, "ESPSomfyRTS");
|
||||
w.add(proto_field_t::chipModel, settings.chipModel);
|
||||
w.add(proto_field_t::hasOtaSlot, settings.hasOtaSlot);
|
||||
w.add(proto_field_t::fw, settings.fwVersion.name);
|
||||
w.add(proto_field_t::connType, net.connTypeName());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ Runs against a real device on your network, by hand, never in CI. It checks sock
|
|||
| `--lockout` | on a Secured device, also fail `/login` until this PC is refused, then wait out the 30 s lockout |
|
||||
| `--record` | save hello, the sync and the schema to `test/fixtures/proto2/`, for the web UI and Home Assistant tests |
|
||||
|
||||
A plain run sends nothing on air, changes no setting (it saves only values the device already has) and doesn't reboot the device. The command tests send ops that are refused before they reach the radio, and one `setPositions` that sets a shade to the position the sync gave: that drops the fraction of a percent the shade's believed position may have had. The edit tests send edits that are refused, and one shade sort order set to the order it has, which saves the shades again unchanged. The settings tests send saves that are refused, and one that saves the NTP server and time zone the device already has. The restore test sends options that aren't JSON with a file that isn't a backup, so nothing is restored. The frequency scan test pauses the radio's sending for a second or two, as a scan does. It does fill every socket slot for a few seconds, and replays a page load.
|
||||
A plain run sends nothing on air, changes no setting (it saves only values the device already has) and doesn't reboot the device. The command tests send ops that are refused before they reach the radio, and one `setPositions` that sets a shade to the position the sync gave: that drops the fraction of a percent the shade's believed position may have had. The edit tests send edits that are refused, and one shade sort order set to the order it has, which saves the shades again unchanged. The settings tests send saves that are refused, and one that saves the NTP server and time zone the device already has. The restore test sends options that aren't JSON with a file that isn't a backup, so nothing is restored. A firmware upload goes only to a device without an OTA slot, which refuses it. The frequency scan test pauses the radio's sending for a second or two, as a scan does. It does fill every socket slot for a few seconds, and replays a page load.
|
||||
|
||||
The web UI holds one of the device's 5 socket slots for each open tab, and Home Assistant holds one. The tests that fill every slot fill the ones left, so close the UI's tabs for the full load.
|
||||
|
||||
|
|
|
|||
|
|
@ -11,7 +11,8 @@ import urllib.request
|
|||
|
||||
import pytest
|
||||
|
||||
from proto_client import ProtoClient
|
||||
from proto_client import ProtoClient, run_sync
|
||||
from test_settings import header, uptime
|
||||
|
||||
# Every route that needs the token under Secured, sent without a body or
|
||||
# parameters, so a route that let one through would still change nothing.
|
||||
|
|
@ -169,3 +170,20 @@ def test_a_socket_from_the_page_the_device_served_is_accepted(host, token):
|
|||
c.close()
|
||||
# The device frees the slot when it sees the disconnect.
|
||||
time.sleep(0.5)
|
||||
|
||||
|
||||
def test_a_firmware_upload_without_an_ota_slot_is_refused(host, token, connect, schema):
|
||||
"""Builds with one app slot (C6, debug) say so in the header, and refuse
|
||||
the upload without stopping the radio or rebooting."""
|
||||
_, frames, _ = run_sync(connect(schema=schema.hash))
|
||||
# Left out at its default, true, as is a field older firmware doesn't have.
|
||||
if header(schema, frames).get("hasOtaSlot", True):
|
||||
pytest.skip("the device has an OTA slot: an upload would start an update")
|
||||
uptime_before = uptime(schema, frames)
|
||||
status, _, body = upload(host, "/updateFirmware", auth(token))
|
||||
assert status == 400, body
|
||||
assert "no OTA slot" in json.loads(body)["desc"]
|
||||
# The old reply rebooted half a second later.
|
||||
time.sleep(2)
|
||||
_, frames, _ = run_sync(connect(schema=schema.hash))
|
||||
assert uptime(schema, frames) > uptime_before, "the device restarted"
|
||||
|
|
|
|||
|
|
@ -86,9 +86,8 @@ def test_a_save_tells_the_other_clients(connect, schema):
|
|||
def test_a_wifi_scan_lists_the_networks(connect, schema):
|
||||
c = connect(schema=schema.hash)
|
||||
_, frames, _ = run_sync(c)
|
||||
header = next(schema.expand(f["d"][schema.keys["header"]]) for f in frames if schema.keys["header"] in f["d"])
|
||||
# The connection in use: Ethernet with WiFi as the fallback may be on either.
|
||||
if header.get("connType") != "Wifi":
|
||||
if header(schema, frames).get("connType") != "Wifi":
|
||||
pytest.skip("the device isn't on WiFi")
|
||||
assert is_ok(schema, op(c, schema, "scanWifi", {}))
|
||||
found = c.recv_until(lambda f: f["t"] == schema.type_numbers["wifiNetworks"], timeout=20)[-1]
|
||||
|
|
@ -159,6 +158,12 @@ def boot_time(schema, frames):
|
|||
return time.time() - uptime(schema, frames) / 1000
|
||||
|
||||
|
||||
def header(schema, frames):
|
||||
"""The header a sync starts with."""
|
||||
key = schema.keys["header"]
|
||||
return next(schema.expand(f["d"][key]) for f in frames if key in f["d"])
|
||||
|
||||
|
||||
def uptime(schema, frames):
|
||||
"""The uptime, in ms, from the status a sync ends with."""
|
||||
key = schema.keys["status"]
|
||||
|
|
|
|||
|
|
@ -37,6 +37,19 @@ test('a field the sync left out is its default', () => {
|
|||
for (const g of m.groupList()) assert.ok(Array.isArray(g.linkedShades));
|
||||
});
|
||||
|
||||
test('the device has an OTA slot unless it says it has none', () => {
|
||||
// The recording is from firmware before the field.
|
||||
assert.equal(synced().header.hasOtaSlot, undefined);
|
||||
const schema = recordedSchema();
|
||||
schema.add({ F: [['os', 'hasOtaSlot', 'b', 1]] });
|
||||
const header = deviceSections(schema).find((d) => d.header).header;
|
||||
const m = new ProtoModel(schema);
|
||||
m.applySync({ header: header });
|
||||
assert.equal(m.header.hasOtaSlot, true);
|
||||
m.applySync({ header: schema.expand(Object.assign({ os: 0 }, schema.pack(header))) });
|
||||
assert.equal(m.header.hasOtaSlot, false);
|
||||
});
|
||||
|
||||
test('the lists come in their sort order', () => {
|
||||
const m = synced();
|
||||
const orders = m.shadeList().map((s) => s.sortOrder);
|
||||
|
|
|
|||
|
|
@ -119,15 +119,17 @@ static void test_values_at_their_default_are_left_out() {
|
|||
w.add(proto_field_t::myPos, (int8_t)-1);
|
||||
w.add(proto_field_t::repeats, (uint8_t)1);
|
||||
w.add(proto_field_t::paired, false);
|
||||
w.add(proto_field_t::hasOtaSlot, true); // a bool whose default is true
|
||||
w.add(proto_field_t::name, "");
|
||||
w.add(proto_field_t::name, (const char *)nullptr);
|
||||
}).c_str());
|
||||
}
|
||||
static void test_values_off_their_default_are_written_by_key() {
|
||||
TEST_ASSERT_EQUAL_STRING("{\"p\":50,\"m\":0,\"i\":1,\"n\":\"Office\",\"a\":4294967295,\"q\":-5}", writeObject([](ProtoWriter &w) {
|
||||
TEST_ASSERT_EQUAL_STRING("{\"p\":50,\"m\":0,\"i\":1,\"os\":0,\"n\":\"Office\",\"a\":4294967295,\"q\":-5}", writeObject([](ProtoWriter &w) {
|
||||
w.add(proto_field_t::position, (int8_t)50);
|
||||
w.add(proto_field_t::myPos, (int8_t)0);
|
||||
w.add(proto_field_t::paired, true);
|
||||
w.add(proto_field_t::hasOtaSlot, false);
|
||||
w.add(proto_field_t::name, "Office");
|
||||
w.add(proto_field_t::remoteAddress, (uint32_t)0xFFFFFFFF);
|
||||
w.add(proto_field_t::tiltPosition, (int8_t)-5);
|
||||
|
|
|
|||
|
|
@ -278,14 +278,12 @@ static void test_releaseAssetName_matches_the_release_workflow() {
|
|||
TEST_ASSERT_EQUAL_STRING("SomfyController.ino.esp32.bin", buf);
|
||||
releaseAssetName(buf, sizeof(buf), "littlefs", "s3");
|
||||
TEST_ASSERT_EQUAL_STRING("SomfyController.littlefs.esp32s3.bin", buf);
|
||||
releaseAssetName(buf, sizeof(buf), "littlefs", "c6");
|
||||
TEST_ASSERT_EQUAL_STRING("SomfyController.littlefs.esp32c6.bin", buf);
|
||||
}
|
||||
static void test_releaseAssetName_keeps_c3() {
|
||||
// C3 is out of the IDF 5.5 builds for now (ADR 0003) but is coming back.
|
||||
char buf[64];
|
||||
releaseAssetName(buf, sizeof(buf), "ino", "c3");
|
||||
TEST_ASSERT_EQUAL_STRING("SomfyController.ino.esp32c3.bin", buf);
|
||||
releaseAssetName(buf, sizeof(buf), "ino", "c5");
|
||||
TEST_ASSERT_EQUAL_STRING("SomfyController.ino.esp32c5.bin", buf);
|
||||
releaseAssetName(buf, sizeof(buf), "littlefs", "c6");
|
||||
TEST_ASSERT_EQUAL_STRING("SomfyController.littlefs.esp32c6.bin", buf);
|
||||
}
|
||||
|
||||
/*********************************************************************
|
||||
|
|
@ -432,7 +430,6 @@ int main(int argc, char **argv) {
|
|||
RUN_TEST(test_otaProven_not_before_the_minimum_uptime);
|
||||
RUN_TEST(test_otaProven_not_without_network);
|
||||
RUN_TEST(test_releaseAssetName_matches_the_release_workflow);
|
||||
RUN_TEST(test_releaseAssetName_keeps_c3);
|
||||
RUN_TEST(test_formatBacktrace_frames_the_dump_holds);
|
||||
RUN_TEST(test_formatBacktrace_garbage_depth_stays_in_bounds);
|
||||
RUN_TEST(test_formatBacktrace_stops_when_the_buffer_is_full);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue