- Firmware: add VERSION? command returning BUILD_NR for version check - Android: flash button checks firmware version first, skips flash if current - After flash: auto key exchange via reboot detection (expectingReboot flag) - device_filter.xml + intent-filter: USB permission 'always allow' checkbox - singleTop + onNewIntent prevents unwanted SettingsActivity launch on USB connect - platformio.ini: upload_port updated to /dev/ttyACM1
346 lines
No EOL
12 KiB
C++
346 lines
No EOL
12 KiB
C++
#include <Arduino.h>
|
|
#include <NimBLEDevice.h>
|
|
#include <NimBLEAdvertising.h>
|
|
|
|
#include <mbedtls/md.h>
|
|
#include <Preferences.h>
|
|
|
|
#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"
|
|
#define FIRMWARE_VERSION "1"
|
|
|
|
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->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 (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;
|
|
|
|
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);
|
|
} |