/* * ESP32-C3 BLE Server * Simple BLE server that can receive and send data * Controls a USB Humidifier Module via BLE * Compatible with Seeed Studio XIAO ESP32-C3 */ #include #include #include #include // BLE Server name #define SERVICE_UUID "4fafc201-1fb5-459e-8fcc-c5c9c331914b" #define CHARACTERISTIC_UUID "beb5483e-36e1-4688-b7f5-ea07361b26a8" // Humidifier control pin (D0 on XIAO ESP32-C3 = GPIO2) #define HUMIDIFIER_PIN 2 // Invert logic: Set to true if your hardware requires inverted logic // (ON = HIGH, OFF = LOW instead of ON = LOW, OFF = HIGH) // Set this to true for the device with MAC address ending in ...B45E9569864F #define INVERT_LOGIC false BLEServer* pServer = NULL; BLECharacteristic* pCharacteristic = NULL; bool deviceConnected = false; bool oldDeviceConnected = false; bool humidifierState = false; String lastReceivedValue = ""; String lastNotificationValue = ""; // Track what we sent for notifications class MyServerCallbacks: public BLEServerCallbacks { void onConnect(BLEServer* pServer) { deviceConnected = true; Serial.println("Device connected"); }; void onDisconnect(BLEServer* pServer) { deviceConnected = false; Serial.println("Device disconnected"); } }; // Function to process received commands void processReceivedCommand(String rxValue) { Serial.println("========================================"); Serial.println(">>> Processing received command <<<"); Serial.print("Received Value: "); Serial.println(rxValue); Serial.println("========================================"); // Control the light based on received message // Clean up the string - remove whitespace and convert to uppercase rxValue.toUpperCase(); rxValue.trim(); // Remove any non-printable characters (like newlines, carriage returns) String cleanValue = ""; for (int i = 0; i < rxValue.length(); i++) { char c = rxValue.charAt(i); if (c >= 32 && c <= 126) { // Only printable ASCII characters cleanValue += c; } } Serial.print("Raw received: '"); Serial.print(rxValue); Serial.print("' (length: "); Serial.print(rxValue.length()); Serial.println(")"); Serial.print("Cleaned value: '"); Serial.print(cleanValue); Serial.print("' (length: "); Serial.print(cleanValue.length()); Serial.println(")"); // Determine pin states based on invert logic setting int onPinState = INVERT_LOGIC ? HIGH : LOW; int offPinState = INVERT_LOGIC ? LOW : HIGH; // Check for OFF commands if (cleanValue == "OFF" || cleanValue == "1" || cleanValue.indexOf("OFF") >= 0) { digitalWrite(HUMIDIFIER_PIN, offPinState); humidifierState = false; Serial.print("💨 Humidifier turned OFF (pin "); Serial.print(offPinState == HIGH ? "HIGH" : "LOW"); Serial.println(")"); } // Check for ON commands else if (cleanValue == "ON" || cleanValue == "0" || cleanValue.indexOf("ON") >= 0) { digitalWrite(HUMIDIFIER_PIN, onPinState); humidifierState = true; Serial.print("💨 Humidifier turned ON (pin "); Serial.print(onPinState == HIGH ? "HIGH" : "LOW"); Serial.println(")"); } // Any other message toggles the humidifier else { humidifierState = !humidifierState; int togglePinState = humidifierState ? onPinState : offPinState; digitalWrite(HUMIDIFIER_PIN, togglePinState); Serial.print("💨 Humidifier toggled to: "); Serial.print(humidifierState ? "ON" : "OFF"); Serial.print(" (pin "); Serial.print(togglePinState == HIGH ? "HIGH" : "LOW"); Serial.println(")"); } // Verify pin state Serial.print("Pin state readback: "); Serial.println(digitalRead(HUMIDIFIER_PIN) ? "HIGH" : "LOW"); } class MyCallbacks: public BLECharacteristicCallbacks { void onWrite(BLECharacteristic *pCharacteristic) { Serial.println("========================================"); Serial.println(">>> onWrite callback triggered! <<<"); String rxValue = pCharacteristic->getValue(); Serial.print("Received data length: "); Serial.println(rxValue.length()); lastReceivedValue = rxValue; processReceivedCommand(rxValue); // This will be handled by processReceivedCommand } }; void setup() { Serial.begin(115200); Serial.println("Starting BLE Server..."); // Setup humidifier control pin // Note: Humidifier module has its own USB power supply // GPIO pin acts as a control signal (3.3V HIGH/LOW) pinMode(HUMIDIFIER_PIN, OUTPUT); // Set initial state to OFF (inverted logic: OFF = LOW, normal logic: OFF = HIGH) int offPinState = INVERT_LOGIC ? LOW : HIGH; digitalWrite(HUMIDIFIER_PIN, offPinState); humidifierState = false; Serial.println("Humidifier control initialized on pin D0 (GPIO2)"); Serial.println("Module uses its own USB power - GPIO provides control signal only"); if (INVERT_LOGIC) { Serial.println("⚠️ INVERTED LOGIC ENABLED (ON=HIGH, OFF=LOW)"); } // Test humidifier on startup to verify pin works Serial.println("Testing humidifier pin - turning on for 1 second..."); int onPinState = INVERT_LOGIC ? HIGH : LOW; digitalWrite(HUMIDIFIER_PIN, onPinState); delay(1000); digitalWrite(HUMIDIFIER_PIN, offPinState); Serial.println("Startup test complete. If you saw/heard the humidifier activate, wiring is correct!"); // Create the BLE Device // You can change the device name here to identify different devices // For example: "ESP32-C3-BLE-1", "ESP32-C3-BLE-2", "Diffuser-Left", "Diffuser-Right", etc. BLEDevice::init("ESP32-C3-BLE"); // Create the BLE Server pServer = BLEDevice::createServer(); pServer->setCallbacks(new MyServerCallbacks()); // Create the BLE Service BLEService *pService = pServer->createService(SERVICE_UUID); // Create a BLE Characteristic pCharacteristic = pService->createCharacteristic( CHARACTERISTIC_UUID, BLECharacteristic::PROPERTY_READ | BLECharacteristic::PROPERTY_WRITE | BLECharacteristic::PROPERTY_WRITE_NR | BLECharacteristic::PROPERTY_NOTIFY ); pCharacteristic->setCallbacks(new MyCallbacks()); Serial.println("BLE Characteristic callbacks registered"); // Add a descriptor for notifications pCharacteristic->addDescriptor(new BLE2902()); // Start the service pService->start(); // Start advertising BLEAdvertising *pAdvertising = BLEDevice::getAdvertising(); pAdvertising->addServiceUUID(SERVICE_UUID); pAdvertising->setScanResponse(false); pAdvertising->setMinPreferred(0x0); // set value to 0x00 to not advertise this parameter BLEDevice::startAdvertising(); Serial.println("Waiting for a client connection to notify..."); Serial.println("BLE Server is ready!"); } void loop() { // Handle disconnection if (!deviceConnected && oldDeviceConnected) { delay(500); // give the bluetooth stack the chance to get things ready pServer->startAdvertising(); // restart advertising Serial.println("Start advertising"); oldDeviceConnected = deviceConnected; } // Handle connection if (deviceConnected && !oldDeviceConnected) { oldDeviceConnected = deviceConnected; } // Check if characteristic value has changed (polling method as backup) // Only process if it's different from what we sent for notifications if (deviceConnected && pCharacteristic) { String currentValue = pCharacteristic->getValue(); if (currentValue != lastReceivedValue && currentValue != lastNotificationValue && currentValue.length() > 0) { Serial.println(">>> Value changed detected via polling! <<<"); lastReceivedValue = currentValue; // Process the received value processReceivedCommand(currentValue); } } // Send a notification every 2 seconds when connected if (deviceConnected) { static unsigned long lastTime = 0; unsigned long currentTime = millis(); if (currentTime - lastTime >= 2000) { String message = "Hello from ESP32-C3! Time: " + String(millis() / 1000) + "s"; lastNotificationValue = message; // Track this so we don't process it as a command pCharacteristic->setValue(message.c_str()); pCharacteristic->notify(); Serial.println("Sent notification: " + message); lastTime = currentTime; } } delay(10); }