commit 3299717d3de47e4650b76f0863e4df71face4479 Author: Justin Ko Date: Sun Oct 4 11:53:38 2026 +0900 Initial sese-controller application diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..790a176 --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +build/ +managed_components/ +sdkconfig +sdkconfig.old +*.log +build-*/ +examples/minimal_consumer/build-*/ +sdkconfig* +test/host/test_ota_version diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..cff0b57 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "components/kesp32-app-base"] + path = components/kesp32-app-base + url = https://gitea.jsfamily.quest/jsfamily/kesp32-app-base.git diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..9275e67 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,6 @@ +cmake_minimum_required(VERSION 3.22) + +include($ENV{IDF_PATH}/tools/cmake/project.cmake) +idf_build_set_property(MINIMAL_BUILD ON) +set(PROJECT_VER "0.5.0") +project(sese-controller) diff --git a/README.md b/README.md new file mode 100644 index 0000000..2b928de --- /dev/null +++ b/README.md @@ -0,0 +1,214 @@ +# SESE Controller + +ESP-IDF 기반 ESP32-Ethernet-Kit 및 ESP32-C3-SUPERMINI 컨트롤러입니다. 최초 설정은 별도 앱이나 PoP 없이 스마트폰 브라우저에서 진행하며, 연결 후에는 Wi-Fi station과 HTTPS OTA를 사용합니다. + +## 최초 Wi-Fi 설정 + +1. 전원을 켭니다. +2. 스마트폰 Wi-Fi에서 `SESE_XXXXXX`를 선택합니다. SoftAP 비밀번호는 없습니다. +3. captive portal이 자동으로 열리지 않으면 브라우저에서 `http://192.168.4.1`을 엽니다. +4. 검색 목록에서 Wi-Fi를 선택합니다. 숨겨진 네트워크는 `숨겨진 Wi-Fi 직접 입력`을 선택하고 SSID를 대소문자까지 정확히 입력합니다. +5. 해당 Wi-Fi 비밀번호를 입력합니다. +6. 장치가 실제로 IP 주소를 받은 뒤에만 설정을 NVS에 저장합니다. +7. 성공 결과를 잠시 표시한 다음 HTTP/DNS server와 SoftAP을 종료하고 station mode를 유지합니다. + +설정 실패 시 후보 비밀번호는 저장하지 않으며 portal을 유지해 다시 시도할 수 있습니다. Wi-Fi 비밀번호는 UART 로그나 HTTP 응답에 출력하지 않습니다. SSID 32바이트와 64바이트 hex PSK도 배열 전체를 길이 기반으로 복사해 지원합니다. + +## Provisioning 정책 + +- SoftAP: `SESE_XXXXXX`, open authentication, 최대 접속자 1명 +- Portal: DNS redirect + DHCP captive-portal URI(Option 114) +- 직접 접속 주소: `http://192.168.4.1` +- API: + - `GET /api/v1/networks` + - `GET /api/v1/status` + - `POST /api/v1/config` +- 설정 window: 10분 +- 저장소: application-owned NVS namespace `wifi_cfg`의 versioned blob +- ESP Wi-Fi 자체 저장소: `WIFI_STORAGE_RAM` +- 설정 성공 후 SoftAP 종료 +- 일반 station 연결이 5회 연속 실패하면 기존 설정을 삭제하지 않고 제한된 복구 portal 시작 + +ESP SoftAP Provision 앱, `esp_prov.py`, Protocomm 및 PoP는 제품 설정 절차에 사용하지 않습니다. + +## Wi-Fi 설정 초기화 + +실행 중 BOOT 버튼을 5초 이상 누르면 `wifi_cfg`만 삭제하고 재부팅하여 browser provisioning으로 돌아갑니다. ESP32-Ethernet-Kit은 GPIO0, ESP32-C3-SUPERMINI는 GPIO9을 사용합니다. 다른 NVS 데이터와 OTA 파티션은 지우지 않습니다. + +개발 시 전체 flash 초기화가 필요한 경우에만 다음 명령을 사용합니다. + +```bash +source ~/esp/esp-idf/export.sh +idf.py -p /dev/ttyUSB1 erase-flash +idf.py -p /dev/ttyUSB1 flash +``` + +`erase-flash`는 Wi-Fi 설정 외 데이터도 모두 삭제하므로 제품의 일반 재설정 절차가 아닙니다. + +## 빌드·다운로드·모니터 + +### ESP32-Ethernet-Kit + +```bash +source ~/esp/esp-idf/export.sh +idf.py build +idf.py -p /dev/ttyUSB1 flash +idf.py -p /dev/ttyUSB1 monitor +``` + +- ESP-IDF: `v6.0.2` +- 대상: `esp32` +- Flash: 4MB +- 다운로드 baudrate: 460800 +- UART: 115200 + +### ESP32-C3-SUPERMINI + +ESP32용 `sdkconfig`와 빌드 결과를 덮어쓰지 않도록 별도의 설정과 build directory를 사용합니다. + +```bash +source ~/esp/esp-idf/export.sh +idf.py -B build-esp32c3 -D SDKCONFIG=sdkconfig.esp32c3 set-target esp32c3 +idf.py -B build-esp32c3 -D SDKCONFIG=sdkconfig.esp32c3 build +``` + +최초 다운로드 시 BOOT(GPIO9)을 누른 상태에서 RESET한 뒤 `/dev/ttyACM0`이 나타나면 다음과 같이 기록합니다. + +```bash +esptool --chip esp32c3 --port /dev/ttyACM0 --baud 460800 \ + --before no-reset --after hard-reset write-flash \ + --flash-mode dio --flash-size 4MB --flash-freq 80m \ + 0x0 build-esp32c3/bootloader/bootloader.bin \ + 0x8000 build-esp32c3/partition_table/partition-table.bin \ + 0xf000 build-esp32c3/ota_data_initial.bin \ + 0x20000 build-esp32c3/sese-controller.bin +``` + +다운로드 후 ROM의 `waiting for download` 상태가 유지되면 BOOT을 누르지 않은 상태에서 USB 전원을 다시 연결합니다. C3 설정은 native USB Serial/JTAG console과 GPIO9 BOOT long-press를 사용합니다. + +- 검증 보드: ESP32-C3 revision v0.4 +- 실제 Flash: 4 MiB (`JEDEC 20:4016`) +- USB: Espressif USB Serial/JTAG (`303a:1001`) +- console: `/dev/ttyACM0`, 115200 baud + +Portal 시작 시 firmware가 `/`, `/api/v1/status`, `/api/v1/networks`에 대한 로컬 HTTP self-test를 한 번 실행합니다. 성공 로그는 다음과 같습니다. + +```text +Captive portal HTTP and Wi-Fi scan API self-test passed +``` + +## NTP 시간 동기화 + +Wi-Fi에서 IP를 받은 뒤 SNTP를 시작해 시스템 시간을 동기화합니다. + +기본 설정: + +- NTP server: `pool.ntp.org` +- timezone: `KST-9` (UTC+09:00) +- 최초 동기화 대기: 30초 +- 이후 주기 동기화: ESP-IDF SNTP 주기(현재 1시간) + +`idf.py menuconfig`의 `SESE Controller` 메뉴에서 server, POSIX timezone 및 최초 대기 시간을 변경할 수 있습니다. 시스템 epoch는 UTC로 유지하고, 사람이 읽는 로그만 설정한 timezone으로 표시합니다. 최초 요청이 timeout되어도 background SNTP 재시도는 계속되며 OTA worker는 기존 재시도 정책을 유지합니다. + +정상 로그 예시: + +```text +NTP time synchronized: 2026-08-31T...+0900 +Initial system time ready: 2026-08-31T...+0900 +``` + +## MQTT client + +Wi-Fi 및 최초 NTP 처리 후 MQTT client를 시작합니다. broker URI가 비어 있으면 기능은 안전하게 비활성화됩니다. `mqtts://`와 `wss://` URI에는 ESP-IDF certificate bundle 및 hostname 검증을 적용합니다. + +`idf.py menuconfig`의 `SESE Controller` 메뉴: + +- `Enable MQTT client` +- `MQTT broker URI` +- `MQTT heartbeat interval (seconds)` (기본 60초) + +broker URI에 username, password, token 등 credential을 넣지 마십시오. 현재 구현은 broker credential을 소스·로그·build 설정에 저장하지 않습니다. + +장치 ID는 Wi-Fi Station MAC의 마지막 3바이트로 만든 `SESE_XXXXXX` 형식입니다. 장치 `SESE_5F2A8C`의 topic 예시는 다음과 같습니다. + +| Topic | 방향 | QoS/retain | Payload | +| --- | --- | --- | --- | +| `sese/SESE_5F2A8C/status` | publish | QoS 1, retained | online 상태·장치 ID·firmware version. 비정상 연결 종료 시 LWT `{"online":false}` | +| `sese/SESE_5F2A8C/heartbeat` | publish | QoS 0 | 장치 ID·Unix timestamp·uptime seconds | +| `sese/SESE_5F2A8C/command/ping` | subscribe | QoS 1 | 정확히 `ping` | +| `sese/SESE_5F2A8C/response/pong` | publish | QoS 1 | pong 응답·장치 ID·Unix timestamp | + +연결이 끊기면 5초 후 자동 재연결하며, 재연결 때 status와 heartbeat를 즉시 다시 발행합니다. + +로컬 Mosquitto contract test 환경: + +```bash +cd test/mqtt +docker compose up -d +# 테스트가 끝나면 LAN에 공개된 anonymous listener를 즉시 종료합니다. +docker compose down +``` + +`test/mqtt` 설정은 실제 장치가 LAN에서 접근하는 일회성 통합 테스트 전용이며 운영 환경에 사용하지 않습니다. + +## 자동 HTTPS OTA + +4MB flash partition: + +- `ota_0`: 1920KB +- `ota_1`: 1920KB +- `otadata`: 활성 slot 및 rollback 상태 +- `nvs`: Wi-Fi 설정 +- `storage`: application data + +설정: + +```bash +idf.py menuconfig +``` + +`SESE Controller` 메뉴: + +- `Enable automatic HTTPS OTA` +- `HTTPS OTA firmware URL` +- 최초 확인 지연, 정기 확인 간격, 실패 재시도 간격 + +URL이 비어 있으면 OTA worker는 비활성 상태입니다. URL에 token, password 또는 API key를 포함하지 마십시오. 서버는 ESP-IDF application binary를 직접 반환하고 공인 CA의 유효한 HTTPS 인증서를 사용해야 합니다. + +릴리스 절차: + +1. `CMakeLists.txt`의 `PROJECT_VER`를 증가시킵니다. +2. `idf.py build`를 실행합니다. +3. `build/sese-controller.bin`을 HTTPS endpoint에 원자적으로 배포합니다. +4. 장치는 project name과 version을 검증한 뒤 inactive OTA slot에 설치합니다. +5. 새 firmware가 Wi-Fi 연결 진단을 통과해야 rollback 상태를 해제합니다. + +동일하거나 낮은 버전, 다른 project name, 불완전하거나 검증에 실패한 이미지는 거부합니다. + +## 보안 경계 + +현재 onboarding은 사용성을 위해 **open SoftAP + HTTP**를 사용합니다. 주변 공격자가 무선 트래픽을 관찰하거나 설정을 시도할 수 있으며 nonce가 암호화나 소유 증명이 되지는 않습니다. + +적용한 완화책: + +- 미설정 장치에서만 자동 portal 진입 +- 이미 설정된 장치는 BOOT 5초 long-press로 재설정 +- 10분 timeout +- SoftAP 접속자 1명 제한 +- boot별 random nonce로 설정 POST 검증 +- request body 및 SSID/password 길이 제한 +- 동시 scan/설정 요청 제한 +- 실제 연결 성공 후에만 NVS 저장 +- 성공 후 HTTP/DNS/SoftAP 종료 +- 비밀번호 비로깅·비반향 및 임시 buffer 정리 + +양산 threat model에서는 NVS encryption, flash encryption, secure boot 및 provisioning transport 암호화를 별도로 검토해야 합니다. +# Architecture and verification + +The reusable `components/kesp32-app-base` component owns NVS initialization, browser provisioning, Wi-Fi readiness, NTP, HTTPS OTA/rollback, reset-button policy, and generic MQTT transport. Its public entry point is `esp32_app_base_start()` in `include/esp32_app_base.h`; consumers provide board reset settings, network/product-health callbacks, and product MQTT topics/payloads. `main/app_main.c` owns one-time integration startup and `main/sese_product.c` owns SESE identity, topics, payloads, and health policy. `examples/minimal_consumer` demonstrates a second consumer without source copies. + +Partitions, sdkconfig, target selection, service identity, and product callbacks are consumer-owned. Existing compatibility Kconfig symbols retain the `SESE_*` prefix. Runtime Wi-Fi, MQTT, and OTA behavior remains hardware-gated and was not claimed from compilation. + +The host test in `test/host` compiles the actual extracted OTA `major.minor.patch` comparator used by firmware. Run `cd test/host && ./run_tests.sh`. The acceptance build evidence is kept in `build-verify-esp32/` and `build-verify-esp32c3/`, with target-specific binaries and `log/` output; no hardware flash, reset, serial, Wi-Fi, MQTT, or OTA exercise is claimed. + +ESP-IDF 6.0.2's local `esp_https_ota` sources do not expose the removed partial-download initializer fields in the installed headers; the component therefore does not hard-code them. Partial-download behavior, when enabled by a future/alternate SDK configuration, remains SDK-owned and must be checked against that SDK's header/Kconfig before enabling. diff --git a/components/kesp32-app-base b/components/kesp32-app-base new file mode 160000 index 0000000..4c91e73 --- /dev/null +++ b/components/kesp32-app-base @@ -0,0 +1 @@ +Subproject commit 4c91e73dbb166adbf28da307d20975480ee185b3 diff --git a/dependencies.lock b/dependencies.lock new file mode 100644 index 0000000..3009b4e --- /dev/null +++ b/dependencies.lock @@ -0,0 +1,32 @@ +dependencies: + espressif/cjson: + component_hash: e788323270d90738662d66fffa910bfe1fba019bba087f01557e70c40485b469 + dependencies: + - name: idf + require: private + version: '>=5.0' + source: + registry_url: https://components.espressif.com/ + type: service + version: 1.7.19~2 + espressif/mqtt: + component_hash: fb18bc3b65aa8c94693a9811ffc322cca8a65d92d5ec84983d3e385080e3969c + dependencies: + - name: idf + require: private + version: '>=5.3' + source: + registry_url: https://components.espressif.com/ + type: service + version: 1.1.0 + idf: + source: + type: idf + version: 6.0.2 +direct_dependencies: +- espressif/cjson +- espressif/mqtt +- idf +manifest_hash: b8c02754f443a429d4e85215560d3a96ea0d39bf229c0f4e9cd7cf7c9f297f24 +target: esp32c3 +version: 3.0.0 diff --git a/examples/minimal_consumer/CMakeLists.txt b/examples/minimal_consumer/CMakeLists.txt new file mode 100644 index 0000000..93b7219 --- /dev/null +++ b/examples/minimal_consumer/CMakeLists.txt @@ -0,0 +1,4 @@ +cmake_minimum_required(VERSION 3.22) +set(EXTRA_COMPONENT_DIRS "${CMAKE_CURRENT_LIST_DIR}/../../components") +include($ENV{IDF_PATH}/tools/cmake/project.cmake) +project(minimal-consumer) diff --git a/examples/minimal_consumer/README.md b/examples/minimal_consumer/README.md new file mode 100644 index 0000000..34da7e2 --- /dev/null +++ b/examples/minimal_consumer/README.md @@ -0,0 +1,11 @@ +# Minimal kesp32-app-base consumer + +This example uses the repository's `components/kesp32-app-base` directly; it does not copy base sources. + +```bash +source "$HOME/esp/esp-idf/export.sh" +idf.py -B build-minimal -D SDKCONFIG= sdkconfig set-target esp32 +idf.py -B build-minimal build +``` + +A consumer supplies `esp32_app_base_config_t` and a product-start callback. Product partition tables and sdkconfig remain owned by the consumer. diff --git a/examples/minimal_consumer/dependencies.lock b/examples/minimal_consumer/dependencies.lock new file mode 100644 index 0000000..1039ff6 --- /dev/null +++ b/examples/minimal_consumer/dependencies.lock @@ -0,0 +1,32 @@ +dependencies: + espressif/cjson: + component_hash: e788323270d90738662d66fffa910bfe1fba019bba087f01557e70c40485b469 + dependencies: + - name: idf + require: private + version: '>=5.0' + source: + registry_url: https://components.espressif.com/ + type: service + version: 1.7.19~2 + espressif/mqtt: + component_hash: fb18bc3b65aa8c94693a9811ffc322cca8a65d92d5ec84983d3e385080e3969c + dependencies: + - name: idf + require: private + version: '>=5.3' + source: + registry_url: https://components.espressif.com/ + type: service + version: 1.1.0 + idf: + source: + type: idf + version: 6.0.2 +direct_dependencies: +- espressif/cjson +- espressif/mqtt +- idf +manifest_hash: e03b47692cc8724400d2cede6d9af2098d53cda7e3f9d4f757f0fcded5b717c7 +target: esp32c3 +version: 3.0.0 diff --git a/examples/minimal_consumer/idf_component.yml b/examples/minimal_consumer/idf_component.yml new file mode 100644 index 0000000..02389e6 --- /dev/null +++ b/examples/minimal_consumer/idf_component.yml @@ -0,0 +1,3 @@ +dependencies: + idf: + version: '>=6.0.2' \ No newline at end of file diff --git a/examples/minimal_consumer/main/CMakeLists.txt b/examples/minimal_consumer/main/CMakeLists.txt new file mode 100644 index 0000000..237cc66 --- /dev/null +++ b/examples/minimal_consumer/main/CMakeLists.txt @@ -0,0 +1 @@ +idf_component_register(SRCS "main.c" INCLUDE_DIRS "." REQUIRES kesp32-app-base) diff --git a/examples/minimal_consumer/main/main.c b/examples/minimal_consumer/main/main.c new file mode 100644 index 0000000..d2de7ec --- /dev/null +++ b/examples/minimal_consumer/main/main.c @@ -0,0 +1,13 @@ +#include "esp32_app_base.h" + +void app_main(void) +{ + const esp32_app_base_config_t config = { + .service_prefix = "MINIMAL_", + .reset_gpio = 0, + .reset_active_low = true, + .reset_hold_ms = 3000, + .product_start = NULL, + }; + (void)esp32_app_base_start(&config); +} diff --git a/main/CMakeLists.txt b/main/CMakeLists.txt new file mode 100644 index 0000000..2c042c6 --- /dev/null +++ b/main/CMakeLists.txt @@ -0,0 +1 @@ +idf_component_register(SRCS "app_main.c" "sese_product.c" INCLUDE_DIRS "." REQUIRES kesp32-app-base) diff --git a/main/Kconfig.projbuild b/main/Kconfig.projbuild new file mode 100644 index 0000000..cea9b28 --- /dev/null +++ b/main/Kconfig.projbuild @@ -0,0 +1 @@ +# Product project Kconfig intentionally empty; reusable settings live in kesp32_app_base. diff --git a/main/app_main.c b/main/app_main.c new file mode 100644 index 0000000..1f7a52c --- /dev/null +++ b/main/app_main.c @@ -0,0 +1,18 @@ +#include "esp32_app_base.h" +esp_err_t sese_product_start(void); + +void app_main(void) +{ + const esp32_app_base_config_t config = { + .service_prefix = "SESE_", +#if CONFIG_IDF_TARGET_ESP32C3 + .reset_gpio = 9, +#else + .reset_gpio = 0, +#endif + .reset_active_low = true, + .reset_hold_ms = 5000, + .product_start = sese_product_start, + }; + (void)esp32_app_base_start(&config); +} diff --git a/main/idf_component.yml b/main/idf_component.yml new file mode 100644 index 0000000..cc8be01 --- /dev/null +++ b/main/idf_component.yml @@ -0,0 +1,4 @@ +dependencies: + idf: + version: '>=6.0.2' + espressif/cjson: ^1.7.19 diff --git a/main/sese_product.c b/main/sese_product.c new file mode 100644 index 0000000..13cc9ea --- /dev/null +++ b/main/sese_product.c @@ -0,0 +1,16 @@ +#include "esp32_app_base.h" +#include "mqtt_transport.h" +#include +#include +#include +#include +#include "esp_app_desc.h" +#include "esp_mac.h" +#include "esp_timer.h" +#include "sdkconfig.h" + +static char id[20], status_topic[64], heartbeat_topic[64], ping_topic[64], pong_topic[64]; +static void connected(void *ctx) { (void)ctx; mqtt_transport_subscribe(ping_topic, 1); const esp_app_desc_t *a=esp_app_get_description(); char p[160]; snprintf(p,sizeof(p),"{\"online\":true,\"device\":\"%s\",\"version\":\"%s\"}",id,a->version); mqtt_transport_publish(status_topic,p,1,true); } +static void heartbeat(void *ctx) { (void)ctx; time_t now; time(&now); char p[160]; snprintf(p,sizeof(p),"{\"device\":\"%s\",\"timestamp\":%" PRId64 ",\"uptime_seconds\":%" PRId64 "}",id,(int64_t)now,esp_timer_get_time()/1000000LL); mqtt_transport_publish(heartbeat_topic,p,0,false); } +static void message(const char *topic,size_t tl,const char *data,size_t dl,void *ctx) { (void)ctx; if(tl==strlen(ping_topic)&&!memcmp(topic,ping_topic,tl)&&dl==4&&!memcmp(data,"ping",4)){ char p[128]; time_t now; time(&now); snprintf(p,sizeof(p),"{\"response\":\"pong\",\"device\":\"%s\",\"timestamp\":%" PRId64 "}",id,(int64_t)now); mqtt_transport_publish(pong_topic,p,1,false); } } +esp_err_t sese_product_start(void) { uint8_t mac[6]; ESP_ERROR_CHECK(esp_read_mac(mac,ESP_MAC_WIFI_STA)); snprintf(id,sizeof(id),"SESE_%02X%02X%02X",mac[3],mac[4],mac[5]); snprintf(status_topic,sizeof(status_topic),"sese/%s/status",id); snprintf(heartbeat_topic,sizeof(heartbeat_topic),"sese/%s/heartbeat",id); snprintf(ping_topic,sizeof(ping_topic),"sese/%s/command/ping",id); snprintf(pong_topic,sizeof(pong_topic),"sese/%s/response/pong",id); mqtt_transport_config_t c={.broker_uri=CONFIG_SESE_MQTT_BROKER_URI,.client_id=id,.last_will_topic=status_topic,.last_will_payload="{\"online\":false}",.heartbeat_interval_seconds=CONFIG_SESE_MQTT_HEARTBEAT_SECONDS,.on_connected=connected,.on_message=message,.on_heartbeat=heartbeat}; return mqtt_transport_start(&c); } diff --git a/partitions.csv b/partitions.csv new file mode 100644 index 0000000..de309b1 --- /dev/null +++ b/partitions.csv @@ -0,0 +1,7 @@ +# Name, Type, SubType, Offset, Size, Flags +nvs, data, nvs, 0x9000, 0x6000, +otadata, data, ota, 0xF000, 0x2000, +phy_init, data, phy, 0x11000, 0x1000, +ota_0, app, ota_0, 0x20000, 0x1E0000, +ota_1, app, ota_1, 0x200000, 0x1E0000, +storage, data, spiffs, 0x3E0000, 0x20000, diff --git a/test/host/run_tests.sh b/test/host/run_tests.sh new file mode 100644 index 0000000..b3f30e6 --- /dev/null +++ b/test/host/run_tests.sh @@ -0,0 +1,7 @@ +# Host tests for extracted OTA version policy + +set -eu +ROOT=$(CDPATH= cd -- "$(dirname -- "$0")/../.." && pwd) +cc -std=c11 -Wall -Wextra -Werror -I"$ROOT/components/kesp32-app-base/include" \ + "$ROOT/components/kesp32-app-base/src/ota_version.c" "$ROOT/test/host/test_ota_version.c" -o "$ROOT/test/host/test_ota_version" +"$ROOT/test/host/test_ota_version" diff --git a/test/host/test_ota_version.c b/test/host/test_ota_version.c new file mode 100644 index 0000000..faaa430 --- /dev/null +++ b/test/host/test_ota_version.c @@ -0,0 +1,13 @@ +#include +#include "ota_version.h" + +int main(void) +{ + int order; + assert(ota_version_compare("0.5.0", "0.4.9", &order) && order > 0); + assert(ota_version_compare("1.2.3", "1.2.3", &order) && order == 0); + assert(ota_version_compare("1.2.3", "1.3.0", &order) && order < 0); + assert(!ota_version_compare("1.2", "1.3.0", &order)); + assert(!ota_version_compare("1.2.3x", "1.3.0", &order)); + return 0; +} diff --git a/test/mqtt/compose.yaml b/test/mqtt/compose.yaml new file mode 100644 index 0000000..4d45c76 --- /dev/null +++ b/test/mqtt/compose.yaml @@ -0,0 +1,9 @@ +services: + broker: + image: eclipse-mosquitto:2.0 + container_name: sese-controller-mosquitto + restart: "no" + ports: + - "1883:1883" + volumes: + - ./mosquitto.conf:/mosquitto/config/mosquitto.conf:ro diff --git a/test/mqtt/mosquitto.conf b/test/mqtt/mosquitto.conf new file mode 100644 index 0000000..9da815f --- /dev/null +++ b/test/mqtt/mosquitto.conf @@ -0,0 +1,8 @@ +listener 1883 0.0.0.0 +allow_anonymous true +persistence false +connection_messages true +log_type error +log_type warning +log_type notice +log_type information