From a371e2905853922369a1ea8c125f793fd740730f Mon Sep 17 00:00:00 2001 From: eddy Date: Sat, 1 Aug 2026 00:18:16 +0200 Subject: [PATCH 1/9] =?UTF-8?q?chore:=20branch=20setup=20=E2=80=94=20versi?= =?UTF-8?q?on.txt,=20platformio.ini=20envs,=20qio=5Fqspi?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 3 +++ platformio.ini | 32 ++++++++++++++++++++++++++------ tools/pio_version_pre.py | 27 +++++++++++++++++++++++++++ version.txt | 2 ++ 4 files changed, 58 insertions(+), 6 deletions(-) create mode 100755 tools/pio_version_pre.py create mode 100644 version.txt diff --git a/.gitignore b/.gitignore index 2cf8684..e669370 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,6 @@ +# Generated from version.txt +src/version.h + # PlatformIO .pio/ .pioenvs/ diff --git a/platformio.ini b/platformio.ini index 11bd9a7..b22a594 100644 --- a/platformio.ini +++ b/platformio.ini @@ -1,18 +1,38 @@ -[env:m5stack-atoms3r] +[env:s3lite-server] platform = espressif32@6.7.0 board = esp32-s3-devkitc-1 framework = arduino -board_build.arduino.memory_type = qio_opi +board_build.arduino.memory_type = qio_qspi build_flags = -DESP32S3 - -DBOARD_HAS_PSRAM - -mfix-esp32-psram-cache-issue -DCORE_DEBUG_LEVEL=5 -DARDUINO_USB_CDC_ON_BOOT=1 -DARDUINO_USB_MODE=1 - -DCONFIG_BT_NIMBLE_ENABLED lib_deps = + fastled/FastLED@^3 h2zero/NimBLE-Arduino@^2 upload_port = /dev/ttyACM1 upload_speed = 115200 -upload_flags = --no-stub \ No newline at end of file +upload_flags = --no-stub +build_src_filter = + - +extra_scripts = pre:tools/pio_version_pre.py + +[env:s3lite-client] +platform = espressif32@6.7.0 +board = esp32-s3-devkitc-1 +framework = arduino +board_build.arduino.memory_type = qio_qspi +build_flags = + -DESP32S3 + -DCORE_DEBUG_LEVEL=5 + -DARDUINO_USB_CDC_ON_BOOT=1 + -DARDUINO_USB_MODE=1 + -DDEVICE_ID=1 +lib_deps = + fastled/FastLED@^3 + h2zero/NimBLE-Arduino@^2 +upload_port = /dev/ttyACM2 +upload_speed = 115200 +upload_flags = --no-stub +build_src_filter = + - +extra_scripts = pre:tools/pio_version_pre.py diff --git a/tools/pio_version_pre.py b/tools/pio_version_pre.py new file mode 100755 index 0000000..130216f --- /dev/null +++ b/tools/pio_version_pre.py @@ -0,0 +1,27 @@ +#!/usr/bin/env python3 +"""Pre-build: generate src/version.h from version.txt so the firmware + uses the single source of truth for version numbers.""" +Import("env") +import os + +project_dir = env.subst("$PROJECT_DIR") +ver_file = os.path.join(project_dir, "version.txt") +out_file = os.path.join(project_dir, "src", "version.h") + +versions = {} +with open(ver_file) as f: + for line in f: + line = line.strip() + if "=" in line: + k, v = line.split("=", 1) + versions[k.strip()] = v.strip() + +header = f"""// Auto-generated from version.txt — DO NOT EDIT +#define FIRMWARE_VERSION_SERVER "{versions['server']}" +#define FIRMWARE_VERSION_CLIENT "{versions['client']}" +""" + +with open(out_file, "w") as f: + f.write(header) + +print(f" versions: server={versions['server']} client={versions['client']} -> src/version.h") diff --git a/version.txt b/version.txt new file mode 100644 index 0000000..71ce7a5 --- /dev/null +++ b/version.txt @@ -0,0 +1,2 @@ +server=6 +client=1 From 1e47fc3de7e84b80b28bc9b5c81e3bf756860a63 Mon Sep 17 00:00:00 2001 From: eddy Date: Sat, 1 Aug 2026 00:26:50 +0200 Subject: [PATCH 2/9] fix: button toggle logic for relay1 --- src/main.cpp | 180 +++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 132 insertions(+), 48 deletions(-) diff --git a/src/main.cpp b/src/main.cpp index 8062e2e..3a12d11 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,6 +1,7 @@ #include #include #include +#include #include #include @@ -9,12 +10,17 @@ #include #include -#define RELAY1_PIN 1 -#define RELAY2_PIN 2 +#define RELAY1_PIN 39 +#define RELAY2_PIN 38 #define TOUCH1_PIN 8 #define TOUCH2_PIN 6 #define BUTTON_PIN 41 -#define LED_PIN 46 +#define LED_PIN 35 +#define NUM_LEDS 1 + +CRGB leds[NUM_LEDS]; + +#define MAX_CLIENTS 2 #define SERVICE_UUID "4fafc201-1fb5-459e-8fcc-c5c9c331914b" #define CHARACTERISTIC_UUID "beb5483e-36e1-4688-b7f5-ea07361f26d8" @@ -29,7 +35,7 @@ #define OTA_STATUS_ACK 0x00 #define OTA_STATUS_NAK 0x01 #define OTA_STATUS_COMPLETE 0x10 -#define OTA_STATUS_ERROR 0x11 // 1 cmd + 4 seq + 32 hmac +#define OTA_STATUS_ERROR 0x11 #define HMAC_KEY_LEN 32 #define ESP_UUID_LEN 16 #define NVS_NAMESPACE "hmac" @@ -38,12 +44,16 @@ #define FIRMWARE_VERSION "6" NimBLECharacteristic *pCharacteristic; -bool deviceConnected = false; -bool bleRelay1 = false; -bool bleRelay2 = false; bool relay1State = false; bool relay2State = false; -uint32_t lastSeq = 0; +bool buttonToggle = false; + +bool connActive[MAX_CLIENTS] = {false, false}; +bool bleRelay1[MAX_CLIENTS] = {false, false}; +bool bleRelay2[MAX_CLIENTS] = {false, false}; +uint32_t lastSeq[MAX_CLIENTS] = {0, 0}; +uint16_t connHandles[MAX_CLIENTS] = {0xFFFF, 0xFFFF}; +int connectedCount = 0; bool otaInProgress = false; esp_ota_handle_t otaHandle = 0; @@ -60,31 +70,75 @@ bool keyProvisioned = false; bool uuidProvisioned = false; Preferences preferences; +static int findClientIdx(uint16_t connHandle) { + for (int i = 0; i < MAX_CLIENTS; i++) { + if (connActive[i] && connHandles[i] == connHandle) return i; + } + return -1; +} + class MyServerCallbacks : public NimBLEServerCallbacks { void onConnect(NimBLEServer *pServer, NimBLEConnInfo &connInfo) override { - deviceConnected = true; - lastSeq = 0; - Serial.println("BLE: device connected"); + uint16_t handle = connInfo.getConnHandle(); + int slot = -1; + for (int i = 0; i < MAX_CLIENTS; i++) { + if (!connActive[i]) { + slot = i; + break; + } + } + if (slot >= 0) { + connHandles[slot] = handle; + connActive[slot] = true; + bleRelay1[slot] = false; + bleRelay2[slot] = false; + lastSeq[slot] = 0; + connectedCount++; + Serial.printf("BLE: device connected (conn=%d, slot=%d, clients=%d/%d)\n", + handle, slot, connectedCount, MAX_CLIENTS); + } else { + Serial.printf("BLE: device connected but no free slot (conn=%d, clients=%d/%d)\n", + handle, connectedCount, MAX_CLIENTS); + } + if (connectedCount < MAX_CLIENTS) { + NimBLEDevice::startAdvertising(); + } } void onDisconnect(NimBLEServer *pServer, NimBLEConnInfo &connInfo, int reason) override { - deviceConnected = false; - bleRelay1 = false; - bleRelay2 = false; - lastSeq = 0; - Serial.println("BLE: device disconnected"); - NimBLEDevice::startAdvertising(); + uint16_t handle = connInfo.getConnHandle(); + for (int i = 0; i < MAX_CLIENTS; i++) { + if (connActive[i] && connHandles[i] == handle) { + connActive[i] = false; + bleRelay1[i] = false; + bleRelay2[i] = false; + lastSeq[i] = 0; + connHandles[i] = 0xFFFF; + connectedCount--; + Serial.printf("BLE: device disconnected (conn=%d, slot=%d, clients=%d/%d)\n", + handle, i, connectedCount, MAX_CLIENTS); + break; + } + } + if (connectedCount < MAX_CLIENTS) { + NimBLEDevice::startAdvertising(); + } } }; -static bool verifyHmac(const uint8_t *packet) { +static bool verifyHmac(const uint8_t *packet, int idx) { + if (idx < 0 || idx >= MAX_CLIENTS || !connActive[idx]) { + Serial.printf("BLE: HMAC verify called for invalid idx=%d\n", idx); + return false; + } + uint8_t cmd = packet[0]; uint32_t seq = ((uint32_t)packet[1] << 24) | ((uint32_t)packet[2] << 16) | ((uint32_t)packet[3] << 8) | (uint32_t)packet[4]; - if (seq <= lastSeq) { - Serial.printf("BLE: replay rejected (seq=%lu, last=%lu)\n", seq, lastSeq); + if (seq <= lastSeq[idx]) { + Serial.printf("BLE: replay rejected (seq=%lu, last=%lu, slot=%d)\n", seq, lastSeq[idx], idx); return false; } @@ -104,7 +158,7 @@ static bool verifyHmac(const uint8_t *packet) { return false; } - lastSeq = seq; + lastSeq[idx] = seq; return true; } @@ -123,7 +177,7 @@ static void parseHexKey(const String &hex, uint8_t *out) { } static void ledOtaBusy() { - digitalWrite(LED_PIN, (millis() / 100) % 2 ? HIGH : LOW); + leds[0] = (millis() / 100) % 2 ? CRGB::Green : CRGB::Black; } static void otaReset() { @@ -193,8 +247,10 @@ class OtaCharacteristicCallbacks : public NimBLECharacteristicCallbacks { digitalWrite(RELAY2_PIN, LOW); relay1State = false; relay2State = false; - bleRelay1 = false; - bleRelay2 = false; + for (int i = 0; i < MAX_CLIENTS; i++) { + bleRelay1[i] = false; + bleRelay2[i] = false; + } Serial.printf("OTA: started, expecting %lu bytes\n", otaExpectedSize); return; } @@ -225,7 +281,8 @@ class OtaCharacteristicCallbacks : public NimBLECharacteristicCallbacks { sendOtaNotify(pChar, OTA_STATUS_ERROR, 0); return; } - digitalWrite(LED_PIN, HIGH); + leds[0] = CRGB::Green; + FastLED.show(); sendOtaNotify(pChar, OTA_STATUS_COMPLETE, 0); Serial.println("OTA: SHA256 OK, rebooting..."); delay(500); @@ -256,6 +313,13 @@ class OtaCharacteristicCallbacks : public NimBLECharacteristicCallbacks { class MyCharacteristicCallbacks : public NimBLECharacteristicCallbacks { void onWrite(NimBLECharacteristic *pChar, NimBLEConnInfo &connInfo) override { if (otaInProgress) return; + + int idx = findClientIdx(connInfo.getConnHandle()); + if (idx < 0) { + Serial.printf("BLE: write from unknown conn handle %d\n", connInfo.getConnHandle()); + return; + } + std::string value = pChar->getValue(); if (value.length() != HMAC_PKT_SIZE) { Serial.printf("BLE: bad packet length %d (expected %d)\n", @@ -265,25 +329,25 @@ class MyCharacteristicCallbacks : public NimBLECharacteristicCallbacks { const uint8_t *pkt = (const uint8_t *)value.data(); - if (!verifyHmac(pkt)) { + if (!verifyHmac(pkt, idx)) { return; } uint8_t cmd = pkt[0]; - uint32_t seq = lastSeq; + uint32_t seq = lastSeq[idx]; if (cmd == 0x01) { - bleRelay1 = true; - Serial.printf("BLE: relay1 ON (seq=%lu)\n", seq); + bleRelay1[idx] = true; + Serial.printf("BLE: relay1 ON (seq=%lu, slot=%d)\n", seq, idx); } else if (cmd == 0x00) { - bleRelay1 = false; - Serial.printf("BLE: relay1 OFF (seq=%lu)\n", seq); + bleRelay1[idx] = false; + Serial.printf("BLE: relay1 OFF (seq=%lu, slot=%d)\n", seq, idx); } else if (cmd == 0x02) { - bleRelay2 = true; - Serial.printf("BLE: relay2 ON (seq=%lu)\n", seq); + bleRelay2[idx] = true; + Serial.printf("BLE: relay2 ON (seq=%lu, slot=%d)\n", seq, idx); } else if (cmd == 0x03) { - bleRelay2 = false; - Serial.printf("BLE: relay2 OFF (seq=%lu)\n", seq); + bleRelay2[idx] = false; + Serial.printf("BLE: relay2 OFF (seq=%lu, slot=%d)\n", seq, idx); } } }; @@ -291,7 +355,9 @@ class MyCharacteristicCallbacks : public NimBLECharacteristicCallbacks { void setup() { Serial.begin(115200); delay(2000); - Serial.println("=== Atom VoiceS3R BLE Relay ==="); + Serial.println("=== AtomS3 Lite BLE Relay ==="); + + FastLED.addLeds(leds, NUM_LEDS); preferences.begin(NVS_NAMESPACE, true); size_t len = preferences.getBytes(NVS_KEY_NAME, hmacKey, HMAC_KEY_LEN); @@ -328,12 +394,13 @@ void setup() { pinMode(BUTTON_PIN, INPUT); pinMode(RELAY1_PIN, OUTPUT); pinMode(RELAY2_PIN, OUTPUT); - pinMode(LED_PIN, OUTPUT); for (int i = 0; i < 3; i++) { - digitalWrite(LED_PIN, HIGH); + leds[0] = CRGB::Green; + FastLED.show(); delay(200); - digitalWrite(LED_PIN, LOW); + leds[0] = CRGB::Black; + FastLED.show(); delay(200); } Serial.println("LED blink done, starting BLE..."); @@ -343,6 +410,7 @@ void setup() { NimBLEServer *pServer = NimBLEDevice::createServer(); pServer->setCallbacks(new MyServerCallbacks()); + pServer->advertiseOnDisconnect(true); NimBLEService *pService = pServer->createService(SERVICE_UUID); pCharacteristic = pService->createCharacteristic( @@ -472,9 +540,20 @@ void loop() { bool t2Active = (millis() - t2Change > DEBOUNCE_MS) && t2Stable; bool btnActive = (millis() - btnChange > DEBOUNCE_MS) && btnStable; + static bool prevBtnActive = false; + if (btnActive && !prevBtnActive) { + buttonToggle = !buttonToggle; + Serial.printf("Button: toggle -> %s\n", buttonToggle ? "ON" : "OFF"); + } + prevBtnActive = btnActive; + if (!otaInProgress) { - relay1State = t1Active || btnActive || (deviceConnected && bleRelay1); - relay2State = t2Active || (deviceConnected && bleRelay2); + relay1State = t1Active || (buttonToggle && digitalRead(BUTTON_PIN) != LOW) || + (connActive[0] && bleRelay1[0]) || (connActive[1] && bleRelay1[1]); + relay2State = t2Active || + (connActive[0] && bleRelay2[0]) || (connActive[1] && bleRelay2[1]); + + leds[0] = (connectedCount > 0) ? CRGB::Green : CRGB::Black; } else { relay1State = false; relay2State = false; @@ -484,35 +563,40 @@ void loop() { digitalWrite(RELAY1_PIN, relay1State ? HIGH : LOW); digitalWrite(RELAY2_PIN, relay2State ? HIGH : LOW); - digitalWrite(LED_PIN, (relay1State || relay2State) ? HIGH : LOW); + FastLED.show(); static bool lastRelay1 = false; static bool lastRelay2 = false; if (relay1State != lastRelay1 || relay2State != lastRelay2) { + bool anyBle1 = (connActive[0] && bleRelay1[0]) || (connActive[1] && bleRelay1[1]); + bool anyBle2 = (connActive[0] && bleRelay2[0]) || (connActive[1] && bleRelay2[1]); Serial.printf("Relays: R1=%s R2=%s (t1=%s t2=%s btn=%s ble1=%s ble2=%s)\n", relay1State ? "ON" : "OFF", relay2State ? "ON" : "OFF", t1Active ? "YES" : "no", t2Active ? "YES" : "no", btnActive ? "YES" : "no", - bleRelay1 ? "ON" : "OFF", - bleRelay2 ? "ON" : "OFF"); + anyBle1 ? "ON" : "OFF", + anyBle2 ? "ON" : "OFF"); lastRelay1 = relay1State; lastRelay2 = relay2State; } static uint32_t heartbeat = 0; if (!otaInProgress && millis() - heartbeat > 5000) { - Serial.printf("[heartbeat] R1=%s R2=%s t1=%s t2=%s btn=%s ble1=%s ble2=%s\n", + bool anyBle1 = (connActive[0] && bleRelay1[0]) || (connActive[1] && bleRelay1[1]); + bool anyBle2 = (connActive[0] && bleRelay2[0]) || (connActive[1] && bleRelay2[1]); + Serial.printf("[heartbeat] R1=%s R2=%s t1=%s t2=%s btn=%s ble1=%s ble2=%s clients=%d\n", relay1State ? "ON" : "OFF", relay2State ? "ON" : "OFF", t1Active ? "YES" : "no", t2Active ? "YES" : "no", btnActive ? "YES" : "no", - bleRelay1 ? "ON" : "OFF", - bleRelay2 ? "ON" : "OFF"); + anyBle1 ? "ON" : "OFF", + anyBle2 ? "ON" : "OFF", + connectedCount); heartbeat = millis(); } delay(50); -} \ No newline at end of file +} From 97c79b9f4c0568784b9b8167fc7eb0673c43b351 Mon Sep 17 00:00:00 2001 From: eddy Date: Sat, 1 Aug 2026 00:32:23 +0200 Subject: [PATCH 3/9] =?UTF-8?q?feat:=20client=20firmware=20=E2=80=94=20tou?= =?UTF-8?q?ch/button,=20BLE=20scan,=20HMAC,=20server=20UUID=20verify,=20US?= =?UTF-8?q?B=20CDC=20provisioning?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/client.cpp | 428 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 428 insertions(+) create mode 100644 src/client.cpp diff --git a/src/client.cpp b/src/client.cpp new file mode 100644 index 0000000..67c1147 --- /dev/null +++ b/src/client.cpp @@ -0,0 +1,428 @@ +#include +#include +#include + +#include +#include + +#include "version.h" + +#define TOUCH1_PIN 8 +#define TOUCH2_PIN 6 +#define BUTTON_PIN 41 +#define LED_PIN 35 +#define NUM_LEDS 1 + +CRGB leds[NUM_LEDS]; + +#define SERVICE_UUID "4fafc201-1fb5-459e-8fcc-c5c9c331914b" +#define CHARACTERISTIC_UUID "beb5483e-36e1-4688-b7f5-ea07361f26d8" +#define UUID_CHAR_UUID "beb5483e-36e1-4688-b7f5-ea07361f26d9" +#define HMAC_PKT_SIZE 37 +#define HMAC_KEY_LEN 32 +#define ESP_UUID_LEN 16 +#define NVS_NAMESPACE "hmac" +#define NVS_KEY_NAME "key" +#define NVS_UUID_NAME "uuid" +#define NVS_SERVER_NAME "server" +#define DEBOUNCE_MS 50 +#define SCAN_INTERVAL 6000 +#define SCAN_TIMEOUT 5000 + +uint8_t hmacKey[HMAC_KEY_LEN]; +uint8_t deviceUuid[ESP_UUID_LEN]; +uint8_t serverUuid[ESP_UUID_LEN]; +bool keyProvisioned = false; +bool uuidProvisioned = false; +bool serverProvisioned = false; +bool allProvisioned = false; +Preferences preferences; + +static bool connected = false; +NimBLEClient *pClient = nullptr; +NimBLERemoteCharacteristic *pCmdChar = nullptr; + +static bool relay1SendState = false; +static bool relay2SendState = false; +static uint32_t seq = 0; + +static bool bleReady = false; +static bool serverFound = false; +static NimBLEAddress serverAddress; + +class MyScanCallbacks : public NimBLEScanCallbacks { + void onResult(const NimBLEAdvertisedDevice *advertisedDevice) override { + if (advertisedDevice->haveName() && advertisedDevice->getName() == "SylDa_Server") { + serverAddress = advertisedDevice->getAddress(); + serverFound = true; + NimBLEDevice::getScan()->stop(); + } + } +}; + +class MyClientCallbacks : public NimBLEClientCallbacks { + void onConnect(NimBLEClient *client) override { + connected = true; + Serial.println("BLE: connected to server"); + } + void onDisconnect(NimBLEClient *client, int reason) override { + connected = false; + relay1SendState = false; + relay2SendState = false; + pCmdChar = nullptr; + Serial.printf("BLE: disconnected (reason=%d)\n", reason); + } +}; + +static bool isHexKey(const String &hex) { + for (int i = 0; i < hex.length(); i++) { + char c = hex.charAt(i); + if (!((c >= '0' && c <= '9') || (c >= 'A' && c <= 'F'))) return false; + } + return true; +} + +static void parseHexKey(const String &hex, uint8_t *out, int len) { + for (int i = 0; i < len; i++) { + out[i] = (uint8_t)strtoul(hex.substring(i * 2, i * 2 + 2).c_str(), nullptr, 16); + } +} + +static void buildHmacPacket(uint8_t cmd, uint8_t *packet) { + uint32_t s = ++seq; + packet[0] = cmd; + packet[1] = (s >> 24) & 0xFF; + packet[2] = (s >> 16) & 0xFF; + packet[3] = (s >> 8) & 0xFF; + packet[4] = s & 0xFF; + uint8_t hmacData[5]; + hmacData[0] = cmd; + memcpy(hmacData + 1, packet + 1, 4); + const mbedtls_md_info_t *mdInfo = mbedtls_md_info_from_type(MBEDTLS_MD_SHA256); + mbedtls_md_hmac(mdInfo, hmacKey, HMAC_KEY_LEN, hmacData, 5, packet + 5); +} + +static void disconnectClient() { + if (pClient != nullptr) { + pClient->disconnect(); + delay(100); + NimBLEDevice::deleteClient(pClient); + pClient = nullptr; + } + pCmdChar = nullptr; + connected = false; + relay1SendState = false; + relay2SendState = false; +} + +static bool connectToServer() { + pClient = NimBLEDevice::createClient(); + pClient->setClientCallbacks(new MyClientCallbacks()); + + if (!pClient->connect(serverAddress)) { + Serial.println("BLE: connect failed"); + NimBLEDevice::deleteClient(pClient); + pClient = nullptr; + return false; + } + + NimBLERemoteService *pSvc = pClient->getService(SERVICE_UUID); + if (pSvc == nullptr) { + Serial.println("BLE: service not found"); + disconnectClient(); + return false; + } + + NimBLERemoteCharacteristic *pUuidChar = pSvc->getCharacteristic(UUID_CHAR_UUID); + if (pUuidChar == nullptr) { + Serial.println("BLE: UUID characteristic not found"); + disconnectClient(); + return false; + } + + std::string uuidVal = pUuidChar->readValue(); + if (uuidVal.length() != ESP_UUID_LEN || + memcmp(uuidVal.data(), serverUuid, ESP_UUID_LEN) != 0) { + Serial.println("BLE: server UUID mismatch — disconnecting"); + disconnectClient(); + return false; + } + + pCmdChar = pSvc->getCharacteristic(CHARACTERISTIC_UUID); + if (pCmdChar == nullptr) { + Serial.println("BLE: command characteristic not found"); + disconnectClient(); + return false; + } + + seq = 0; + relay1SendState = false; + relay2SendState = false; + Serial.println("BLE: server UUID verified, ready"); + return true; +} + +void setup() { + Serial.begin(115200); + delay(2000); + Serial.println("=== SylDa Client ==="); + + FastLED.addLeds(leds, NUM_LEDS); + leds[0] = CRGB::Red; + FastLED.show(); + + preferences.begin(NVS_NAMESPACE, true); + size_t len = preferences.getBytes(NVS_KEY_NAME, hmacKey, HMAC_KEY_LEN); + size_t uuidLen = preferences.getBytes(NVS_UUID_NAME, deviceUuid, ESP_UUID_LEN); + size_t serverLen = preferences.getBytes(NVS_SERVER_NAME, serverUuid, ESP_UUID_LEN); + preferences.end(); + + if (len == HMAC_KEY_LEN) { + keyProvisioned = true; + Serial.println("HMAC key loaded from NVS"); + } else { + keyProvisioned = false; + Serial.println("No HMAC key — use USB SET_KEY to provision"); + } + + if (uuidLen == ESP_UUID_LEN) { + uuidProvisioned = true; + Serial.print("UUID loaded from NVS: "); + for (int i = 0; i < ESP_UUID_LEN; i++) Serial.printf("%02X", deviceUuid[i]); + Serial.println(); + } else { + uuidProvisioned = false; + for (int i = 0; i < ESP_UUID_LEN; i++) deviceUuid[i] = (uint8_t)(esp_random() & 0xFF); + preferences.begin(NVS_NAMESPACE, false); + preferences.putBytes(NVS_UUID_NAME, deviceUuid, ESP_UUID_LEN); + preferences.end(); + uuidProvisioned = true; + Serial.print("Generated new UUID: "); + for (int i = 0; i < ESP_UUID_LEN; i++) Serial.printf("%02X", deviceUuid[i]); + Serial.println(); + } + + if (serverLen == ESP_UUID_LEN) { + serverProvisioned = true; + Serial.print("Server UUID loaded from NVS: "); + for (int i = 0; i < ESP_UUID_LEN; i++) Serial.printf("%02X", serverUuid[i]); + Serial.println(); + } else { + serverProvisioned = false; + Serial.println("No server UUID — use USB SET_SERVER to provision"); + } + + allProvisioned = keyProvisioned && uuidProvisioned && serverProvisioned; + + pinMode(TOUCH1_PIN, INPUT_PULLUP); + pinMode(TOUCH2_PIN, INPUT_PULLUP); + pinMode(BUTTON_PIN, INPUT); + + if (allProvisioned) { + NimBLEDevice::init("SylDa_Client"); + NimBLEDevice::setScanFilterMode(CONFIG_BTDM_SCAN_DUPL_TYPE_DATA); + + NimBLEScan *pScan = NimBLEDevice::getScan(); + pScan->setScanCallbacks(new MyScanCallbacks()); + pScan->setInterval(97); + pScan->setWindow(37); + bleReady = true; + Serial.println("BLE: initialized, will scan for SylDa_Server"); + } + + Serial.println("Setup done."); +} + +void loop() { + if (Serial.available()) { + static String serialBuf; + char c = Serial.read(); + serialBuf += c; + if (c == '\n') { + serialBuf.trim(); + if (serialBuf == "KEY?") { + Serial.print("DEVICE_ID:"); + for (int i = 0; i < ESP_UUID_LEN; i++) Serial.printf("%02X", deviceUuid[i]); + Serial.println(); + Serial.print("HMAC_KEY:"); + if (keyProvisioned) { + for (int i = 0; i < HMAC_KEY_LEN; i++) Serial.printf("%02X", hmacKey[i]); + } + Serial.println(); + Serial.print("SERVER_UUID:"); + if (serverProvisioned) { + for (int i = 0; i < ESP_UUID_LEN; i++) Serial.printf("%02X", serverUuid[i]); + } + Serial.println(); + } else if (serialBuf == "VERSION?") { + Serial.print("BUILD_NR:"); + Serial.println(FIRMWARE_VERSION_CLIENT); + } else if (serialBuf.startsWith("SET_KEY:")) { + String hex = serialBuf.substring(8); + hex.toUpperCase(); + if (hex.length() == 64 && isHexKey(hex)) { + parseHexKey(hex, hmacKey, HMAC_KEY_LEN); + preferences.begin(NVS_NAMESPACE, false); + preferences.putBytes(NVS_KEY_NAME, hmacKey, HMAC_KEY_LEN); + preferences.end(); + keyProvisioned = true; + allProvisioned = keyProvisioned && serverProvisioned; + Serial.println("OK"); + Serial.println("HMAC key stored in NVS"); + if (allProvisioned && !bleReady) { + NimBLEDevice::init("SylDa_Client"); + NimBLEDevice::setScanFilterMode(CONFIG_BTDM_SCAN_DUPL_TYPE_DATA); + NimBLEScan *pScan = NimBLEDevice::getScan(); + pScan->setScanCallbacks(new MyScanCallbacks()); + pScan->setInterval(97); + pScan->setWindow(37); + bleReady = true; + Serial.println("BLE: initialized after provisioning"); + } + } else { + Serial.println("ERROR: invalid key format"); + } + } else if (serialBuf.startsWith("SET_ID:")) { + String hex = serialBuf.substring(7); + hex.toUpperCase(); + if (hex.length() == 32 && isHexKey(hex)) { + parseHexKey(hex, deviceUuid, ESP_UUID_LEN); + preferences.begin(NVS_NAMESPACE, false); + preferences.putBytes(NVS_UUID_NAME, deviceUuid, ESP_UUID_LEN); + preferences.end(); + uuidProvisioned = true; + allProvisioned = keyProvisioned && serverProvisioned; + Serial.println("OK"); + Serial.println("UUID stored in NVS"); + } else { + Serial.println("ERROR: invalid UUID format"); + } + } else if (serialBuf.startsWith("SET_SERVER:")) { + String hex = serialBuf.substring(11); + hex.toUpperCase(); + if (hex.length() == 32 && isHexKey(hex)) { + parseHexKey(hex, serverUuid, ESP_UUID_LEN); + preferences.begin(NVS_NAMESPACE, false); + preferences.putBytes(NVS_SERVER_NAME, serverUuid, ESP_UUID_LEN); + preferences.end(); + serverProvisioned = true; + allProvisioned = keyProvisioned && serverProvisioned; + Serial.println("OK"); + Serial.println("Server UUID stored in NVS"); + if (allProvisioned && !bleReady) { + NimBLEDevice::init("SylDa_Client"); + NimBLEDevice::setScanFilterMode(CONFIG_BTDM_SCAN_DUPL_TYPE_DATA); + NimBLEScan *pScan = NimBLEDevice::getScan(); + pScan->setScanCallbacks(new MyScanCallbacks()); + pScan->setInterval(97); + pScan->setWindow(37); + bleReady = true; + Serial.println("BLE: initialized after provisioning"); + } + } else { + Serial.println("ERROR: invalid server UUID format"); + } + } + serialBuf = ""; + } + } + + if (!allProvisioned) { + delay(20); + return; + } + + static bool t1Stable = false; + static bool t2Stable = false; + static bool btnStable = false; + static uint32_t t1Change = 0; + static uint32_t t2Change = 0; + static uint32_t btnChange = 0; + bool t1Raw = (digitalRead(TOUCH1_PIN) == LOW); + bool t2Raw = (digitalRead(TOUCH2_PIN) == LOW); + bool btnRaw = (digitalRead(BUTTON_PIN) == LOW); + if (t1Raw != t1Stable) { t1Change = millis(); t1Stable = t1Raw; } + if (t2Raw != t2Stable) { t2Change = millis(); t2Stable = t2Raw; } + if (btnRaw != btnStable) { btnChange = millis(); btnStable = btnRaw; } + bool t1Active = (millis() - t1Change > DEBOUNCE_MS) && t1Stable; + bool t2Active = (millis() - t2Change > DEBOUNCE_MS) && t2Stable; + bool btnActive = (millis() - btnChange > DEBOUNCE_MS) && btnStable; + + static bool prevBtnActive = false; + static bool buttonToggle = false; + if (btnActive && !prevBtnActive) { + buttonToggle = !buttonToggle; + Serial.printf("Button: toggle -> %s\n", buttonToggle ? "ON" : "OFF"); + } + prevBtnActive = btnActive; + + bool relay1State = t1Active || buttonToggle; + bool relay2State = t2Active; + + if (relay1State || relay2State) { + leds[0] = CRGB::Green; + } else if (connected) { + leds[0] = CRGB::Blue; + } else { + leds[0] = CRGB::Red; + } + FastLED.show(); + + if (connected && pCmdChar != nullptr) { + if (relay1State != relay1SendState) { + uint8_t pkt[HMAC_PKT_SIZE]; + buildHmacPacket(relay1State ? 0x01 : 0x00, pkt); + pCmdChar->writeValue(pkt, HMAC_PKT_SIZE, false); + relay1SendState = relay1State; + Serial.printf("TX: relay1 %s (seq=%lu)\n", relay1State ? "ON" : "OFF", seq); + } + if (relay2State != relay2SendState) { + uint8_t pkt[HMAC_PKT_SIZE]; + buildHmacPacket(relay2State ? 0x02 : 0x03, pkt); + pCmdChar->writeValue(pkt, HMAC_PKT_SIZE, false); + relay2SendState = relay2State; + Serial.printf("TX: relay2 %s (seq=%lu)\n", relay2State ? "ON" : "OFF", seq); + } + } + + static uint32_t lastScanStart = 0; + static bool scanRunnable = false; + + if (!connected) { + NimBLEScan *pScan = NimBLEDevice::getScan(); + if ((millis() - lastScanStart > SCAN_INTERVAL) && !pScan->isScanning()) { + serverFound = false; + pScan->start(SCAN_TIMEOUT / 1000, false); + lastScanStart = millis(); + scanRunnable = true; + } + + if (!pScan->isScanning() && scanRunnable) { + scanRunnable = false; + if (serverFound) { + Serial.printf("BLE: found SylDa_Server (%s), connecting...\n", + serverAddress.toString().c_str()); + connectToServer(); + } + } + } + + if (connected && pClient != nullptr && !pClient->isConnected()) { + Serial.println("BLE: lost connection"); + disconnectClient(); + } + + static uint32_t heartbeat = 0; + if (millis() - heartbeat > 5000) { + Serial.printf("[heartbeat] t1=%s t2=%s btn=%s r1=%s r2=%s connected=%s\n", + t1Active ? "YES" : "no", + t2Active ? "YES" : "no", + btnActive ? "YES" : "no", + relay1State ? "ON" : "OFF", + relay2State ? "ON" : "OFF", + connected ? "yes" : "no"); + heartbeat = millis(); + } + + delay(20); +} From bce5fc41078e5123e9e939ca771e6b9fba999596 Mon Sep 17 00:00:00 2001 From: eddy Date: Sat, 1 Aug 2026 00:35:15 +0200 Subject: [PATCH 4/9] =?UTF-8?q?fix:=20client=20=E2=80=94=20spec=20complian?= =?UTF-8?q?ce,=20memory=20leak,=20LED=20order?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/client.cpp | 50 ++++++++++++++++++++++++++++---------------------- 1 file changed, 28 insertions(+), 22 deletions(-) diff --git a/src/client.cpp b/src/client.cpp index 67c1147..a8d28df 100644 --- a/src/client.cpp +++ b/src/client.cpp @@ -3,6 +3,7 @@ #include #include +#include #include #include "version.h" @@ -74,6 +75,8 @@ class MyClientCallbacks : public NimBLEClientCallbacks { } }; +static MyClientCallbacks clientCallbacks; + static bool isHexKey(const String &hex) { for (int i = 0; i < hex.length(); i++) { char c = hex.charAt(i); @@ -117,7 +120,7 @@ static void disconnectClient() { static bool connectToServer() { pClient = NimBLEDevice::createClient(); - pClient->setClientCallbacks(new MyClientCallbacks()); + pClient->setClientCallbacks(&clientCallbacks); if (!pClient->connect(serverAddress)) { Serial.println("BLE: connect failed"); @@ -167,7 +170,7 @@ void setup() { delay(2000); Serial.println("=== SylDa Client ==="); - FastLED.addLeds(leds, NUM_LEDS); + FastLED.addLeds(leds, NUM_LEDS); leds[0] = CRGB::Red; FastLED.show(); @@ -266,7 +269,7 @@ void loop() { preferences.putBytes(NVS_KEY_NAME, hmacKey, HMAC_KEY_LEN); preferences.end(); keyProvisioned = true; - allProvisioned = keyProvisioned && serverProvisioned; + allProvisioned = keyProvisioned && uuidProvisioned && serverProvisioned; Serial.println("OK"); Serial.println("HMAC key stored in NVS"); if (allProvisioned && !bleReady) { @@ -291,7 +294,7 @@ void loop() { preferences.putBytes(NVS_UUID_NAME, deviceUuid, ESP_UUID_LEN); preferences.end(); uuidProvisioned = true; - allProvisioned = keyProvisioned && serverProvisioned; + allProvisioned = keyProvisioned && uuidProvisioned && serverProvisioned; Serial.println("OK"); Serial.println("UUID stored in NVS"); } else { @@ -306,7 +309,7 @@ void loop() { preferences.putBytes(NVS_SERVER_NAME, serverUuid, ESP_UUID_LEN); preferences.end(); serverProvisioned = true; - allProvisioned = keyProvisioned && serverProvisioned; + allProvisioned = keyProvisioned && uuidProvisioned && serverProvisioned; Serial.println("OK"); Serial.println("Server UUID stored in NVS"); if (allProvisioned && !bleReady) { @@ -349,14 +352,12 @@ void loop() { bool btnActive = (millis() - btnChange > DEBOUNCE_MS) && btnStable; static bool prevBtnActive = false; - static bool buttonToggle = false; if (btnActive && !prevBtnActive) { - buttonToggle = !buttonToggle; - Serial.printf("Button: toggle -> %s\n", buttonToggle ? "ON" : "OFF"); + Serial.printf("Button: pressed\n"); } prevBtnActive = btnActive; - bool relay1State = t1Active || buttonToggle; + bool relay1State = t1Active || btnActive; bool relay2State = t2Active; if (relay1State || relay2State) { @@ -369,19 +370,24 @@ void loop() { FastLED.show(); if (connected && pCmdChar != nullptr) { - if (relay1State != relay1SendState) { - uint8_t pkt[HMAC_PKT_SIZE]; - buildHmacPacket(relay1State ? 0x01 : 0x00, pkt); - pCmdChar->writeValue(pkt, HMAC_PKT_SIZE, false); - relay1SendState = relay1State; - Serial.printf("TX: relay1 %s (seq=%lu)\n", relay1State ? "ON" : "OFF", seq); - } - if (relay2State != relay2SendState) { - uint8_t pkt[HMAC_PKT_SIZE]; - buildHmacPacket(relay2State ? 0x02 : 0x03, pkt); - pCmdChar->writeValue(pkt, HMAC_PKT_SIZE, false); - relay2SendState = relay2State; - Serial.printf("TX: relay2 %s (seq=%lu)\n", relay2State ? "ON" : "OFF", seq); + static uint32_t lastCmd = 0; + if (millis() - lastCmd > 200) { + if (relay1State != relay1SendState) { + uint8_t pkt[HMAC_PKT_SIZE]; + buildHmacPacket(relay1State ? 0x01 : 0x00, pkt); + pCmdChar->writeValue(pkt, HMAC_PKT_SIZE, false); + relay1SendState = relay1State; + Serial.printf("TX: relay1 %s (seq=%lu)\n", relay1State ? "ON" : "OFF", seq); + lastCmd = millis(); + } + if (relay2State != relay2SendState) { + uint8_t pkt[HMAC_PKT_SIZE]; + buildHmacPacket(relay2State ? 0x02 : 0x03, pkt); + pCmdChar->writeValue(pkt, HMAC_PKT_SIZE, false); + relay2SendState = relay2State; + Serial.printf("TX: relay2 %s (seq=%lu)\n", relay2State ? "ON" : "OFF", seq); + lastCmd = millis(); + } } } From 793c8f190c80a697614633bd21cd6d049fa6e159 Mon Sep 17 00:00:00 2001 From: eddy Date: Sat, 1 Aug 2026 00:37:52 +0200 Subject: [PATCH 5/9] =?UTF-8?q?feat:=20Android=20=E2=80=94=20device/transp?= =?UTF-8?q?ort=20radio=20buttons,=20PrefsHelper=20keys?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/nl/eddydevink/sylda/PrefsHelper.kt | 8 +++ .../nl/eddydevink/sylda/SettingsActivity.kt | 20 +++++++ .../src/main/res/layout/activity_settings.xml | 53 +++++++++++++++++++ 3 files changed, 81 insertions(+) diff --git a/android/app/src/main/java/nl/eddydevink/sylda/PrefsHelper.kt b/android/app/src/main/java/nl/eddydevink/sylda/PrefsHelper.kt index 5b123ac..f446ef5 100644 --- a/android/app/src/main/java/nl/eddydevink/sylda/PrefsHelper.kt +++ b/android/app/src/main/java/nl/eddydevink/sylda/PrefsHelper.kt @@ -15,8 +15,16 @@ object PrefsHelper { const val PREF_THEME = "theme" const val PREF_LABEL1 = "label_relay1" const val PREF_LABEL2 = "label_relay2" + const val KEY_DEVICE_MODE = "deviceMode" + const val KEY_SERVER_UUID = "serverUuid" + const val KEY_CLIENT_UUID = "clientUuid" const val SEQ_MAX = 1_000_000 + fun getDeviceMode(context: Context): String = getPrefs(context).getString(KEY_DEVICE_MODE, "server") ?: "server" + fun setDeviceMode(context: Context, mode: String) = getPrefs(context).edit().putString(KEY_DEVICE_MODE, mode).apply() + fun getTransportMode(context: Context): String = getPrefs(context).getString("transportMode", "ble") ?: "ble" + fun setTransportMode(context: Context, mode: String) = getPrefs(context).edit().putString("transportMode", mode).apply() + fun applyTheme(context: Context) { val prefs = getPrefs(context) val mode = when (prefs.getString(PREF_THEME, "system")) { diff --git a/android/app/src/main/java/nl/eddydevink/sylda/SettingsActivity.kt b/android/app/src/main/java/nl/eddydevink/sylda/SettingsActivity.kt index 59ea230..e3cf253 100644 --- a/android/app/src/main/java/nl/eddydevink/sylda/SettingsActivity.kt +++ b/android/app/src/main/java/nl/eddydevink/sylda/SettingsActivity.kt @@ -140,6 +140,26 @@ class SettingsActivity : AppCompatActivity() { supportActionBar?.setDisplayHomeAsUpEnabled(true) supportActionBar?.title = getString(R.string.title_settings) + val radioDeviceMode = findViewById(R.id.radioDeviceMode) + val radioTransport = findViewById(R.id.radioTransport) + + radioDeviceMode.check( + if (PrefsHelper.getDeviceMode(this) == "client") R.id.radioClient else R.id.radioServer + ) + radioTransport.check( + if (PrefsHelper.getTransportMode(this) == "usb") R.id.radioUsb else R.id.radioBle + ) + + radioDeviceMode.setOnCheckedChangeListener { _, checkedId -> + val mode = if (checkedId == R.id.radioClient) "client" else "server" + PrefsHelper.setDeviceMode(this, mode) + } + + radioTransport.setOnCheckedChangeListener { _, checkedId -> + val mode = if (checkedId == R.id.radioUsb) "usb" else "ble" + PrefsHelper.setTransportMode(this, mode) + } + findViewById