Project_SylDa/src/main.cpp
2026-08-01 00:26:50 +02:00

602 lines
21 KiB
C++

#include <Arduino.h>
#include <NimBLEDevice.h>
#include <NimBLEAdvertising.h>
#include <FastLED.h>
#include <mbedtls/md.h>
#include <mbedtls/sha256.h>
#include <Preferences.h>
#include <esp_ota_ops.h>
#include <esp_partition.h>
#include <nvs_flash.h>
#define RELAY1_PIN 39
#define RELAY2_PIN 38
#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 MAX_CLIENTS 2
#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 VERSION_UUID "beb5483e-36e1-4688-b7f5-ea07361f26db"
#define OTA_UUID "beb5483e-36e1-4688-b7f5-ea07361f26da"
#define HMAC_PKT_SIZE 37
#define OTA_CHUNK_SIZE 500
#define OTA_CMD_START 0x10
#define OTA_CMD_END 0x11
#define OTA_STATUS_ACK 0x00
#define OTA_STATUS_NAK 0x01
#define OTA_STATUS_COMPLETE 0x10
#define OTA_STATUS_ERROR 0x11
#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 FIRMWARE_VERSION "6"
NimBLECharacteristic *pCharacteristic;
bool relay1State = false;
bool relay2State = false;
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;
const esp_partition_t *otaPartition = nullptr;
uint32_t otaExpectedSize = 0;
uint32_t otaWritten = 0;
uint8_t otaExpectedSha[32];
mbedtls_sha256_context otaShaCtx;
bool otaShaStarted = false;
uint8_t hmacKey[HMAC_KEY_LEN];
uint8_t deviceUuid[ESP_UUID_LEN];
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 {
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 {
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, 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[idx]) {
Serial.printf("BLE: replay rejected (seq=%lu, last=%lu, slot=%d)\n", seq, lastSeq[idx], idx);
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[idx] = 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);
}
}
static void ledOtaBusy() {
leds[0] = (millis() / 100) % 2 ? CRGB::Green : CRGB::Black;
}
static void otaReset() {
otaInProgress = false;
otaHandle = 0;
otaPartition = nullptr;
otaExpectedSize = 0;
otaWritten = 0;
if (otaShaStarted) {
mbedtls_sha256_free(&otaShaCtx);
otaShaStarted = false;
}
}
static void sendOtaNotify(NimBLECharacteristic *otaChar, uint8_t status, uint32_t seq) {
uint8_t buf[5];
buf[0] = status;
buf[1] = (seq >> 24) & 0xFF;
buf[2] = (seq >> 16) & 0xFF;
buf[3] = (seq >> 8) & 0xFF;
buf[4] = seq & 0xFF;
otaChar->setValue(buf, 5);
otaChar->notify();
}
static void abortOta(NimBLECharacteristic *otaChar, const char *reason) {
Serial.printf("OTA: abort — %s\n", reason);
if (otaHandle != 0) {
esp_ota_end(otaHandle);
}
otaReset();
sendOtaNotify(otaChar, OTA_STATUS_ERROR, 0);
}
class OtaCharacteristicCallbacks : public NimBLECharacteristicCallbacks {
void onWrite(NimBLECharacteristic *pChar, NimBLEConnInfo &connInfo) override {
std::string value = pChar->getValue();
if (value.empty()) return;
const uint8_t *data = (const uint8_t *)value.data();
size_t len = value.length();
if (len == 5 && data[0] == OTA_CMD_START) {
if (otaInProgress) {
esp_ota_end(otaHandle);
otaReset();
}
otaExpectedSize = ((uint32_t)data[1] << 24) | ((uint32_t)data[2] << 16) |
((uint32_t)data[3] << 8) | (uint32_t)data[4];
otaPartition = esp_ota_get_next_update_partition(nullptr);
if (otaPartition == nullptr) {
Serial.println("OTA: no update partition found");
sendOtaNotify(pChar, OTA_STATUS_ERROR, 0);
return;
}
esp_err_t err = esp_ota_begin(otaPartition, otaExpectedSize, &otaHandle);
if (err != ESP_OK) {
Serial.printf("OTA: begin failed (%d)\n", err);
sendOtaNotify(pChar, OTA_STATUS_ERROR, 0);
return;
}
otaInProgress = true;
otaWritten = 0;
mbedtls_sha256_init(&otaShaCtx);
mbedtls_sha256_starts_ret(&otaShaCtx, 0);
otaShaStarted = true;
digitalWrite(RELAY1_PIN, LOW);
digitalWrite(RELAY2_PIN, LOW);
relay1State = false;
relay2State = 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;
}
if (len == 32 && otaInProgress) {
memcpy(otaExpectedSha, data, 32);
uint8_t computedSha[32];
mbedtls_sha256_finish_ret(&otaShaCtx, computedSha);
mbedtls_sha256_free(&otaShaCtx);
otaShaStarted = false;
esp_err_t err = esp_ota_end(otaHandle);
if (err != ESP_OK) {
Serial.printf("OTA: end failed (%d)\n", err);
otaReset();
sendOtaNotify(pChar, OTA_STATUS_ERROR, 0);
return;
}
if (memcmp(computedSha, otaExpectedSha, 32) != 0) {
Serial.println("OTA: SHA256 mismatch — aborting");
otaReset();
sendOtaNotify(pChar, OTA_STATUS_ERROR, 0);
return;
}
err = esp_ota_set_boot_partition(otaPartition);
if (err != ESP_OK) {
Serial.printf("OTA: set boot partition failed (%d)\n", err);
otaReset();
sendOtaNotify(pChar, OTA_STATUS_ERROR, 0);
return;
}
leds[0] = CRGB::Green;
FastLED.show();
sendOtaNotify(pChar, OTA_STATUS_COMPLETE, 0);
Serial.println("OTA: SHA256 OK, rebooting...");
delay(500);
esp_restart();
return;
}
if (!otaInProgress) return;
if (data[0] < 0x10) {
uint32_t seq = ((uint32_t)data[0] << 24) | ((uint32_t)data[1] << 16) |
((uint32_t)data[2] << 8) | (uint32_t)data[3];
size_t dataLen = len - 4;
mbedtls_sha256_update_ret(&otaShaCtx, data + 4, dataLen);
esp_err_t err = esp_ota_write(otaHandle, data + 4, dataLen);
if (err != ESP_OK) {
Serial.printf("OTA: write err at seq %lu (%d)\n", seq, err);
sendOtaNotify(pChar, OTA_STATUS_NAK, seq);
abortOta(pChar, "write error");
return;
}
otaWritten += dataLen;
sendOtaNotify(pChar, OTA_STATUS_ACK, seq);
}
}
};
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",
(int)value.length(), HMAC_PKT_SIZE);
return;
}
const uint8_t *pkt = (const uint8_t *)value.data();
if (!verifyHmac(pkt, idx)) {
return;
}
uint8_t cmd = pkt[0];
uint32_t seq = lastSeq[idx];
if (cmd == 0x01) {
bleRelay1[idx] = true;
Serial.printf("BLE: relay1 ON (seq=%lu, slot=%d)\n", seq, idx);
} else if (cmd == 0x00) {
bleRelay1[idx] = false;
Serial.printf("BLE: relay1 OFF (seq=%lu, slot=%d)\n", seq, idx);
} else if (cmd == 0x02) {
bleRelay2[idx] = true;
Serial.printf("BLE: relay2 ON (seq=%lu, slot=%d)\n", seq, idx);
} else if (cmd == 0x03) {
bleRelay2[idx] = false;
Serial.printf("BLE: relay2 OFF (seq=%lu, slot=%d)\n", seq, idx);
}
}
};
void setup() {
Serial.begin(115200);
delay(2000);
Serial.println("=== AtomS3 Lite BLE Relay ===");
FastLED.addLeds<WS2812, LED_PIN, GRB>(leds, NUM_LEDS);
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);
for (int i = 0; i < 3; i++) {
leds[0] = CRGB::Green;
FastLED.show();
delay(200);
leds[0] = CRGB::Black;
FastLED.show();
delay(200);
}
Serial.println("LED blink done, starting BLE...");
NimBLEDevice::init("SylDa_Server");
NimBLEDevice::setMTU(248);
NimBLEServer *pServer = NimBLEDevice::createServer();
pServer->setCallbacks(new MyServerCallbacks());
pServer->advertiseOnDisconnect(true);
NimBLEService *pService = pServer->createService(SERVICE_UUID);
pCharacteristic = pService->createCharacteristic(
CHARACTERISTIC_UUID,
NIMBLE_PROPERTY::WRITE_NR
);
pCharacteristic->setCallbacks(new MyCharacteristicCallbacks());
NimBLECharacteristic *pUuidChar = pService->createCharacteristic(
UUID_CHAR_UUID,
NIMBLE_PROPERTY::READ
);
pUuidChar->setValue(deviceUuid, ESP_UUID_LEN);
NimBLECharacteristic *pVersionChar = pService->createCharacteristic(
VERSION_UUID,
NIMBLE_PROPERTY::READ
);
pVersionChar->setValue(FIRMWARE_VERSION);
NimBLECharacteristic *pOtaChar = pService->createCharacteristic(
OTA_UUID,
NIMBLE_PROPERTY::WRITE_NR | NIMBLE_PROPERTY::NOTIFY
);
pOtaChar->setCallbacks(new OtaCharacteristicCallbacks());
NimBLEAdvertising *pAdvertising = NimBLEDevice::getAdvertising();
pAdvertising->setName("SylDa_Server");
pAdvertising->addServiceUUID(SERVICE_UUID);
NimBLEAdvertisementData scanResp;
scanResp.setCompleteServices(NimBLEUUID(SERVICE_UUID));
pAdvertising->setScanResponseData(scanResp);
pAdvertising->enableScanResponse(true);
pAdvertising->setMinInterval(32);
pAdvertising->setMaxInterval(64);
bool advOk = pAdvertising->start();
Serial.printf("BLE: advertising started as 'SylDa_Server' (start=%d, isAdv=%d)\n",
advOk, pAdvertising->isAdvertising());
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 (!otaInProgress && 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 == "VERSION?") {
Serial.print("BUILD_NR:");
Serial.println(FIRMWARE_VERSION);
} 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;
static bool prevBtnActive = false;
if (btnActive && !prevBtnActive) {
buttonToggle = !buttonToggle;
Serial.printf("Button: toggle -> %s\n", buttonToggle ? "ON" : "OFF");
}
prevBtnActive = btnActive;
if (!otaInProgress) {
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;
ledOtaBusy();
}
digitalWrite(RELAY1_PIN, relay1State ? HIGH : LOW);
digitalWrite(RELAY2_PIN, 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",
anyBle1 ? "ON" : "OFF",
anyBle2 ? "ON" : "OFF");
lastRelay1 = relay1State;
lastRelay2 = relay2State;
}
static uint32_t heartbeat = 0;
if (!otaInProgress && millis() - heartbeat > 5000) {
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",
anyBle1 ? "ON" : "OFF",
anyBle2 ? "ON" : "OFF",
connectedCount);
heartbeat = millis();
}
delay(50);
}