#include #include #include #include #define RELAY1_PIN 1 #define RELAY2_PIN 2 #define TOUCH1_PIN 8 #define TOUCH2_PIN 6 #define BUTTON_PIN 41 #define LED_PIN 46 #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 // 1 cmd + 4 seq + 32 hmac #define HMAC_KEY_LEN 32 #define ESP_UUID_LEN 16 #define NVS_NAMESPACE "hmac" #define NVS_KEY_NAME "key" #define NVS_UUID_NAME "uuid" NimBLECharacteristic *pCharacteristic; bool deviceConnected = false; bool bleRelay1 = false; bool bleRelay2 = false; bool relay1State = false; bool relay2State = false; uint32_t lastSeq = 0; uint8_t hmacKey[HMAC_KEY_LEN]; uint8_t deviceUuid[ESP_UUID_LEN]; bool keyProvisioned = false; bool uuidProvisioned = false; Preferences preferences; class MyServerCallbacks : public NimBLEServerCallbacks { void onConnect(NimBLEServer *pServer, NimBLEConnInfo &connInfo) override { deviceConnected = true; lastSeq = 0; Serial.println("BLE: device connected"); } void onDisconnect(NimBLEServer *pServer, NimBLEConnInfo &connInfo, int reason) override { deviceConnected = false; bleRelay1 = false; bleRelay2 = false; lastSeq = 0; Serial.println("BLE: device disconnected"); NimBLEDevice::startAdvertising(); } }; static bool verifyHmac(const uint8_t *packet) { 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); return false; } uint8_t hmacData[5]; hmacData[0] = cmd; hmacData[1] = (seq >> 24) & 0xFF; hmacData[2] = (seq >> 16) & 0xFF; hmacData[3] = (seq >> 8) & 0xFF; hmacData[4] = seq & 0xFF; uint8_t computed[32]; const mbedtls_md_info_t *mdInfo = mbedtls_md_info_from_type(MBEDTLS_MD_SHA256); mbedtls_md_hmac(mdInfo, hmacKey, HMAC_KEY_LEN, hmacData, 5, computed); if (memcmp(computed, packet + 5, 32) != 0) { Serial.println("BLE: HMAC mismatch"); return false; } lastSeq = seq; return true; } 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) { for (int i = 0; i < HMAC_KEY_LEN; i++) { out[i] = (uint8_t)strtoul(hex.substring(i * 2, i * 2 + 2).c_str(), nullptr, 16); } } class MyCharacteristicCallbacks : public NimBLECharacteristicCallbacks { void onWrite(NimBLECharacteristic *pChar, NimBLEConnInfo &connInfo) override { std::string value = pChar->getValue(); if (value.length() != HMAC_PKT_SIZE) { Serial.printf("BLE: bad packet length %d (expected %d)\n", (int)value.length(), HMAC_PKT_SIZE); return; } const uint8_t *pkt = (const uint8_t *)value.data(); if (!verifyHmac(pkt)) { return; } uint8_t cmd = pkt[0]; uint32_t seq = lastSeq; if (cmd == 0x01) { bleRelay1 = true; Serial.printf("BLE: relay1 ON (seq=%lu)\n", seq); } else if (cmd == 0x00) { bleRelay1 = false; Serial.printf("BLE: relay1 OFF (seq=%lu)\n", seq); } else if (cmd == 0x02) { bleRelay2 = true; Serial.printf("BLE: relay2 ON (seq=%lu)\n", seq); } else if (cmd == 0x03) { bleRelay2 = false; Serial.printf("BLE: relay2 OFF (seq=%lu)\n", seq); } } }; void setup() { Serial.begin(115200); delay(2000); Serial.println("=== Atom VoiceS3R BLE Relay ==="); 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); preferences.end(); if (len == HMAC_KEY_LEN) { keyProvisioned = true; Serial.println("HMAC key loaded from NVS"); } else { keyProvisioned = false; Serial.println("No HMAC key provisioned — connect USB and run SET_KEY"); } 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(); } pinMode(TOUCH1_PIN, INPUT_PULLUP); pinMode(TOUCH2_PIN, INPUT_PULLUP); 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); delay(200); digitalWrite(LED_PIN, LOW); delay(200); } Serial.println("LED blink done, starting BLE..."); NimBLEDevice::init("SylDa_Server"); NimBLEServer *pServer = NimBLEDevice::createServer(); pServer->setCallbacks(new MyServerCallbacks()); NimBLEService *pService = pServer->createService(SERVICE_UUID); pCharacteristic = pService->createCharacteristic( CHARACTERISTIC_UUID, NIMBLE_PROPERTY::WRITE_NR | NIMBLE_PROPERTY::NOTIFY ); pCharacteristic->setCallbacks(new MyCharacteristicCallbacks()); pCharacteristic->setValue("0"); NimBLECharacteristic *pUuidChar = pService->createCharacteristic( UUID_CHAR_UUID, NIMBLE_PROPERTY::READ ); pUuidChar->setValue(deviceUuid, ESP_UUID_LEN); NimBLEAdvertising *pAdvertising = NimBLEDevice::getAdvertising(); pAdvertising->addServiceUUID(SERVICE_UUID); pAdvertising->enableScanResponse(true); pAdvertising->setMinInterval(32); pAdvertising->setMaxInterval(64); NimBLEDevice::startAdvertising(); Serial.println("BLE: advertising started as 'SylDa_Server'"); if (keyProvisioned) { Serial.print("HMAC Key: "); for (int i = 0; i < HMAC_KEY_LEN; i++) { Serial.printf("%02X", hmacKey[i]); } Serial.println(); } else { Serial.println("No HMAC key — use USB SET_KEY to provision"); } 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?") { if (keyProvisioned && uuidProvisioned) { Serial.print("DEVICE_ID:"); for (int i = 0; i < ESP_UUID_LEN; i++) Serial.printf("%02X", deviceUuid[i]); Serial.println(); Serial.print("HMAC_KEY:"); for (int i = 0; i < HMAC_KEY_LEN; i++) { Serial.printf("%02X", hmacKey[i]); } Serial.println(); } else { Serial.println("NO_KEY"); } } else if (serialBuf.startsWith("SET_KEY:")) { String hex = serialBuf.substring(8); hex.toUpperCase(); if (hex.length() == 64 && isHexKey(hex)) { parseHexKey(hex, hmacKey); preferences.begin(NVS_NAMESPACE, false); preferences.putBytes(NVS_KEY_NAME, hmacKey, HMAC_KEY_LEN); preferences.end(); keyProvisioned = true; Serial.println("OK"); Serial.println("HMAC key stored in NVS"); } 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)) { for (int i = 0; i < ESP_UUID_LEN; i++) { deviceUuid[i] = (uint8_t)strtoul(hex.substring(i * 2, i * 2 + 2).c_str(), nullptr, 16); } preferences.begin(NVS_NAMESPACE, false); preferences.putBytes(NVS_UUID_NAME, deviceUuid, ESP_UUID_LEN); preferences.end(); uuidProvisioned = true; Serial.println("OK"); Serial.println("UUID stored in NVS"); } else { Serial.println("ERROR: invalid UUID format"); } } serialBuf = ""; } } 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; #define DEBOUNCE_MS 50 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; relay1State = t1Active || btnActive || (deviceConnected && bleRelay1); relay2State = t2Active || (deviceConnected && bleRelay2); digitalWrite(RELAY1_PIN, relay1State ? HIGH : LOW); digitalWrite(RELAY2_PIN, relay2State ? HIGH : LOW); digitalWrite(LED_PIN, (relay1State || relay2State) ? HIGH : LOW); static bool lastRelay1 = false; static bool lastRelay2 = false; if (relay1State != lastRelay1 || relay2State != lastRelay2) { 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"); lastRelay1 = relay1State; lastRelay2 = relay2State; } static uint32_t heartbeat = 0; if (millis() - heartbeat > 5000) { Serial.printf("[heartbeat] 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"); heartbeat = millis(); } delay(50); }