#include #include #include #include #include #define LED_PIN 10 #define NUM_LEDS 236 #define LED_TYPE WS2812B #define COLOR_ORDER GRB #define HIGH_PIN 7 constexpr uint8_t MAX_VALUE = 200; constexpr unsigned long LED_UPDATE_INTERVAL_MS = 500; constexpr uint8_t RELAY_ON_STATE = HIGH; constexpr uint8_t RELAY_OFF_STATE = LOW; static BLEUUID SERVICE_UUID("7e08f6a0-3f3f-4b55-a0a2-8c30f6f19a10"); static BLEUUID CHARACTERISTIC_UUID("4b5f3e26-87d1-48de-9f14-6e0f9e4c7811"); CRGB leds[NUM_LEDS]; volatile uint8_t currentR = 0; volatile uint8_t currentG = 0; volatile uint8_t currentB = 0; volatile bool newBleValues = false; unsigned long lastLedUpdate = 0; class LampServerCallbacks : public BLEServerCallbacks { void onConnect(BLEServer *server) override { Serial.println("BLE device connected"); } void onDisconnect(BLEServer *server) override { Serial.println("BLE device disconnected"); BLEDevice::startAdvertising(); } }; bool parseIntegerField(const String &payload, const char *key, int &value) { const String field = String(key) + "="; const int start = payload.indexOf(field); if (start < 0) { return false; } const int valueStart = start + field.length(); int valueEnd = payload.indexOf(';', valueStart); if (valueEnd < 0) { valueEnd = payload.length(); } String text = payload.substring(valueStart, valueEnd); text.trim(); if (text.length() == 0) { return false; } uint8_t index = 0; if (text.charAt(0) == '-') { if (text.length() == 1) { return false; } index = 1; } for (; index < text.length(); index++) { if (!isDigit(text.charAt(index))) { return false; } } value = text.toInt(); return true; } bool parseFloatField(const String &payload, const char *key, float &value) { const String field = String(key) + "="; const int start = payload.indexOf(field); if (start < 0) { return false; } const int valueStart = start + field.length(); int valueEnd = payload.indexOf(';', valueStart); if (valueEnd < 0) { valueEnd = payload.length(); } String text = payload.substring(valueStart, valueEnd); text.trim(); if (text.length() == 0) { return false; } value = text.toFloat(); return true; } uint8_t sanitizeChannel(bool parsed, int value) { if (!parsed) { return 0; } if (value < 0) { return 0; } if (value > MAX_VALUE) { return 0; } return uint8_t(value); } uint8_t capColorValue(uint8_t value) { return value > MAX_VALUE ? MAX_VALUE : value; } void setAll(uint8_t r, uint8_t g, uint8_t b) { r = capColorValue(r); g = capColorValue(g); b = capColorValue(b); fill_solid(leds, NUM_LEDS, CRGB(r, g, b)); } void updateRelay(uint8_t r, uint8_t g, uint8_t b) { const bool allChannelsOff = (r == 0 && g == 0 && b == 0); digitalWrite(HIGH_PIN, allChannelsOff ? RELAY_OFF_STATE : RELAY_ON_STATE); Serial.print("Relay: "); Serial.println(allChannelsOff ? "OFF" : "ON"); } void applyLedColor() { const uint8_t r = currentR; const uint8_t g = currentG; const uint8_t b = currentB; setAll(r, g, b); FastLED.show(); updateRelay(r, g, b); Serial.print("LED RGB: R="); Serial.print(r); Serial.print(" G="); Serial.print(g); Serial.print(" B="); Serial.print(b); if (newBleValues) { Serial.print(" updated-from-BLE"); newBleValues = false; } Serial.println(); } class ColorWriteCallbacks : public BLECharacteristicCallbacks { void onWrite(BLECharacteristic *characteristic) override { auto rawValue = characteristic->getValue(); String payload = String(rawValue.c_str()); payload.trim(); int parsedR = -1; int parsedG = -1; int parsedB = -1; float batteryVoltage = 0.0f; const bool hasR = parseIntegerField(payload, "R", parsedR); const bool hasG = parseIntegerField(payload, "G", parsedG); const bool hasB = parseIntegerField(payload, "B", parsedB); const bool hasBattery = parseFloatField(payload, "BAT", batteryVoltage); const uint8_t safeR = sanitizeChannel(hasR, parsedR); const uint8_t safeG = sanitizeChannel(hasG, parsedG); const uint8_t safeB = sanitizeChannel(hasB, parsedB); currentR = safeR; currentG = safeG; currentB = safeB; newBleValues = true; Serial.print("BLE received: "); Serial.println(payload); Serial.print("Parsed RGB: R="); Serial.print(parsedR); Serial.print(" G="); Serial.print(parsedG); Serial.print(" B="); Serial.print(parsedB); Serial.print(" -> safe RGB: R="); Serial.print(safeR); Serial.print(" G="); Serial.print(safeG); Serial.print(" B="); Serial.print(safeB); if (hasBattery) { Serial.print(" BAT="); Serial.print(batteryVoltage, 2); Serial.print("V"); } Serial.println(); } }; void setupBle() { BLEDevice::init("FabAcademy Lamp Receiver V3"); BLEDevice::setPower(ESP_PWR_LVL_P9); BLEServer *server = BLEDevice::createServer(); server->setCallbacks(new LampServerCallbacks()); BLEService *service = server->createService(SERVICE_UUID); BLECharacteristic *colorCharacteristic = service->createCharacteristic( CHARACTERISTIC_UUID, BLECharacteristic::PROPERTY_WRITE ); colorCharacteristic->setCallbacks(new ColorWriteCallbacks()); service->start(); BLEAdvertising *advertising = BLEDevice::getAdvertising(); advertising->addServiceUUID(SERVICE_UUID); advertising->setScanResponse(true); advertising->setMinPreferred(0x06); advertising->setMinPreferred(0x12); BLEDevice::startAdvertising(); Serial.println("BLE server started and advertising"); } void setup() { Serial.begin(115200); delay(300); Serial.println(); Serial.println("FabAcademy V3 BLE lamp receiver starting"); pinMode(HIGH_PIN, OUTPUT); digitalWrite(HIGH_PIN, RELAY_OFF_STATE); FastLED.addLeds(leds, NUM_LEDS); FastLED.setBrightness(MAX_VALUE); FastLED.clear(true); setupBle(); applyLedColor(); } void loop() { if (millis() - lastLedUpdate >= LED_UPDATE_INTERVAL_MS) { lastLedUpdate = millis(); applyLedColor(); } }