diff --git a/.gitignore b/.gitignore index 4fbc9ac..30a5810 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ bin/* *.bin .pio +*.swp diff --git a/lib/Figments/Input.h b/lib/Figments/Input.h index deec40d..0036ef3 100644 --- a/lib/Figments/Input.h +++ b/lib/Figments/Input.h @@ -163,7 +163,7 @@ class OnlineTaskMixin : public virtual Loopable { } } - virtual void handleEventOnline(const InputEvent &evt) = 0; + virtual void handleEventOnline(const InputEvent &evt) {} void loop() override { if (m_online) { @@ -171,7 +171,7 @@ class OnlineTaskMixin : public virtual Loopable { } } - virtual void loopOnline() = 0; + virtual void loopOnline() {} private: bool m_online = false; diff --git a/lib/Figments/Ringbuf.h b/lib/Figments/Ringbuf.h index 5807ca5..023be93 100644 --- a/lib/Figments/Ringbuf.h +++ b/lib/Figments/Ringbuf.h @@ -31,6 +31,16 @@ struct Ringbuf { } m_items[cur] = src; } + + size_t write(T(&dest)[Size]) { + int i = 0; + size_t ret = 0; + while(take(dest[i])) { + i++; + ret += sizeof(T); + } + return ret; + } private: int m_head = 0; int m_tail = 0; diff --git a/platformio.ini b/platformio.ini index 1fc65a7..0ce140f 100644 --- a/platformio.ini +++ b/platformio.ini @@ -8,17 +8,37 @@ ; Please visit documentation for the other options and examples ; https://docs.platformio.org/page/projectconf.html +[common_env_data] +src_filter = "+<*> -<.git/> -<.svn/> -" + [env:featheresp32] platform = espressif32 board = featheresp32 framework = arduino +build_flags = + -D PLATFORM_ARDUINO + -D BOARD_ESP32 lib_deps = fastled/FastLED@^3.4.0 thijse/ArduinoLog@^1.0.3 knolleary/PubSubClient@^2.8.0 bblanchon/ArduinoJson@^6.17.3 - ricaun/ArduinoUniqueID@^1.1.0 sstaub/NTP@^1.4.0 -src_filter = "+<*> -<.git/> -<.svn/> - +" -; upload_protocol = espota -; upload_port = 10.0.0.171 + arduino-libraries/NTPClient@^3.1.0 +src_filter = "${common_env_data.src_filter} +" + +[env:huzzah] +platform = espressif8266 +board = huzzah +framework = arduino +build_flags = + -D PLATFORM_ARDUINO + -D BOARD_ESP8266 +lib_deps = + fastled/FastLED@^3.4.0 + thijse/ArduinoLog@^1.0.3 + knolleary/PubSubClient@^2.8.0 + bblanchon/ArduinoJson@^6.17.3 + sstaub/NTP@^1.4.0 + arduino-libraries/NTPClient@^3.1.0 +src_filter = "${common_env_data.src_filter} +" diff --git a/src/Config.cpp b/src/Config.cpp index f924afd..4e4a24e 100644 --- a/src/Config.cpp +++ b/src/Config.cpp @@ -1,15 +1,14 @@ #include "./Config.h" #include "./Static.h" #include +#include constexpr uint16_t HardwareConfig::MAX_LED_NUM; HardwareConfig HardwareConfig::load() { HardwareConfig ret; -#ifdef PLATFORM_PHOTON EEPROM.get(0, ret); -#endif return ret; } @@ -17,9 +16,7 @@ void HardwareConfig::save() { HardwareConfig dataCopy{*this}; dataCopy.checksum = getCRC(); -#ifdef PLATFORM_PHOTON EEPROM.put(0, dataCopy); -#endif } LinearCoordinateMapping @@ -103,6 +100,8 @@ ConfigService::handleEvent(const InputEvent &evt) //Log.info("Saving configuration"); m_config.save(); break; + default: + break; } } diff --git a/src/Config.h b/src/Config.h index 1f4b021..a0eda84 100644 --- a/src/Config.h +++ b/src/Config.h @@ -2,7 +2,8 @@ #include //#define PLATFORM_PHOTON -#define PLATFORM_ARDUINO +//#define PLATFORM_ARDUINO +#define RENDERBUG_VERSION 2 struct HardwareConfig { uint8_t version = 2; @@ -29,7 +30,11 @@ struct HardwareConfig { LinearCoordinateMapping toCoordMap() const; static constexpr uint16_t MAX_LED_NUM = 255; +#ifdef PLATFORM_PHOTON + static constexpr bool HAS_MPU_6050 = true; +#else static constexpr bool HAS_MPU_6050 = false; +#endif private: uint8_t getCRC() const; diff --git a/src/LogService.cpp b/src/LogService.cpp new file mode 100644 index 0000000..3d76568 --- /dev/null +++ b/src/LogService.cpp @@ -0,0 +1,100 @@ +#include "LogService.h" +#include "Static.h" +#include + +void +LogService::handleEvent(const InputEvent& evt) { + if (evt.intent != InputEvent::None) { + const char* sourceName; + switch(evt.intent) { + case InputEvent::PowerToggle: + sourceName = "power-toggle"; + break; + case InputEvent::SetPower: + sourceName = "set-power"; + break; + case InputEvent::PreviousPattern: + sourceName = "previous-pattern"; + break; + case InputEvent::NextPattern: + sourceName = "next-pattern"; + break; + case InputEvent::SetPattern: + sourceName = "set-pattern"; + break; + case InputEvent::SetColor: + sourceName = "set-color"; + break; + case InputEvent::Acceleration: + sourceName = "acceleration"; + break; + case InputEvent::UserInput: + sourceName = "user"; + break; + case InputEvent::SetBrightness: + sourceName = "set-brightness"; + break; + case InputEvent::FirmwareUpdate: + sourceName = "firmware-update"; + break; + case InputEvent::NetworkStatus: + sourceName = "network-status"; + break; + case InputEvent::NetworkActivity: + sourceName = "network-activity"; + break; + case InputEvent::StartThing: + sourceName = "start-thing"; + break; + case InputEvent::StopThing: + sourceName = "stop-thing"; + break; + case InputEvent::SetDisplayOffset: + sourceName = "set-display-offset"; + break; + case InputEvent::SetDisplayLength: + sourceName = "set-display-length"; + break; + case InputEvent::SaveConfigurationRequest: + sourceName = "save-configuration"; + break; + default: + sourceName = 0; + break; + } + char valueBuf[255]; + switch(evt.type) { + case InputEvent::Null: + snprintf(valueBuf, sizeof(valueBuf), "null");break; + case InputEvent::Integer: + snprintf(valueBuf, sizeof(valueBuf), "%d %02x", evt.asInt(), evt.asInt());break; + case InputEvent::String: + snprintf(valueBuf, sizeof(valueBuf), "\"%s\"", evt.asString());break; + case InputEvent::Color: + snprintf(valueBuf, sizeof(valueBuf), "[%d, %d, %d]", evt.asRGB().r, evt.asRGB().g, evt.asRGB().b);break; + } + char buf[255 * 2]; + if (sourceName == 0) { + snprintf(buf, sizeof(buf), "{\"intent\": %d, \"value\": %s}", evt.intent, valueBuf); + } else { + snprintf(buf, sizeof(buf), "{\"intent\": \"%s\", \"value\": %s}", sourceName, valueBuf); + } + if (evt.intent != m_lastEvent.intent) { + if (m_duplicateEvents > 0) { + Log.notice("Suppressed reporting %u duplicate events.", m_duplicateEvents); + } + Log.verbose("Event: %s", buf); + m_duplicateEvents = 0; + m_lastEvent = evt; + //Particle.publish("renderbug/event", buf, PRIVATE); + } else { + m_duplicateEvents++; + } + /*if (m_online) { + } else { + Log.info("[offline] Event: %s", buf); + }*/ + } +} + +STATIC_ALLOC(LogService); diff --git a/src/LogService.h b/src/LogService.h new file mode 100644 index 0000000..32f3e49 --- /dev/null +++ b/src/LogService.h @@ -0,0 +1,12 @@ +#include + +class LogService : public Task { + public: + LogService() : Task("Logging") {} + void handleEvent(const InputEvent& event) override; + void loop() override {} + + private: + uint16_t m_duplicateEvents = 0; + InputEvent m_lastEvent; +}; diff --git a/src/Platform.cpp b/src/Platform.cpp new file mode 100644 index 0000000..3cc5ee8 --- /dev/null +++ b/src/Platform.cpp @@ -0,0 +1,139 @@ +#include "Platform.h" +#include +#include "Static.h" + +#ifdef BOARD_ESP32 +#include +#elif defined(BOARD_ESP8266) +#include +#include +#include +#include + +WiFiUDP wifiUdp; +NTPClient timeClient(wifiUdp, "pool.ntp.org", 3600 * -7); +#endif + +#ifdef PLATFORM_PHOTON +STARTUP(BootOptions::initPins()); +#else +#include "platform/arduino/MQTTTelemetry.h" +void printNewline(Print* logOutput) { + logOutput->print("\r\n"); +} +#endif + +int Platform::s_timezone = 0; + +const char* +Platform::name() +{ +#ifdef PLATFORM_PHOTON + return "Photon"; +#else + return "Unknown!"; +#endif +} + +const char* +Platform::version() +{ +#ifdef PLATFORM_PHOTON + return System.version().c_str(); +#else + return "Unknown!"; +#endif +} + +void +Platform::preSetup() +{ + Serial.begin(115200); +#ifdef PLATFORM_PHOTON + System.enableFeature(FEATURE_RETAINED_MEMORY); + if (bootopts.isFlash) { + System.dfu(); + } + if (bootopts.isSerial) { + bootopts.waitForRelease(); + while(!Serial.isConnected()) { + Particle.process(); + } + Log.notice("\xf0\x9f\x94\x8c Serial connected"); + } +#else + Log.begin(LOG_LEVEL_VERBOSE, Static::instance()->logPrinter()); + Log.setSuffix(printNewline); +#endif +} + +void +Platform::setup() +{ +#ifdef PLATFORM_PHOTON + Time.zone(Static::instance()->getTimezone()); +#elif defined(BOARD_ESP32) + constexpr int dst = 1; + configTime(s_timezone* 3600, 3600 * dst, "pool.ntp.org"); +#elif defined(BOARD_ESP8266) + timeClient.begin(); +#endif +} + +void +Platform::bootSplash() +{ +#ifdef PLATFORM_PHOTON + Log.notice(u8" Boot pin configuration:"); + Log.notice(u8" 2: Setup - %d", bootopts.isSetup); + Log.notice(u8" 3: Serial - %d", bootopts.isSerial); + Log.notice(u8" 4: Flash - %d", bootopts.isFlash); +#endif +} + +void +Platform::loop() +{ +#ifdef BOARD_ESP8266 + timeClient.update(); +#endif +} + +bool +Platform::getLocalTime(struct tm* timedata) +{ +#ifdef PLATFORM_PHOTON + if (Time.isValid()) { + timedata->tm_hour = Time.hour(); + timedata->tm_min = Time.minute(); + return true; + } + return false; +#elif defined(BOARD_ESP32) + return getLocalTime(timedata); +#else + timeClient.update(); + timedata->tm_hour = timeClient.getHours(); + timedata->tm_min = timeClient.getMinutes(); + return true; +#endif +} + +const char* +Platform::deviceID() +{ + uint64_t chipid; +#ifdef BOARD_ESP32 + chipid = ESP.getEfuseMac(); +#elif defined(BOARD_ESP8266) + chipid = ESP.getChipId(); +#endif + snprintf(s_deviceID, sizeof(s_deviceID), "%08X", (uint32_t)chipid); + return s_deviceID; +} + +BootOptions +Platform::bootopts; + +char +Platform::s_deviceID[15]; diff --git a/src/Platform.h b/src/Platform.h new file mode 100644 index 0000000..b8aba95 --- /dev/null +++ b/src/Platform.h @@ -0,0 +1,31 @@ +#pragma once +#include +#include "BootOptions.h" + +class Platform { + static int s_timezone; + static char s_deviceID[15]; + public: + static BootOptions bootopts; + static void setTimezone(int tz) { s_timezone = tz; } + static int getTimezone() { return s_timezone; } + + static void addLEDs(CRGB* leds, unsigned int ledCount) { +#ifdef PLATFORM_PHOTON + FastLED.addLeds(leds, ledCount); +#elif defined(BOARD_ESP32) + FastLED.addLeds(leds, ledCount); +#else + FastLED.addLeds(leds, ledCount); +#endif + } + + static const char* name(); + static const char* version(); + static void preSetup(); + static void bootSplash(); + static void setup(); + static void loop(); + static bool getLocalTime(struct tm* timedata); + static const char* deviceID(); +}; diff --git a/src/WiFiTask.cpp b/src/WiFiTask.cpp index 9c93661..01751d0 100644 --- a/src/WiFiTask.cpp +++ b/src/WiFiTask.cpp @@ -1,10 +1,16 @@ #include #include + +#ifdef BOARD_ESP8266 +#include +#endif +#ifdef BOARD_ESP32 #include +#endif #include "Static.h" #include "WiFiTask.h" -WiFiTask::WiFiTask() : InputSource("WiFi") {} +WiFiTask::WiFiTask() : InputSource("WiFi"), m_lastStatus(WL_IDLE_STATUS) {} void WiFiTask::onStart() diff --git a/src/WiFiTask.h b/src/WiFiTask.h index fc95d4e..d19262f 100644 --- a/src/WiFiTask.h +++ b/src/WiFiTask.h @@ -1,5 +1,4 @@ #include -#include #include "Static.h" class WiFiTask : public InputSource { @@ -8,5 +7,5 @@ class WiFiTask : public InputSource { void onStart() override; InputEvent read() override; private: - uint8_t m_lastStatus = WL_IDLE_STATUS; + uint8_t m_lastStatus; }; diff --git a/src/animations/Power.cpp b/src/animations/Power.cpp index 1451f0f..e0e09ad 100644 --- a/src/animations/Power.cpp +++ b/src/animations/Power.cpp @@ -16,6 +16,8 @@ public: break; case InputEvent::SetBrightness: m_brightness = evt.asInt(); + default: + return; } } diff --git a/src/firmware b/src/firmware new file mode 120000 index 0000000..cc2dd92 --- /dev/null +++ b/src/firmware @@ -0,0 +1 @@ +firmware \ No newline at end of file diff --git a/src/inputs/Buttons.cpp b/src/inputs/Buttons.cpp index a8b4ed5..5692f71 100644 --- a/src/inputs/Buttons.cpp +++ b/src/inputs/Buttons.cpp @@ -1,11 +1,13 @@ #include "./Buttons.h" +#include "../Static.h" void Buttons::onStart() { for(int i = 0; i < 3; i++) { //Log.info("Bound pin %d to button %d", 2 + i, i); - m_buttons[i].attach(2 + i, INPUT_PULLDOWN); + //m_buttons[i].attach(2 + i, INPUT_PULLDOWN); + m_buttons[i].attach(2 + i, INPUT_PULLUP); m_buttons[i].interval(15); } } @@ -35,3 +37,5 @@ Buttons::read() } return InputEvent{}; } + +STATIC_ALLOC(Buttons); diff --git a/src/inputs/MPU6050.cpp b/src/inputs/MPU6050.cpp index e69de29..488dd2c 100644 --- a/src/inputs/MPU6050.cpp +++ b/src/inputs/MPU6050.cpp @@ -0,0 +1,65 @@ +#include "MPU6050.h" +#include +#include "../Config.h" +#include "../Static.h" + +MPU5060::MPU5060() : InputSource("MPU5060") {} + +void +MPU5060::onStart() +{ + Wire.begin(); + + // Turn on the sensor + Wire.beginTransmission(I2C_ADDRESS); + Wire.write(PWR_MGMT_1); + Wire.write(0); + Wire.endTransmission(true); + + // Configure the filter + Wire.beginTransmission(I2C_ADDRESS); + Wire.write(CONFIG_REG); + Wire.write(3); + Wire.endTransmission(true); + + // Configure the accel range + Wire.beginTransmission(I2C_ADDRESS); + Wire.write(ACCEL_CONFIG_REG); + // 4G + Wire.write(2 << 3); + Wire.endTransmission(true); +} + +void +MPU5060::onStop() +{ + Wire.beginTransmission(I2C_ADDRESS); + // Turn off the sensor + Wire.write(PWR_MGMT_1); + Wire.write(1); + Wire.endTransmission(true); + //Wire.end(); +} + +InputEvent +MPU5060::read() +{ + EVERY_N_MILLISECONDS(5) { + Wire.beginTransmission(I2C_ADDRESS); + Wire.write(ACCEL_XOUT_HIGH); + Wire.endTransmission(false); + Wire.requestFrom(I2C_ADDRESS, 6); + const int16_t accelX = Wire.read() << 8 | Wire.read(); + const int16_t accelY = Wire.read() << 8 | Wire.read(); + const int16_t accelZ = Wire.read() << 8 | Wire.read(); + const uint16_t accelSum = abs(accelX) + abs(accelY) + abs(accelZ); + const uint16_t delta = abs(m_value.value() - accelSum); + m_value.add(accelSum); + if (delta > 32) { + return InputEvent{InputEvent::Acceleration, delta}; + } + } + return InputEvent{}; +} + +STATIC_ALLOC(MPU5060); diff --git a/src/inputs/MPU6050.h b/src/inputs/MPU6050.h index 107c5c3..7f8fbd3 100644 --- a/src/inputs/MPU6050.h +++ b/src/inputs/MPU6050.h @@ -1,4 +1,4 @@ -#include +#include class MPU5060: public InputSource { const int ACCEL_XOUT_HIGH = 0x3B; @@ -15,57 +15,11 @@ class MPU5060: public InputSource { const int ACCEL_CONFIG_REG = 0x1C; public: - MPU5060() : InputSource("MPU5060", HardwareConfig::HAS_MPU_6050 ? Task::Running : Task::Stopped) {} - void onStart() override { - Wire.begin(); + MPU5060(); - // Turn on the sensor - Wire.beginTransmission(I2C_ADDRESS); - Wire.write(PWR_MGMT_1); - Wire.write(0); - Wire.endTransmission(true); - - // Configure the filter - Wire.beginTransmission(I2C_ADDRESS); - Wire.write(CONFIG_REG); - Wire.write(3); - Wire.endTransmission(true); - - // Configure the accel range - Wire.beginTransmission(I2C_ADDRESS); - Wire.write(ACCEL_CONFIG_REG); - // 4G - Wire.write(2 << 3); - Wire.endTransmission(true); - } - - void onStop() override { - Wire.beginTransmission(I2C_ADDRESS); - // Turn off the sensor - Wire.write(PWR_MGMT_1); - Wire.write(1); - Wire.endTransmission(true); - //Wire.end(); - } - - InputEvent read() override { - EVERY_N_MILLISECONDS(5) { - Wire.beginTransmission(I2C_ADDRESS); - Wire.write(ACCEL_XOUT_HIGH); - Wire.endTransmission(false); - Wire.requestFrom(I2C_ADDRESS, 6); - const int16_t accelX = Wire.read() << 8 | Wire.read(); - const int16_t accelY = Wire.read() << 8 | Wire.read(); - const int16_t accelZ = Wire.read() << 8 | Wire.read(); - const uint16_t accelSum = abs(accelX) + abs(accelY) + abs(accelZ); - const uint16_t delta = abs(m_value.value() - accelSum); - m_value.add(accelSum); - if (delta > 32) { - return InputEvent{InputEvent::Acceleration, delta}; - } - } - return InputEvent{}; - } + void onStart() override; + void onStop() override; + InputEvent read() override; template struct Averager { diff --git a/src/main.cpp b/src/main.cpp index 1052e94..640fba2 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,5 +1,3 @@ -#define RENDERBUG_VERSION 1 - #include "Arduino.h" #include @@ -10,13 +8,14 @@ #include #endif -#include "BootOptions.h" +#include "Platform.h" #include "Static.h" #include "Config.h" #include "colors.h" #include "Sequencer.h" +#include "LogService.h" #include "animations/Power.cpp" #include "animations/SolidAnimation.cpp" @@ -62,16 +61,6 @@ Display neckDisplay(leds, HardwareConfig::MAX_LED_NUM, &neckMap); // Setup power management Power power; -// Clip the display at whatever is configured while still showing over-paints -FigmentFunc displayClip([](Display* dpy) { - auto coords = Static::instance()->coordMap(); - for(int i = 0; i < HardwareConfig::MAX_LED_NUM; i++) { - if (i < coords->startPixel || i > coords->pixelCount + coords->startPixel) { - dpy->pixelAt(i) %= 40; - } - } -}); - FigmentFunc configDisplay([](Display* dpy) { uint8_t brightness = brighten8_video(beatsin8(60)); auto coords = Static::instance()->coordMap(); @@ -112,23 +101,39 @@ InputBlip inputBlip; class ArduinoOTAUpdater : public BufferedInputSource { public: ArduinoOTAUpdater() : BufferedInputSource("ArduinoOTA") { - ArduinoOTA.onStart(&ArduinoOTAUpdater::s_onStart).onProgress(&ArduinoOTAUpdater::s_onProgress); + ArduinoOTA.onStart(&ArduinoOTAUpdater::s_onStart); + ArduinoOTA.onProgress(&ArduinoOTAUpdater::s_onProgress); } void loop() override { - ArduinoOTA.handle(); + if (m_online) { + ArduinoOTA.handle(); + } BufferedInputSource::loop(); } + + void handleEvent(const InputEvent& evt) { + if (evt.intent == InputEvent::NetworkStatus) { + Log.notice("Booting OTA"); + m_online = true; + ArduinoOTA.begin(); + } + } private: + bool m_online = false; static void s_onStart() { + Log.notice("OTA Start!"); Static::instance()->setEvent(InputEvent::FirmwareUpdate); } static void s_onProgress(unsigned int progress, unsigned int total) { + Log.notice("OTA Progress! %d / %d", progress, total); Static::instance()->setEvent(InputEvent{InputEvent::FirmwareUpdate, progress}); } }; +STATIC_ALLOC(ArduinoOTAUpdater); + InputFunc randomPulse([]() { static unsigned int pulse = 0; EVERY_N_MILLISECONDS(25) { @@ -158,6 +163,8 @@ InputMapper keyMap([](const InputEvent& evt) { case Buttons::Cross: return InputEvent::UserInput; break; + default: + break; } } return InputEvent::None; @@ -189,7 +196,6 @@ Renderer renderer{ &solid, &flashlight, Static::instance(), - &displayClip, &inputBlip, &power, } @@ -249,6 +255,8 @@ public: //Log.info("Save..."); setEvent(InputEvent::SaveConfigurationRequest); break; + default: + break; } } } @@ -265,6 +273,8 @@ private: case InputEvent::SetDisplayOffset: current = Static::instance()->coordMap()->startPixel; break; + default: + break; } setEvent(InputEvent{m_currentIntent, current - 1}); } @@ -278,6 +288,8 @@ private: case InputEvent::SetDisplayOffset: current = Static::instance()->coordMap()->startPixel; break; + default: + break; } setEvent(InputEvent{m_currentIntent, current + 1}); } @@ -288,22 +300,12 @@ private: return InputEvent::SetDisplayOffset; case InputEvent::SetDisplayOffset: return InputEvent::SetDisplayLength; + default: + return InputEvent::None; } } }; -enum Phase { - Null, - AstronomicalDay, - NauticalDay, - CivilDay, - CivilNight, - NauticalNight, - AstronomicalNight, - Evening, - Bedtime, -}; - struct ScheduleEntry { uint8_t hour; uint8_t brightness; @@ -358,28 +360,32 @@ uint8_t brightnessForTime(uint8_t hour, uint8_t minute) { return lerp8by8(start.brightness, end.brightness, frac); } -WiFiUDP wifiUdp; -NTP ntp(wifiUdp); +class CircadianRhythm : public InputSource { + private: + bool needsUpdate = true; + public: + CircadianRhythm() : InputSource("CircadianRhythm") {} -InputFunc circadianRhythm([]() { - static bool needsUpdate = true; + InputEvent read() { EVERY_N_SECONDS(60) { needsUpdate = true; } -#ifdef PLATFORM_PHOTON - if (Time.isValid() && needsUpdate) { - needsUpdate = false; - return InputEvent{InputEvent::SetBrightness, brightnessForTime(Time.hour(), Time.minute())}; + if (needsUpdate) { + uint8_t hour = 0; + uint8_t minute = 0; + needsUpdate = false; + struct tm timeinfo; + Platform::getLocalTime(&timeinfo); + hour = timeinfo.tm_hour; + minute = timeinfo.tm_min; + Log.notice("Current time: %d:%d", hour, minute); + return InputEvent{InputEvent::SetBrightness, brightnessForTime(hour, minute)}; + } + return InputEvent{}; } -#else - ntp.update(); - if (needsUpdate) { - needsUpdate = false; - return InputEvent{InputEvent::SetBrightness, brightnessForTime(ntp.hours(), ntp.minutes())}; - } -#endif - return InputEvent{}; -}, "CircadianRhythm", Task::Running); +}; + +STATIC_ALLOC(CircadianRhythm); // A special mainloop app for configuring hardware settings that reboots the // device when the user is finished. @@ -393,11 +399,14 @@ MainLoop configApp{{ #endif // Read hardware inputs - new Buttons(), + Static::instance(), // Map input buttons to configuration commands new ConfigInputTask(), + // System logging + Static::instance(), + // Fill the entire display with a color, to see size &configDisplay, // Render some basic input feedback @@ -409,10 +418,11 @@ MainLoop configApp{{ // Turn on, MainLoop renderbugApp{{ - // Load/update graphics configuration from EEPROM and Particle + // Load/update graphics configuration from EEPROM Static::instance(), // Platform inputs + // TODO: Merge cloud and esp wifi tasks into a common networking base #ifdef PLATFORM_PHOTON // Particle cloud status Static::instance(), @@ -424,9 +434,12 @@ MainLoop renderbugApp{{ Static::instance(), #endif + // System logging + Static::instance(), + // Hardware drivers - //new MPU5060(), - //new Buttons(), + Static::instance(), + Static::instance(), // Map buttons to events &keyMap, @@ -435,7 +448,7 @@ MainLoop renderbugApp{{ &sequencer, // Daily rhythm activities - &circadianRhythm, + Static::instance(), // Periodic motion input &randomPulse, @@ -454,7 +467,6 @@ MainLoop renderbugApp{{ // Update UI layer &power, - &displayClip, Static::instance(), &inputBlip, @@ -462,6 +474,9 @@ MainLoop renderbugApp{{ &renderer, // Platform telemetry + // TODO: Combine some of these services into a unified telemetry API with + // platform-specific backends? + // Or at least, just the MQTT and watchdog ones. #ifdef PLATFORM_PHOTON // Update photon telemetry Static::instance(), @@ -480,63 +495,33 @@ MainLoop renderbugApp{{ #else // MQTT Static::instance(), + + // OTA Updates + Static::instance(), #endif }}; MainLoop &runner = renderbugApp; -#ifdef PLATFORM_PHOTON -STARTUP(BootOptions::initPins()); -retained BootOptions bootopts; -#else -BootOptions bootopts; - -void printNewline(Print* logOutput) { - logOutput->print("\r\n"); -} -#endif - // Tune in, void setup() { - Serial.begin(115200); -#ifdef PLATFORM_PHOTON - System.enableFeature(FEATURE_RETAINED_MEMORY); - if (bootopts.isFlash) { - System.dfu(); - } - if (bootopts.isSerial) { - bootopts.waitForRelease(); - while(!Serial.isConnected()) { - Particle.process(); - } - //Log.info("\xf0\x9f\x94\x8c Serial connected"); - } -#else - Log.begin(LOG_LEVEL_VERBOSE, &Serial, true); - Log.setSuffix(printNewline); - Log.notice("Test log?"); -#endif + Platform::preSetup(); + Static::instance()->setSequencer(&sequencer); Log.notice(u8"🐛 Booting Renderbug!"); Log.notice(u8"🐞 I am built for %d LEDs running on %dmA", HardwareConfig::MAX_LED_NUM, PSU_MILLIAMPS); -#ifdef PLATFORM_PHOTON - Log.notice(u8"📡 Particle version %s", System.version().c_str()); -#endif - Log.notice(u8" Boot pin configuration:"); - Log.notice(u8" 2: Setup - %d", bootopts.isSetup); - Log.notice(u8" 3: Serial - %d", bootopts.isSerial); - Log.notice(u8" 4: Flash - %d", bootopts.isFlash); + Log.notice(u8"📡 Platform %s version %s", Platform::name(), Platform::version()); + Platform::bootSplash(); - //Log.info(u8" Setting timezone to UTC-7"); - //Time.zone(-7); + Log.notice(u8"Setting timezone to -7 (PST)"); + Platform::setTimezone(-7); + + Log.notice(u8" Setting up platform..."); + Platform::setup(); Log.notice(u8"💡 Starting FastLED..."); -#ifdef PLATFORM_PHOTON - FastLED.addLeds(leds, HardwareConfig::MAX_LED_NUM); -#else - FastLED.addLeds(leds, HardwareConfig::MAX_LED_NUM); -#endif + Platform::addLEDs(leds, HardwareConfig::MAX_LED_NUM); - if (bootopts.isSetup) { + if (Platform::bootopts.isSetup) { Log.notice(u8"🌌 Starting Figment in configuration mode..."); runner = configApp; } else { @@ -548,8 +533,6 @@ void setup() { //Log.info(u8"💽 %lu bytes of free RAM", System.freeMemory()); Log.notice(u8"🚀 Setup complete! Ready to rock and roll."); Serial.flush(); - ntp.begin(); - ArduinoOTA.begin(); } // Drop out. diff --git a/src/platform/arduino/MQTTTelemetry.cpp b/src/platform/arduino/MQTTTelemetry.cpp new file mode 100644 index 0000000..c390e81 --- /dev/null +++ b/src/platform/arduino/MQTTTelemetry.cpp @@ -0,0 +1,230 @@ +#include "MQTTTelemetry.h" + +#ifdef BOARD_ESP8266 +#include +#elif defined(BOARD_ESP32) +#include +#endif + +#include + +#include "../../Static.h" +#include "../../Config.h" +#include "../../Platform.h" + +WiFiClient wifiClient; + +MQTTTelemetry::MQTTTelemetry() : BufferedInputSource("MQTT"), + m_mqtt(PubSubClient(wifiClient)), + m_logPrinter(this) +{} + +void +MQTTTelemetry::handleEventOnline(const InputEvent& evt) +{ + if (!m_mqtt.connected()) { + Log.notice("Connecting to MQTT..."); + const IPAddress server(10, 0, 0, 2); + const char* deviceID = Platform::deviceID(); + Log.verbose("Device ID %s", deviceID); + m_mqtt.setServer(server, 1883); + m_mqtt.setBufferSize(512); + m_mqtt.setCallback(&MQTTTelemetry::s_callback); + if (m_mqtt.connect(deviceID)) { + Log.notice("Connected to MQTT"); + m_needHeartbeat = true; + + const String deviceName = String("Renderbug ESP8266") + (char*)deviceID; + const String rootTopic = String("homeassistant/light/renderbug/") + (char*)deviceID; + const String configTopic = rootTopic + "/config"; + Log.verbose("root topic %s", rootTopic.c_str()); + Log.verbose("config topic %s", configTopic.c_str()); + const String statTopic = rootTopic + "/state"; + const String cmdTopic = rootTopic + "/set"; + const String attrTopic = rootTopic + "/attributes"; + const String logTopic = rootTopic + "/log"; + const String heartbeatTopic = rootTopic + "/heartbeat"; + strcpy(m_statTopic, statTopic.c_str()); + strcpy(m_attrTopic, attrTopic.c_str()); + strcpy(m_cmdTopic, cmdTopic.c_str()); + strcpy(m_logTopic, logTopic.c_str()); + strcpy(m_heartbeatTopic, heartbeatTopic.c_str()); + + StaticJsonDocument<1024> configJson; + configJson["~"] = rootTopic; + configJson["name"] = deviceName; + configJson["ret"] = true; + configJson["unique_id"] = (char*) deviceID; + configJson["cmd_t"] = "~/set"; + configJson["stat_t"] = "~/state"; + configJson["json_attr_t"] = "~/attributes"; + configJson["schema"] = "json"; + configJson["brightness"] = true; + configJson["rgb"] = true; + + int i = 0; + for(const Sequencer::Scene& scene : m_sequencer->scenes()) { + configJson["fx_list"][i++] = scene.name; + } + + configJson["dev"]["name"] = "Renderbug"; +#ifdef PLATFORM_PHOTON + configJson["dev"]["mdl"] = "Photon"; +#elif defined(BOARD_ESP32) + configJson["dev"]["mdl"] = "ESP32"; +#elif defined(BOARD_ESP8266) + configJson["dev"]["mdl"] = "ESP8266"; +#else + configJson["dev"]["mdl"] = "Unknown"; +#endif + configJson["dev"]["sw"] = RENDERBUG_VERSION; + configJson["dev"]["mf"] = "Phong Robotics"; + configJson["dev"]["ids"][0] = (char*)deviceID; + char buf[1024]; + serializeJson(configJson, buf, sizeof(buf)); + Log.verbose("Publish %s %s", configTopic.c_str(), buf); + m_mqtt.publish(configTopic.c_str(), buf); + m_mqtt.subscribe(m_cmdTopic); + } else { + Log.warning("Could not connect to MQTT"); + } + } else { + if (evt.intent == InputEvent::SetPower) { + StaticJsonDocument<256> doc; + char buf[256]; + doc["state"] = evt.asInt() ? "ON" : "OFF"; + serializeJson(doc, buf, sizeof(buf)); + m_mqtt.publish(m_statTopic, buf); + } else if (evt.intent == InputEvent::SetBrightness) { + StaticJsonDocument<256> doc; + char buf[256]; + doc["brightness"] = evt.asInt(); + doc["state"] = "ON"; + serializeJson(doc, buf, sizeof(buf)); + m_mqtt.publish(m_statTopic, buf); + } else if (evt.intent == InputEvent::SetColor) { + StaticJsonDocument<256> doc; + char buf[256]; + CRGB color = evt.asRGB(); + doc["color"]["r"] = color.r; + doc["color"]["g"] = color.g; + doc["color"]["b"] = color.b; + doc["state"] = "ON"; + serializeJson(doc, buf, sizeof(buf)); + m_mqtt.publish(m_statTopic, buf); + } else if (evt.intent == InputEvent::SetPattern) { + StaticJsonDocument<256> doc; + char buf[256]; + doc["effect"] = evt.asString(); + doc["state"] = "ON"; + serializeJson(doc, buf, sizeof(buf)); + m_mqtt.publish(m_statTopic, buf); + } else if (evt.intent == InputEvent::FirmwareUpdate) { + m_mqtt.publish("renderbug/debug/firmware", "firmware update!"); + } + } +} + +void +MQTTTelemetry::loop() +{ + BufferedInputSource::loop(); + OnlineTaskMixin::loop(); +} + +void +MQTTTelemetry::loopOnline() +{ + m_mqtt.loop(); + EVERY_N_SECONDS(10) { + m_needHeartbeat = true; + } + if (m_needHeartbeat) { + char buf[512]; + StaticJsonDocument<512> response; + response["fps"] = FastLED.getFPS(); + response["RSSI"] = WiFi.RSSI(); + response["localip"] = WiFi.localIP().toString(); + response["free_ram"] = ESP.getFreeHeap(); + response["os_version"] = ESP.getSdkVersion(); + response["sketch_version"] = ESP.getSketchMD5(); + serializeJson(response, buf, sizeof(buf)); + m_mqtt.publish(m_attrTopic, buf); + m_mqtt.publish(m_heartbeatTopic, buf); + Log.notice("Heartbeat: %s", buf); + + response.clear(); + auto sched = MainLoop::instance()->scheduler; + for(auto task : sched.tasks) { + response[task->name] = task->state == Task::Running; + } + serializeJson(response, buf, sizeof(buf)); + m_mqtt.publish(m_heartbeatTopic, buf); + m_needHeartbeat = false; + } +} + +void +MQTTTelemetry::callback(char* topic, byte* payload, unsigned int length) +{ + DynamicJsonDocument doc(1024); + deserializeJson(doc, payload, length); + + if (doc.containsKey("state")) { + if (doc["state"] == "ON") { + setEvent(InputEvent{InputEvent::SetPower, true}); + } else if (doc["state"] == "OFF") { + setEvent(InputEvent{InputEvent::SetPower, false}); + } + } + + if (doc.containsKey("start")) { + strcpy(m_patternBuf, doc["start"].as()); + setEvent(InputEvent{InputEvent::StartThing, m_patternBuf}); + } + + if (doc.containsKey("stop")) { + if (doc["stop"] == name) { + Log.notice("You can't kill an idea, or stop the MQTT Task via MQTT."); + } else { + strcpy(m_patternBuf, doc["stop"].as()); + setEvent(InputEvent{InputEvent::StopThing, m_patternBuf}); + } + } + + if (doc.containsKey("pixelCount")) { + setEvent(InputEvent{InputEvent::SetDisplayLength, (int)doc["pixelCount"]}); + } + + if (doc.containsKey("startPixel")) { + setEvent(InputEvent{InputEvent::SetDisplayOffset, (int)doc["startPixel"]}); + } + + if (doc.containsKey("save")) { + setEvent(InputEvent{InputEvent::SaveConfigurationRequest}); + } + + if (doc.containsKey("effect")) { + strcpy(m_patternBuf, doc["effect"].as()); + setEvent(InputEvent{InputEvent::SetPattern, m_patternBuf}); + } + + if (doc.containsKey("color")) { + uint8_t r = doc["color"]["r"]; + uint8_t g = doc["color"]["g"]; + uint8_t b = doc["color"]["b"]; + setEvent(InputEvent{InputEvent::SetColor, CRGB(r, g, b)}); + } + + if (doc.containsKey("brightness")) { + setEvent(InputEvent{InputEvent::SetBrightness, (int)doc["brightness"]}); + } +} + +void +MQTTTelemetry::s_callback(char* topic, byte* payload, unsigned int length) +{ + Static::instance()->callback(topic, payload, length); +} + +STATIC_ALLOC(MQTTTelemetry); diff --git a/src/platform/arduino/MQTTTelemetry.h b/src/platform/arduino/MQTTTelemetry.h index a676a36..3bc5f8b 100644 --- a/src/platform/arduino/MQTTTelemetry.h +++ b/src/platform/arduino/MQTTTelemetry.h @@ -1,163 +1,58 @@ -#include +#include +#include + #include #include -#include -#include -WiFiClient wifi; -PubSubClient m_mqtt(wifi); +#include "../../Sequencer.h" class MQTTTelemetry : public BufferedInputSource, OnlineTaskMixin { public: - MQTTTelemetry() : BufferedInputSource("MQTT") {} + MQTTTelemetry(); + void setSequencer(Sequencer* seq) { m_sequencer = seq; } - void handleEventOnline(const InputEvent& evt) override { - if (!m_mqtt.connected()) { - Log.notice("Connecting to MQTT..."); - const IPAddress server(10, 0, 0, 2); - uint64_t chipid = ESP.getEfuseMac(); - char deviceID[15]; - snprintf(deviceID, 15, "%08X", (uint32_t)chipid); - Log.verbose("Device ID %s", deviceID); - m_mqtt.setServer(server, 1883); - m_mqtt.setBufferSize(512); - m_mqtt.setCallback(&MQTTTelemetry::s_callback); - if (m_mqtt.connect(deviceID)) { - Log.notice("Connected to MQTT"); - - const String deviceName = String("Renderbug ESP32 ") + (char*)deviceID; - const String rootTopic = String("homeassistant/light/renderbug/") + (char*)deviceID; - const String configTopic = rootTopic + "/config"; - Log.verbose("root topic %s", rootTopic.c_str()); - Log.verbose("config topic %s", configTopic.c_str()); - const String statTopic = rootTopic + "/state"; - const String cmdTopic = rootTopic + "/set"; - const String attrTopic = rootTopic + "/attributes"; - strcpy(m_statTopic, statTopic.c_str()); - strcpy(m_attrTopic, attrTopic.c_str()); - strcpy(m_cmdTopic, cmdTopic.c_str()); - - StaticJsonDocument<1024> configJson; - configJson["~"] = rootTopic; - configJson["name"] = deviceName; - configJson["ret"] = true; - configJson["unique_id"] = (char*) deviceID; - configJson["cmd_t"] = "~/set"; - configJson["stat_t"] = "~/state"; - configJson["json_attr_t"] = "~/attributes"; - configJson["schema"] = "json"; - configJson["brightness"] = true; - configJson["rgb"] = true; - - configJson["dev"]["name"] = "Renderbug"; -#ifdef PLATFORM_PHOTON - configJson["dev"]["mdl"] = "Photon"; -#else - configJson["dev"]["mdl"] = "ESP32"; -#endif - configJson["dev"]["sw"] = RENDERBUG_VERSION; - configJson["dev"]["mf"] = "Phong Robotics"; - configJson["dev"]["ids"][0] = (char*)deviceID; - char buf[1024]; - serializeJson(configJson, buf, sizeof(buf)); - Log.verbose("Publish %s %s", configTopic.c_str(), buf); - m_mqtt.publish(configTopic.c_str(), buf); - m_mqtt.subscribe(m_cmdTopic); - } else { - Log.warning("Could not connect to MQTT"); + class LogPrinter : public Print { + private: + MQTTTelemetry* telemetry; + Ringbuf buf; + public: + LogPrinter(MQTTTelemetry* telemetry) : telemetry(telemetry) {}; + size_t write(uint8_t byte) { + char outBuf[512]; + if (byte == '\n') { + size_t bufSize = buf.write(outBuf); + outBuf[std::min(sizeof(outBuf), bufSize)] = 0; + telemetry->m_mqtt.publish(telemetry->m_logTopic, outBuf); + } else { + buf.insert(byte); + } + return sizeof(byte); } - } else { - if (evt.intent == InputEvent::SetPower) { - StaticJsonDocument<256> doc; - char buf[256]; - doc["state"] = evt.asInt() ? "ON" : "OFF"; - serializeJson(doc, buf, sizeof(buf)); - m_mqtt.publish(m_statTopic, buf); - } else if (evt.intent == InputEvent::SetBrightness) { - StaticJsonDocument<256> doc; - char buf[256]; - doc["brightness"] = evt.asInt(); - serializeJson(doc, buf, sizeof(buf)); - m_mqtt.publish(m_statTopic, buf); - } else if (evt.intent == InputEvent::SetColor) { - StaticJsonDocument<256> doc; - char buf[256]; - CRGB color = evt.asRGB(); - doc["color"]["r"] = color.r; - doc["color"]["g"] = color.g; - doc["color"]["b"] = color.b; - serializeJson(doc, buf, sizeof(buf)); - m_mqtt.publish(m_statTopic, buf); - } else if (evt.intent == InputEvent::SetPattern) { - StaticJsonDocument<256> doc; - char buf[256]; - doc["effect"] = evt.asString(); - serializeJson(doc, buf, sizeof(buf)); - m_mqtt.publish(m_statTopic, buf); - } - } + }; + + Print* logPrinter() { + return &m_logPrinter; } - void loop() override { - BufferedInputSource::loop(); - OnlineTaskMixin::loop(); - } + void handleEventOnline(const InputEvent& evt) override; + void loop() override; - void loopOnline() override { - m_mqtt.loop(); - EVERY_N_SECONDS(10) { - char buf[254]; - StaticJsonDocument<200> response; - response["fps"] = FastLED.getFPS(); - response["RSSI"] = WiFi.RSSI(); - response["localip"] = WiFi.localIP().toString(); - response["free_ram"] = ESP.getFreeHeap(); - response["os_version"] = ESP.getSdkVersion(); - response["sketch_version"] = ESP.getSketchMD5(); - serializeJson(response, buf, sizeof(buf)); - m_mqtt.publish(m_attrTopic, buf); - } - } + void loopOnline() override; private: char m_statTopic[100]; char m_attrTopic[100]; char m_cmdTopic[100]; + char m_logTopic[100]; + char m_heartbeatTopic[100]; - void callback(char* topic, byte* payload, unsigned int length) { - DynamicJsonDocument doc(1024); - deserializeJson(doc, payload, length); - - if (doc.containsKey("state")) { - if (doc["state"] == "ON") { - setEvent(InputEvent{InputEvent::SetPower, true}); - } else if (doc["state"] == "OFF") { - setEvent(InputEvent{InputEvent::SetPower, false}); - } - } - - if (doc.containsKey("effect")) { - strcpy(m_patternBuf, doc["effect"].as()); - setEvent(InputEvent{InputEvent::SetPattern, m_patternBuf}); - } - - if (doc.containsKey("color")) { - uint8_t r = doc["color"]["r"]; - uint8_t g = doc["color"]["g"]; - uint8_t b = doc["color"]["b"]; - setEvent(InputEvent{InputEvent::SetColor, CRGB(r, g, b)}); - } - - if (doc.containsKey("brightness")) { - setEvent(InputEvent{InputEvent::SetBrightness, (int)doc["brightness"]}); - } - } - - static void s_callback(char* topic, byte* payload, unsigned int length) { - Static::instance()->callback(topic, payload, length); - } + void callback(char* topic, byte* payload, unsigned int length); + static void s_callback(char* topic, byte* payload, unsigned int length); char m_patternBuf[48]; -}; + bool m_needHeartbeat = false; -STATIC_ALLOC(MQTTTelemetry); + Sequencer *m_sequencer = 0; + PubSubClient m_mqtt; + LogPrinter m_logPrinter; +}; diff --git a/src/platform/particle/PhotonTelemetry.cpp b/src/platform/particle/PhotonTelemetry.cpp index 2315cbe..2ef2d89 100644 --- a/src/platform/particle/PhotonTelemetry.cpp +++ b/src/platform/particle/PhotonTelemetry.cpp @@ -57,108 +57,12 @@ PhotonTelemetry::loop() void PhotonTelemetry::handleEvent(const InputEvent &evt) { - Serial.flush(); - if (evt.intent == InputEvent::NetworkStatus) { - onConnected(); - } - if (evt.intent != InputEvent::None) { - const char* sourceName; - switch(evt.intent) { - case InputEvent::PowerToggle: - sourceName = "power-toggle"; - break; - case InputEvent::SetPower: - sourceName = "set-power"; - break; - case InputEvent::PreviousPattern: - sourceName = "previous-pattern"; - break; - case InputEvent::NextPattern: - sourceName = "next-pattern"; - break; - case InputEvent::SetPattern: - sourceName = "set-pattern"; - break; - case InputEvent::SetColor: - sourceName = "set-color"; - break; - case InputEvent::Acceleration: - sourceName = "acceleration"; - break; - case InputEvent::UserInput: - sourceName = "user"; - break; - case InputEvent::SetBrightness: - sourceName = "set-brightness"; - break; - case InputEvent::FirmwareUpdate: - sourceName = "firmware-update"; - break; - case InputEvent::NetworkStatus: - sourceName = "network-status"; - break; - case InputEvent::NetworkActivity: - sourceName = "network-activity"; - break; - case InputEvent::StartThing: - sourceName = "start-thing"; - break; - case InputEvent::StopThing: - sourceName = "stop-thing"; - break; - case InputEvent::SetDisplayOffset: - sourceName = "set-display-offset"; - break; - case InputEvent::SetDisplayLength: - sourceName = "set-display-length"; - break; - case InputEvent::SaveConfigurationRequest: - sourceName = "save-configuration"; - break; - default: - sourceName = 0; - break; - } - char valueBuf[255]; - switch(evt.type) { - case InputEvent::Null: - snprintf(valueBuf, sizeof(valueBuf), "null");break; - case InputEvent::Integer: - snprintf(valueBuf, sizeof(valueBuf), "%d %02x", evt.asInt(), evt.asInt());break; - case InputEvent::String: - snprintf(valueBuf, sizeof(valueBuf), "\"%s\"", evt.asString());break; - case InputEvent::Color: - snprintf(valueBuf, sizeof(valueBuf), "[%d, %d, %d]", evt.asRGB().r, evt.asRGB().g, evt.asRGB().b);break; - } - char buf[255 * 2]; - if (sourceName == 0) { - snprintf(buf, sizeof(buf), "{\"intent\": %d, \"value\": %s}", evt.intent, valueBuf); - } else { - snprintf(buf, sizeof(buf), "{\"intent\": \"%s\", \"value\": %s}", sourceName, valueBuf); - } - if (m_online) { - if (evt.intent != m_lastEvent.intent) { - if (m_duplicateEvents > 0) { - Log.info("Suppressed reporting %ld duplicate events.", m_duplicateEvents); - } - Log.info("Event: %s", buf); - m_duplicateEvents = 0; - m_lastEvent = evt; - Particle.publish("renderbug/event", buf, PRIVATE); - } else { - m_duplicateEvents++; - } - } else { - Log.info("[offline] Event: %s", buf); - } - - if (evt.intent == InputEvent::SetColor) { - NSFastLED::CRGB rgb {evt.asRGB()}; - uint32_t color = (rgb.r << 16) + (rgb.g << 8) + (rgb.b << 0); - m_ledStatus.setColor(color); - m_ledStatus.setActive(true); - m_rgbPulseFrame = 1000; - } + if (evt.intent == InputEvent::SetColor) { + NSFastLED::CRGB rgb {evt.asRGB()}; + uint32_t color = (rgb.r << 16) + (rgb.g << 8) + (rgb.b << 0); + m_ledStatus.setColor(color); + m_ledStatus.setActive(true); + m_rgbPulseFrame = 1000; } }