/* ==================================================================
ESP32 DEVKIT V1
AUTOMOTIVE-GRADE BLE TAG + PHONE CONTROL SYSTEM
Version: FINAL-2 (24/7 Production)
==================================================================
🔒 FIXED REQUIREMENTS:
------------------------------------------------------------------
Octocoupler 1 (⚡) = GPIO 32 → Ignition ON (2 quick presses)
Octocoupler 2 (🔓) = GPIO 33 → Ignition OFF (1 press)
Octocoupler 3 (🔒) = GPIO 04 → Motor Lock (1 press)
Relay 1 (Trunk) = GPIO 27
Relay 2 (Horn) = GPIO 25
Relay 3 (Indicator) = GPIO 26
Onboard LED = GPIO 02
RSSI Switch = GPIO 18
------------------------------------------------------------------
SWITCH:
ON (LOW) → Safe Mode : RSSI OFF, no disconnect action
OFF (HIGH) → Normal Mode : RSSI ON, full disconnect action
================================================================== */
#include <BLEDevice.h>
#include <BLEServer.h>
#include <BLEUtils.h>
#include <BLE2902.h>
#include <BLEAdvertisedDevice.h>
#include <esp_task_wdt.h>
#include <esp_bt.h>
#include <esp_sleep.h>
#include "soc/soc.h"
#include "soc/rtc_cntl_reg.h"
#if defined(ESP_ARDUINO_VERSION_MAJOR) && ESP_ARDUINO_VERSION_MAJOR >= 3
#define CORE_V3 1
#else
#define CORE_V3 0
#endif
// ==================================================================
// FIXED HARDWARE PIN DEFINITIONS
// ==================================================================
#define RELAY_HORN 25
#define RELAY_IND 26
#define RELAY_TRUNK 27
#define OCTO_TRIGGER 32
#define OCTO_UNLOCK 33
#define OCTO_LOCK 04
#define ONBOARD_LED 2
#define SWITCH_PIN 18
// ==================================================================
// FIXED BLE PARAMETERS
// ==================================================================
#define TAG_SERVICE_UUID "0000ffe0-0000-1000-8000-00805f9b34fb"
#define TAG_CHAR_UUID "0000ffe1-0000-1000-8000-00805f9b34fb"
#define PHONE_SERVICE_UUID "6E400001-B5A3-F393-E0A9-E50E24DCCA9E"
#define PHONE_CHAR_UUID "6E400002-B5A3-F393-E0A9-E50E24DCCA9E"
#define CCCD_UUID 0x2902
static String TAG_MAC = "ff:ff:11:ba:87:0c";
// ==================================================================
// TUNABLE PARAMETERS
// ==================================================================
#define DIRECT_CONNECT_MS 800
#define FAST_LOOP_DELAY_MS 20
#define DISCONNECT_CLEANUP_MS 120
#define HEAP_MIN_SAFE 18000
#define HEAP_CRITICAL 12000
#define RSSI_WEAK_THRESHOLD -90
// ==================================================================
// EVENT TYPES
// ==================================================================
enum EventType : uint8_t {
EVT_NONE = 0,
EVT_TAG_CONNECTED,
EVT_TAG_DISCONNECTED,
EVT_PHONE_CONNECTED,
EVT_PHONE_DISCONNECTED,
EVT_TAG_BUTTON,
EVT_PHONE_CMD_OPEN_BOOT,
EVT_PHONE_CMD_UNLOCK,
EVT_PHONE_CMD_LOCK
};
// ==================================================================
// GLOBAL STATE
// ==================================================================
BLEClient* tagClient = nullptr;
BLEServer* phoneServer = nullptr;
BLEScan* bleScan = nullptr;
BLERemoteCharacteristic* tagChar = nullptr;
BLECharacteristic* phoneChar = nullptr;
volatile bool tagConnected = false;
volatile bool phoneConnected = false;
volatile bool hadActiveConnection = false;
volatile bool patternBusy = false;
volatile bool tagFound = false;
volatile bool systemReady = false;
volatile uint32_t lastTagEventMs = 0;
volatile uint32_t lastPhoneEventMs = 0;
SemaphoreHandle_t stateMutex;
QueueHandle_t eventQueue;
// ==================================================================
// FUNCTION PROTOTYPES
// ==================================================================
void welcomePattern();
void goodbyePattern();
void bootPattern();
void tagNotifyCallback(BLERemoteCharacteristic*, uint8_t*, size_t, bool);
// ==================================================================
// HELPER: MUTEX
// ==================================================================
bool lockState(TickType_t t = pdMS_TO_TICKS(10)) {
return xSemaphoreTake(stateMutex, t) == pdTRUE;
}
void unlockState() { xSemaphoreGive(stateMutex); }
// ==================================================================
// SAFE GPIO STATE CONTROL
// ==================================================================
inline void relayOn(int pin) { digitalWrite(pin, LOW); }
inline void relayOff(int pin) { digitalWrite(pin, HIGH); }
inline void octoOn(int pin) { digitalWrite(pin, HIGH); }
inline void octoOff(int pin) { digitalWrite(pin, LOW); }
void forceAllOutputsSafe() {
relayOff(RELAY_HORN);
relayOff(RELAY_IND);
relayOff(RELAY_TRUNK);
octoOff(OCTO_TRIGGER);
octoOff(OCTO_UNLOCK);
octoOff(OCTO_LOCK);
}
// ==================================================================
// SWITCH STATE HELPERS
// ==================================================================
inline bool isSafeMode() {
return digitalRead(SWITCH_PIN) == LOW;
}
inline bool isRssiFilterEnabled() {
return !isSafeMode();
}
// ==================================================================
// WDT-SAFE DELAY
// ==================================================================
void wdtSafeDelay(uint32_t ms) {
uint32_t elapsed = 0;
while (elapsed < ms) {
uint32_t step = (ms - elapsed > 50) ? 50 : (ms - elapsed);
vTaskDelay(pdMS_TO_TICKS(step));
esp_task_wdt_reset();
elapsed += step;
}
}
void octoPulse(int pin, uint32_t ms) {
octoOn(pin);
wdtSafeDelay(ms);
octoOff(pin);
}
void relayPulse(int pin, uint32_t ms) {
relayOn(pin);
wdtSafeDelay(ms);
relayOff(pin);
}
// ==================================================================
// OCTOCOUPLER ACTIONS
// ==================================================================
void pressIgnitionON() {
Serial.printf("[%lu ms] [OCTO] ⚡ Ignition ON (2 quick presses)...\n", millis());
octoPulse(OCTO_TRIGGER, 200);
wdtSafeDelay(150);
octoPulse(OCTO_TRIGGER, 200);
octoOff(OCTO_TRIGGER);
}
void pressIgnitionOFF() {
Serial.printf("[%lu ms] [OCTO] 🔓 Ignition OFF...\n", millis());
octoPulse(OCTO_UNLOCK, 300);
octoOff(OCTO_UNLOCK);
}
void pressMotorLock() {
Serial.printf("[%lu ms] [OCTO] 🔒 Motor Lock...\n", millis());
octoPulse(OCTO_LOCK, 300);
octoOff(OCTO_LOCK);
}
// ==================================================================
// PATTERNS
// ==================================================================
void hornBeep(uint32_t ms) {
relayOn(RELAY_HORN);
wdtSafeDelay(ms);
relayOff(RELAY_HORN);
}
void indicatorBlink(uint32_t onMs, uint32_t offMs) {
relayOn(RELAY_IND);
wdtSafeDelay(onMs);
relayOff(RELAY_IND);
if (offMs > 0) wdtSafeDelay(offMs);
}
void welcomePattern() {
Serial.printf("[%lu ms] [PATTERN] ▶ Welcome START\n", millis());
hornBeep(200);
wdtSafeDelay(2000);
hornBeep(150); wdtSafeDelay(150);
hornBeep(150);
wdtSafeDelay(2000);
indicatorBlink(1000, 0);
wdtSafeDelay(2000);
indicatorBlink(300, 500);
indicatorBlink(300, 0);
relayOff(RELAY_HORN);
relayOff(RELAY_IND);
Serial.printf("[%lu ms] [PATTERN] ■ Welcome DONE\n", millis());
}
void goodbyePattern() {
Serial.printf("[%lu ms] [PATTERN] ▶ Goodbye START\n", millis());
hornBeep(1000);
wdtSafeDelay(2000);
hornBeep(200); wdtSafeDelay(300);
hornBeep(800);
wdtSafeDelay(2000);
indicatorBlink(1500, 0);
wdtSafeDelay(2000);
indicatorBlink(200, 300);
indicatorBlink(200, 0);
relayOff(RELAY_HORN);
relayOff(RELAY_IND);
Serial.printf("[%lu ms] [PATTERN] ■ Goodbye DONE\n", millis());
}
void bootPattern() {
Serial.printf("[%lu ms] [PATTERN] ▶ Boot START\n", millis());
hornBeep(200);
indicatorBlink(300, 0);
relayOff(RELAY_HORN);
relayOff(RELAY_IND);
Serial.printf("[%lu ms] [PATTERN] ■ Boot DONE\n", millis());
}
// ==================================================================
// BOOT SPACE OPENER
// ==================================================================
void openBootSpace() {
Serial.printf("[%lu ms] [BOOT] Opening trunk (Relay 1)...\n", millis());
relayPulse(RELAY_TRUNK, 800);
relayOff(RELAY_TRUNK);
bootPattern();
Serial.printf("[%lu ms] [BOOT] Trunk operation complete\n", millis());
}
// ==================================================================
// DISCONNECT SEQUENCE (guarded)
// ==================================================================
void handleDisconnectSequence() {
if (!hadActiveConnection) {
Serial.printf("[%lu ms] [DISCONNECT] ⛔ SKIPPED\n", millis());
return;
}
Serial.printf("[%lu ms] [DISCONNECT] ▶ Sequence START\n", millis());
goodbyePattern();
pressIgnitionOFF();
wdtSafeDelay(1000);
pressMotorLock();
hadActiveConnection = false;
octoOff(OCTO_UNLOCK);
octoOff(OCTO_LOCK);
octoOff(OCTO_TRIGGER);
Serial.printf("[%lu ms] [DISCONNECT] ■ Sequence COMPLETE\n", millis());
}
// ==================================================================
// TAG NOTIFICATION CALLBACK
// ==================================================================
void tagNotifyCallback(BLERemoteCharacteristic* pChar,
uint8_t* pData, size_t length, bool isNotify) {
Serial.printf("[%lu ms] [TAG] ◆ Notify len=%d data=", millis(), length);
for (size_t i = 0; i < length; i++) Serial.printf("0x%02X ", pData[i]);
Serial.println();
if (length >= 1 && pData[0] == 0x01) {
EventType evt = EVT_TAG_BUTTON;
xQueueSend(eventQueue, &evt, 0);
}
}
// ==================================================================
// SCAN CALLBACK
// ==================================================================
class TagScanCallbacks : public BLEAdvertisedDeviceCallbacks {
void onResult(BLEAdvertisedDevice device) override {
String mac = device.getAddress().toString().c_str();
mac.toLowerCase();
if (mac == TAG_MAC) {
tagFound = true;
if (bleScan) bleScan->stop();
}
}
};
// ==================================================================
// PHONE SERVER CALLBACKS
// ==================================================================
class PhoneServerCallbacks : public BLEServerCallbacks {
void onConnect(BLEServer* pServer) override {
if (tagConnected) {
Serial.printf("[%lu ms] [PHONE] ⛔ Rejected (tag active)\n", millis());
pServer->disconnect(pServer->getConnId());
return;
}
uint32_t now = millis();
Serial.printf("[%lu ms] [PHONE] ✔ Connected\n", now);
lastPhoneEventMs = now;
EventType evt = EVT_PHONE_CONNECTED;
xQueueSend(eventQueue, &evt, 0);
}
void onDisconnect(BLEServer* pServer) override {
uint32_t now = millis();
Serial.printf("[%lu ms] [PHONE] ✘ Disconnected\n", now);
lastPhoneEventMs = now;
EventType evt = EVT_PHONE_DISCONNECTED;
xQueueSend(eventQueue, &evt, 0);
delay(50);
if (systemReady) {
pServer->startAdvertising();
}
}
};
class PhoneCharCallbacks : public BLECharacteristicCallbacks {
void onWrite(BLECharacteristic* pChar) override {
String cmd = String(pChar->getValue().c_str());
cmd.trim();
Serial.printf("[%lu ms] [PHONE] Received: '%s'\n", millis(), cmd.c_str());
EventType evt = EVT_NONE;
if (cmd.equalsIgnoreCase("open boot")) evt = EVT_PHONE_CMD_OPEN_BOOT;
else if (cmd.equalsIgnoreCase("unlock")) evt = EVT_PHONE_CMD_UNLOCK;
else if (cmd.equalsIgnoreCase("lock")) evt = EVT_PHONE_CMD_LOCK;
if (evt != EVT_NONE) xQueueSend(eventQueue, &evt, 0);
}
};
// ==================================================================
// CLIENT RESET
// ==================================================================
void resetTagClient() {
Serial.printf("[%lu ms] [MEM] Resetting tag client...\n", millis());
if (tagClient != nullptr) {
if (tagClient->isConnected()) {
tagClient->disconnect();
}
delete tagClient;
tagClient = nullptr;
}
vTaskDelay(pdMS_TO_TICKS(DISCONNECT_CLEANUP_MS));
tagClient = BLEDevice::createClient();
tagChar = nullptr;
Serial.printf("[%lu ms] [MEM] Fresh client created\n", millis());
}
// ==================================================================
// QUICK SCAN
// ==================================================================
bool quickScanForTag() {
tagFound = false;
if (bleScan == nullptr) {
bleScan = BLEDevice::getScan();
bleScan->setAdvertisedDeviceCallbacks(new TagScanCallbacks(), false);
bleScan->setActiveScan(true);
bleScan->setInterval(80);
bleScan->setWindow(79);
}
bleScan->start(1, false);
bleScan->stop();
bleScan->clearResults();
return tagFound;
}
// ==================================================================
// TRY CONNECT TAG
// ==================================================================
bool tryConnectTag() {
uint32_t startMs = millis();
if (tagClient->connect(BLEAddress(TAG_MAC.c_str()), DIRECT_CONNECT_MS)) {
return true;
}
if (quickScanForTag()) {
if (tagClient->connect(BLEAddress(TAG_MAC.c_str()), DIRECT_CONNECT_MS)) {
return true;
}
}
return false;
}
// ==================================================================
// TAG NOTIFICATIONS SETUP
// ==================================================================
bool setupTagNotifications() {
BLERemoteService* svc = tagClient->getService(TAG_SERVICE_UUID);
if (!svc) return false;
tagChar = svc->getCharacteristic(TAG_CHAR_UUID);
if (!tagChar) return false;
if (tagChar->canNotify()) {
tagChar->registerForNotify(tagNotifyCallback);
vTaskDelay(pdMS_TO_TICKS(150));
BLERemoteDescriptor* cccd = tagChar->getDescriptor(
BLEUUID((uint16_t)CCCD_UUID));
if (cccd) {
uint8_t notifyOn[] = {0x01, 0x00};
for (int r = 0; r < 3; r++) {
cccd->writeValue(notifyOn, 2, true);
vTaskDelay(pdMS_TO_TICKS(80));
}
}
}
if (tagChar->canWrite()) {
uint8_t initVal = 0x01;
tagChar->writeValue(&initVal, 1, true);
vTaskDelay(pdMS_TO_TICKS(50));
}
return true;
}
// ==================================================================
// CORE 0: BLE CONNECTION TASK
// ==================================================================
void bleConnectionTask(void *param) {
Serial.println("\n========== BLE INIT (Core 0) ==========");
esp_bt_controller_mem_release(ESP_BT_MODE_CLASSIC_BT);
BLEDevice::init("ESP32_Control");
BLEDevice::setPower(ESP_PWR_LVL_P9);
Serial.println("[BLE] Name: ESP32_Control TX: +9 dBm");
phoneServer = BLEDevice::createServer();
phoneServer->setCallbacks(new PhoneServerCallbacks());
BLEService* phoneSvc = phoneServer->createService(PHONE_SERVICE_UUID);
phoneChar = phoneSvc->createCharacteristic(
PHONE_CHAR_UUID,
BLECharacteristic::PROPERTY_WRITE |
BLECharacteristic::PROPERTY_WRITE_NR |
BLECharacteristic::PROPERTY_READ |
BLECharacteristic::PROPERTY_NOTIFY);
phoneChar->addDescriptor(new BLE2902());
phoneChar->setCallbacks(new PhoneCharCallbacks());
phoneSvc->start();
BLEAdvertising* adv = phoneServer->getAdvertising();
adv->addServiceUUID(PHONE_SERVICE_UUID);
#if CORE_V3
adv->setScanResponse(true);
#endif
adv->start();
resetTagClient();
systemReady = true;
Serial.println("\n[IDLE] Waiting for Tag or Phone...\n");
while (true) {
if (ESP.getFreeHeap() < HEAP_CRITICAL) {
Serial.println("[CRIT] Critical heap! Restarting...");
forceAllOutputsSafe();
delay(100);
ESP.restart();
}
// ---- TAG CONNECT ----
if (!tagConnected && !phoneConnected) {
if (tryConnectTag()) {
uint32_t connectTime = millis();
setupTagNotifications();
tagConnected = true;
lastTagEventMs = connectTime;
Serial.printf("[%lu ms] [TAG] ✔ CONNECTED\n", connectTime);
EventType evt = EVT_TAG_CONNECTED;
xQueueSend(eventQueue, &evt, 0);
}
}
// ---- TAG DISCONNECT ----
if (tagConnected && tagClient && !tagClient->isConnected()) {
uint32_t dcTime = millis();
tagConnected = false;
tagChar = nullptr;
lastTagEventMs = dcTime;
Serial.printf("[%lu ms] [TAG] ✘ DISCONNECTED\n", dcTime);
EventType evt = EVT_TAG_DISCONNECTED;
xQueueSend(eventQueue, &evt, 0);
resetTagClient();
}
// ---- RSSI FILTER ----
if (tagConnected && tagClient && tagClient->isConnected()) {
if (isRssiFilterEnabled()) {
int rssi = tagClient->getRssi();
if (rssi < RSSI_WEAK_THRESHOLD) {
Serial.printf("[%lu ms] [TAG] Weak RSSI %d → disconnect\n",
millis(), rssi);
tagClient->disconnect();
}
}
}
vTaskDelay(pdMS_TO_TICKS(FAST_LOOP_DELAY_MS));
}
}
// ==================================================================
// CORE 1: EVENT TASK
// ==================================================================
void eventTask(void *param) {
esp_task_wdt_add(NULL);
esp_task_wdt_reset();
EventType evt;
while (true) {
esp_task_wdt_reset();
if (xQueueReceive(eventQueue, &evt, pdMS_TO_TICKS(50)) == pdTRUE) {
if (patternBusy) {
xQueueSend(eventQueue, &evt, pdMS_TO_TICKS(10));
vTaskDelay(pdMS_TO_TICKS(100));
continue;
}
patternBusy = true;
uint32_t tNow = millis();
switch (evt) {
case EVT_TAG_CONNECTED:
Serial.printf("[%lu ms] [ACTION] ▶ TAG connect\n", tNow);
if (tagConnected && !phoneConnected) {
digitalWrite(ONBOARD_LED, HIGH);
if (!hadActiveConnection) {
Serial.printf("[%lu ms] [ACTION] → First connect: welcome\n", millis());
pressIgnitionON();
welcomePattern();
hadActiveConnection = true;
} else {
Serial.printf("[%lu ms] [ACTION] → Silent reconnect\n", millis());
}
Serial.printf("[%lu ms] [ACTION] ■ TAG connect complete\n", millis());
}
break;
case EVT_PHONE_CONNECTED:
Serial.printf("[%lu ms] [ACTION] ▶ PHONE connect\n", tNow);
if (!tagConnected) {
phoneConnected = true;
digitalWrite(ONBOARD_LED, HIGH);
if (!hadActiveConnection) {
Serial.printf("[%lu ms] [ACTION] → First connect: welcome\n", millis());
pressIgnitionON();
welcomePattern();
hadActiveConnection = true;
} else {
Serial.printf("[%lu ms] [ACTION] → Silent reconnect\n", millis());
}
Serial.printf("[%lu ms] [ACTION] ■ PHONE connect complete\n", millis());
} else {
phoneConnected = false;
Serial.printf("[%lu ms] [ACTION] ⛔ Phone refused\n", millis());
}
break;
case EVT_TAG_DISCONNECTED:
Serial.printf("[%lu ms] [ACTION] ▶ TAG disconnect\n", tNow);
digitalWrite(ONBOARD_LED, LOW);
if (isSafeMode()) {
Serial.printf("[%lu ms] [ACTION] → Switch ON: skip action\n", millis());
} else {
Serial.printf("[%lu ms] [ACTION] → Switch OFF: run action\n", millis());
handleDisconnectSequence();
}
Serial.printf("[%lu ms] [ACTION] ■ TAG disconnect complete\n", millis());
break;
case EVT_PHONE_DISCONNLoading
esp32-devkit-c-v4
esp32-devkit-c-v4