From 97c79b9f4c0568784b9b8167fc7eb0673c43b351 Mon Sep 17 00:00:00 2001 From: eddy Date: Sat, 1 Aug 2026 00:32:23 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20client=20firmware=20=E2=80=94=20touch/b?= =?UTF-8?q?utton,=20BLE=20scan,=20HMAC,=20server=20UUID=20verify,=20USB=20?= =?UTF-8?q?CDC=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); +}