diff --git a/lib/Figments/Figment.h b/lib/Figments/Figment.h index db21fba..fbce8e3 100644 --- a/lib/Figments/Figment.h +++ b/lib/Figments/Figment.h @@ -34,6 +34,12 @@ struct Task : public virtual Loopable { State state = Running; }; +struct TaskFunc: public Task { + TaskFunc(std::function func) : Task("lambda"), func(func) {} + void loop() override {func();} + std::function func; +}; + struct Figment: public Task { Figment() : Task() {} Figment(State initialState) : Task(initialState) {} diff --git a/lib/Figments/Input.h b/lib/Figments/Input.h index 0036ef3..a3fd2c0 100644 --- a/lib/Figments/Input.h +++ b/lib/Figments/Input.h @@ -137,7 +137,7 @@ protected: void setEvent(InputEvent::Intent intent, Variant &&v); private: - Ringbuf m_eventQueue; + Ringbuf m_eventQueue; }; class InputMapper: public BufferedInputSource { @@ -157,12 +157,20 @@ class OnlineTaskMixin : public virtual Loopable { void handleEvent(const InputEvent &evt) override { if (evt.intent == InputEvent::NetworkStatus) { m_online = evt.asInt(); + if (m_online) { + onOnline(); + } else { + onOffline(); + } } if (m_online) { handleEventOnline(evt); } } + virtual void onOnline() {} + virtual void onOffline() {} + virtual void handleEventOnline(const InputEvent &evt) {} void loop() override { diff --git a/lib/Figments/MainLoop.cpp b/lib/Figments/MainLoop.cpp index 5f9625c..bdb20f6 100644 --- a/lib/Figments/MainLoop.cpp +++ b/lib/Figments/MainLoop.cpp @@ -37,10 +37,27 @@ MainLoop::loop() task->handleEvent(evt); } } + unsigned int slowest = 0; + unsigned int frameSpeed = 0; + unsigned int frameStart = millis(); + unsigned int taskCount = 0; + Task* slowestTask = NULL; for(Task* task : scheduler) { - //Log.notice("Running %s", task->name); + //unsigned int start = millis(); + unsigned int start = ESP.getCycleCount(); task->loop(); - //Log.notice("next"); + //unsigned int runtime = millis() - start; + unsigned int runtime = ESP.getCycleCount() - start; + frameSpeed += runtime; + taskCount++; + if (runtime > slowest) { + slowest = runtime; + slowestTask = task; + } + } + frameSpeed = millis() - frameStart; + if (frameSpeed >= 23) { + Log.notice("Slow frame: %dms, %d tasks, longest task %s was %dms", frameSpeed, taskCount, slowestTask->name, slowest/160000); } } diff --git a/lib/Figments/Renderer.cpp b/lib/Figments/Renderer.cpp index 3e7b2aa..acdafb2 100644 --- a/lib/Figments/Renderer.cpp +++ b/lib/Figments/Renderer.cpp @@ -9,11 +9,12 @@ Renderer::loop() for(Display* dpy : m_displays) { for(Figment* figment : m_figments) { if (figment->state == Task::Running) { - //Log.notice("Rendering %s", figment->name); + unsigned int frameStart = ESP.getCycleCount(); figment->render(dpy); - //Log.notice("next"); - } else { - //Log.notice("Not rendering %s", figment->name); + unsigned int runtime = (ESP.getCycleCount() - frameStart) / 160000; + if (runtime >= 8) { + Log.notice("SLOW RENDER: %s took %dms!", figment->name, runtime); + } } }; } diff --git a/lib/README b/lib/README deleted file mode 100644 index 6debab1..0000000 --- a/lib/README +++ /dev/null @@ -1,46 +0,0 @@ - -This directory is intended for project specific (private) libraries. -PlatformIO will compile them to static libraries and link into executable file. - -The source code of each library should be placed in a an own separate directory -("lib/your_library_name/[here are source files]"). - -For example, see a structure of the following two libraries `Foo` and `Bar`: - -|--lib -| | -| |--Bar -| | |--docs -| | |--examples -| | |--src -| | |- Bar.c -| | |- Bar.h -| | |- library.json (optional, custom build options, etc) https://docs.platformio.org/page/librarymanager/config.html -| | -| |--Foo -| | |- Foo.c -| | |- Foo.h -| | -| |- README --> THIS FILE -| -|- platformio.ini -|--src - |- main.c - -and a contents of `src/main.c`: -``` -#include -#include - -int main (void) -{ - ... -} - -``` - -PlatformIO Library Dependency Finder will find automatically dependent -libraries scanning project source files. - -More information about PlatformIO Library Dependency Finder -- https://docs.platformio.org/page/librarymanager/ldf.html diff --git a/no_ota.csv b/no_ota.csv new file mode 100644 index 0000000..899edca --- /dev/null +++ b/no_ota.csv @@ -0,0 +1,4 @@ +# Name, Type, SubType, Offset, Size, Flags +nvs, data, nvs, 0x9000, 0x5000, +otadata, data, ota, 0xe000, 0x1000, +app, app, factory, 0x10000, 2M, diff --git a/out.log b/out.log deleted file mode 100644 index 6d8bf29..0000000 --- a/out.log +++ /dev/null @@ -1,33 +0,0 @@ -ets Jun 8 2016 00:22:57 - -rst:0x1 (POWERON_RESET),boot:0x13 (SPI_FAST_FLASH_BOOT) -configsip: 0, SPIWP:0xee -clk_drv:0x00,q_drv:0x00,d_drv:0x00,cs0_drv:0x00,hd_drv:0x00,wp_drv:0x00 -mode:DIO, clock div:2 -load:0x3fff0018,len:4 -load:0x3fff001c,len:1044 -load:0x40078000,len:10124 -load:0x40080400,len:5828 -entry 0x400806a8 -N: 🐛 Booting Renderbug! -N: 🐞 I am built for 255 LEDs running on 2000mA -N: Boot pin configuration: -N: 2: Setup - 0 -N: 3: Serial - 0 -N: 4: Flash - 0 -N: 💡 Starting FastLED... -N: 🌌 Starting Figment... -N: *** Starting 20 tasks... -N: * Starting Configuration... -N: * Starting MPU5060... -N: * Starting Buttons... -N: * Starting ... -N: * Starting SceneSequencer... -N: * Starting CircadianRhythm... -N: * Starting Pulse... -N: * Starting Solid... -N: * Starting Power... -N: * Starting lambda... -N: * Starting UpdateStatusAnimation... -N: * Starting Renderer... -N: 🚀 Setup complete! Ready to rock and roll. diff --git a/platformio.ini b/platformio.ini index 0ce140f..23664ca 100644 --- a/platformio.ini +++ b/platformio.ini @@ -11,13 +11,37 @@ [common_env_data] src_filter = "+<*> -<.git/> -<.svn/> -" -[env:featheresp32] +[env:esp32] platform = espressif32 board = featheresp32 framework = arduino build_flags = - -D PLATFORM_ARDUINO - -D BOARD_ESP32 + -DPLATFORM_ARDUINO + -DBOARD_ESP32 + -DCONFIG_NO_COLORDATA +; -DCORE_DEBUG_LEVEL=5 +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} +" +board_build.partitions = no_ota.csv +;build_type = debug + +[env:cyberplague] +extends = env:esp32 +board_build.partitions = no_ota.csv + +[env:esp8266] +platform = espressif8266 +board = huzzah +framework = arduino +build_flags = + -DPLATFORM_ARDUINO + -DBOARD_ESP8266 lib_deps = fastled/FastLED@^3.4.0 thijse/ArduinoLog@^1.0.3 @@ -27,18 +51,7 @@ lib_deps = 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} +" +;[env:photon] +;platform = particlephoton +;board = photon +;framework = arduino diff --git a/src/BootOptions.cpp b/src/BootOptions.cpp index 0429b35..5361c57 100644 --- a/src/BootOptions.cpp +++ b/src/BootOptions.cpp @@ -1,4 +1,9 @@ #include "BootOptions.h" +#ifdef BOARD_ESP8266 +#include +#endif +#include +#include "Config.h" #ifdef PLATFORM_PHOTON LEDStatus serialStatus = LEDStatus(RGB_COLOR_ORANGE, LED_PATTERN_FADE, LED_SPEED_FAST, LED_PRIORITY_BACKGROUND); @@ -31,6 +36,28 @@ BootOptions::BootOptions() configStatus.setActive(isSetup); serialStatus.setActive(isSerial); #endif +#ifdef BOARD_ESP8266 + struct rst_info resetInfo = *ESP.getResetInfoPtr(); + uint8_t crashCount; + EEPROM.begin(sizeof(crashCount)); + EEPROM.get(sizeof(HardwareConfig) + 32, crashCount); + EEPROM.end(); + if (resetInfo.reason == REASON_WDT_RST) { + if (crashCount++ >= 3) { + // Boot into safe mode if the watchdog reset us three times in a row. + isSafeMode = true; + } else { + EEPROM.begin(sizeof(crashCount)); + EEPROM.put(sizeof(HardwareConfig) + 32, crashCount); + EEPROM.end(); + } + } else if (crashCount != 0) { + crashCount = 0; + EEPROM.begin(sizeof(crashCount)); + EEPROM.put(sizeof(HardwareConfig) + 32, crashCount); + EEPROM.end(); + } +#endif } void diff --git a/src/BootOptions.h b/src/BootOptions.h index 1925a84..26acb5e 100644 --- a/src/BootOptions.h +++ b/src/BootOptions.h @@ -11,4 +11,5 @@ struct BootOptions { bool isSerial = false; bool isFlash = false; bool lastBootWasFlash = false; + bool isSafeMode = false; }; diff --git a/src/Config.cpp b/src/Config.cpp index 4e4a24e..2637176 100644 --- a/src/Config.cpp +++ b/src/Config.cpp @@ -8,7 +8,10 @@ constexpr uint16_t HardwareConfig::MAX_LED_NUM; HardwareConfig HardwareConfig::load() { HardwareConfig ret; + EEPROM.begin(sizeof(ret)); EEPROM.get(0, ret); + EEPROM.end(); + Log.notice("Loaded config version %d, CRC %d", ret.version, ret.checksum); return ret; } @@ -16,7 +19,10 @@ void HardwareConfig::save() { HardwareConfig dataCopy{*this}; dataCopy.checksum = getCRC(); + EEPROM.begin(sizeof(dataCopy)); EEPROM.put(0, dataCopy); + EEPROM.commit(); + EEPROM.end(); } LinearCoordinateMapping @@ -66,13 +72,13 @@ ConfigService::onStart() m_coordMap = m_config.toCoordMap(); Log.notice("Configured to use %d pixels, starting at %d", m_config.data.pixelCount, m_config.data.startPixel); - Log.notice("Loading task states..."); + /*Log.notice("Loading task states..."); for(int i = 0; i < 32; i++) { auto svc = m_config.data.serviceStates[i]; - if (strlen(svc.name) > 0) { + if (strnlen(svc.name, 16) > 0) { Log.notice("* %s: %s", svc.name, svc.isDisabled? "DISABLED" : "ENABLED"); } - } + }*/ } void diff --git a/src/LogService.cpp b/src/LogService.cpp index 3d76568..7bc33e3 100644 --- a/src/LogService.cpp +++ b/src/LogService.cpp @@ -81,7 +81,7 @@ LogService::handleEvent(const InputEvent& evt) { } if (evt.intent != m_lastEvent.intent) { if (m_duplicateEvents > 0) { - Log.notice("Suppressed reporting %u duplicate events.", m_duplicateEvents); + Log.notice("Suppressed reporting %d duplicate events.", m_duplicateEvents); } Log.verbose("Event: %s", buf); m_duplicateEvents = 0; diff --git a/src/Platform.cpp b/src/Platform.cpp index 3cc5ee8..4998c61 100644 --- a/src/Platform.cpp +++ b/src/Platform.cpp @@ -4,6 +4,8 @@ #ifdef BOARD_ESP32 #include +#include +#include #elif defined(BOARD_ESP8266) #include #include @@ -11,16 +13,23 @@ #include WiFiUDP wifiUdp; -NTPClient timeClient(wifiUdp, "pool.ntp.org", 3600 * -7); +//NTPClient timeClient(wifiUdp, "pool.ntp.org", 3600 * -7); +NTPClient timeClient(wifiUdp, "10.0.0.1", 3600 * -7); #endif #ifdef PLATFORM_PHOTON STARTUP(BootOptions::initPins()); #else #include "platform/arduino/MQTTTelemetry.h" -void printNewline(Print* logOutput) { +void printNewline(Print* logOutput) +{ logOutput->print("\r\n"); } +int printEspLog(const char* fmt, va_list args) +{ + Log.notice(fmt, args); + return 1; +} #endif int Platform::s_timezone = 0; @@ -30,6 +39,10 @@ Platform::name() { #ifdef PLATFORM_PHOTON return "Photon"; +#elif defined(BOARD_ESP8266) + return "ESP8266"; +#elif defined(BOARD_ESP32) + return "ESP32"; #else return "Unknown!"; #endif @@ -40,6 +53,8 @@ Platform::version() { #ifdef PLATFORM_PHOTON return System.version().c_str(); +#elif defined(BOARD_ESP32) + return ESP.getSdkVersion(); #else return "Unknown!"; #endif @@ -49,6 +64,7 @@ void Platform::preSetup() { Serial.begin(115200); + delay(5000); #ifdef PLATFORM_PHOTON System.enableFeature(FEATURE_RETAINED_MEMORY); if (bootopts.isFlash) { @@ -65,6 +81,15 @@ Platform::preSetup() Log.begin(LOG_LEVEL_VERBOSE, Static::instance()->logPrinter()); Log.setSuffix(printNewline); #endif + +#ifdef BOARD_ESP32 + esp_task_wdt_init(10, true); + esp_task_wdt_add(NULL); + esp_log_set_vprintf(printEspLog); +#endif +#ifdef BOARD_ESP8266 + ESP.wdtEnable(0); +#endif } void @@ -95,7 +120,12 @@ void Platform::loop() { #ifdef BOARD_ESP8266 - timeClient.update(); + if (WiFi.status() == WL_CONNECTED) { + timeClient.update(); + } + ESP.wdtFeed(); +#elif defined(BOARD_ESP32) + esp_task_wdt_reset(); #endif } @@ -110,9 +140,12 @@ Platform::getLocalTime(struct tm* timedata) } return false; #elif defined(BOARD_ESP32) - return getLocalTime(timedata); + time_t rawtime; + time(&rawtime); + (*timedata) = (*localtime(&rawtime)); + return (timedata->tm_year > (2016-1990)); + //return getLocalTime(timedata); #else - timeClient.update(); timedata->tm_hour = timeClient.getHours(); timedata->tm_min = timeClient.getMinutes(); return true; @@ -137,3 +170,5 @@ Platform::bootopts; char Platform::s_deviceID[15]; + +STATIC_ALLOC(Platform); diff --git a/src/Platform.h b/src/Platform.h index b8aba95..ecaeced 100644 --- a/src/Platform.h +++ b/src/Platform.h @@ -1,11 +1,13 @@ #pragma once #include +#include #include "BootOptions.h" -class Platform { +class Platform : public Task { static int s_timezone; static char s_deviceID[15]; public: + Platform() : Task("Platform") {} static BootOptions bootopts; static void setTimezone(int tz) { s_timezone = tz; } static int getTimezone() { return s_timezone; } @@ -14,18 +16,28 @@ class Platform { #ifdef PLATFORM_PHOTON FastLED.addLeds(leds, ledCount); #elif defined(BOARD_ESP32) - FastLED.addLeds(leds, ledCount); + FastLED.addLeds(leds, ledCount); #else - FastLED.addLeds(leds, ledCount); + //FastLED.addLeds(leds, ledCount); + FastLED.addLeds(leds, ledCount); #endif } static const char* name(); static const char* version(); + static const String model() { + static String modelName = String("Renderbug " ) + Platform::name(); + return modelName; + } + static const String deviceName() { + static String devName = model() + " " + Platform::deviceID(); + return devName; + } static void preSetup(); static void bootSplash(); static void setup(); - static void loop(); + void loop() override; static bool getLocalTime(struct tm* timedata); static const char* deviceID(); }; + diff --git a/src/Sequencer.cpp b/src/Sequencer.cpp index e573c3c..1ccbe04 100644 --- a/src/Sequencer.cpp +++ b/src/Sequencer.cpp @@ -25,10 +25,13 @@ Sequencer::scenes() const void Sequencer::handleEvent(const InputEvent& evt) { + if (evt.intent == InputEvent::SetPattern && evt.asString() == m_scenes[m_idx].name) { + return; + } if (evt.intent == InputEvent::SetPattern || evt.intent == InputEvent::NextPattern || evt.intent == InputEvent::PreviousPattern) { Log.notice("Switching pattern!"); for(const char* pattern : m_scenes[m_idx].patterns) { - Log.notice("Stopping %s", pattern); + //Log.notice("Stopping %s", pattern); MainLoop::instance()->dispatch(InputEvent{InputEvent::StopThing, pattern}); } @@ -54,7 +57,7 @@ Sequencer::handleEvent(const InputEvent& evt) } for(const char* pattern : m_scenes[m_idx].patterns) { - Log.notice("Starting %s", pattern); + //Log.notice("Starting %s", pattern); MainLoop::instance()->dispatch(InputEvent{InputEvent::StartThing, pattern}); } } diff --git a/src/WiFiTask.cpp b/src/WiFiTask.cpp index 01751d0..2f819e6 100644 --- a/src/WiFiTask.cpp +++ b/src/WiFiTask.cpp @@ -17,21 +17,7 @@ WiFiTask::onStart() { Log.notice("Starting wifi..."); WiFi.mode(WIFI_STA); - int n = WiFi.scanNetworks(); - if (n == 0) { - Log.notice("No wifi found"); - } else { - for(int i = 0; i < n; ++i) { - Serial.print("WiFi: "); - Serial.println(WiFi.SSID(i)); - } - } - WiFi.mode(WIFI_STA); WiFi.begin("The Frequency", "thepasswordkenneth"); - while(WiFi.status() != WL_CONNECTED) { - Serial.print('.'); - delay(1000); - } } InputEvent @@ -42,7 +28,7 @@ WiFiTask::read() m_lastStatus = curStatus; Log.verbose("WiFi Status: %d", curStatus); if (curStatus == WL_CONNECTED) { - Log.notice("Connected!"); + Log.notice("Connected! IP address is %s", WiFi.localIP().toString().c_str()); return InputEvent{InputEvent::NetworkStatus, true}; } else if (curStatus == WL_CONNECTION_LOST || curStatus == WL_DISCONNECTED) { Log.notice("Lost wifi connection!"); diff --git a/src/colors.cpp b/src/colors.cpp index 270d7a8..7c860b8 100644 --- a/src/colors.cpp +++ b/src/colors.cpp @@ -1,6 +1,8 @@ #include "colors.h" const ColorInfo color_data[] = { +#ifdef CONFIG_NO_COLORDATA +#else { "Air Superiority Blue", { 114, 160, 193 } }, { "Alabama Crimson", { 163, 38, 56 } }, { "Alice Blue", { 240, 248, 255 } }, @@ -792,6 +794,7 @@ const ColorInfo color_data[] = { { "Yellow Orange", { 255, 174, 66 } }, { "Zaffre", { 0, 20, 168 } }, { "Zinnwaldite Brown", { 44, 22, 8 } }, +#endif {0, {0, 0, 0}}, }; diff --git a/src/firmware b/src/firmware deleted file mode 120000 index cc2dd92..0000000 --- a/src/firmware +++ /dev/null @@ -1 +0,0 @@ -firmware \ No newline at end of file diff --git a/src/main.cpp b/src/main.cpp index 640fba2..b4844fa 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -12,7 +12,6 @@ #include "Static.h" #include "Config.h" -#include "colors.h" #include "Sequencer.h" #include "LogService.h" @@ -36,6 +35,7 @@ #include "platform/particle/MDNSService.cpp" #else #include "WiFiTask.h" +#include "platform/arduino/BluetoothSerialTelemetry.h" #include "platform/arduino/MQTTTelemetry.h" #include #endif @@ -55,9 +55,6 @@ CRGB leds[HardwareConfig::MAX_LED_NUM]; Display dpy(leds, HardwareConfig::MAX_LED_NUM, Static::instance()->coordMap()); -LinearCoordinateMapping neckMap{60, 0}; -Display neckDisplay(leds, HardwareConfig::MAX_LED_NUM, &neckMap); - // Setup power management Power power; @@ -113,7 +110,7 @@ class ArduinoOTAUpdater : public BufferedInputSource { } void handleEvent(const InputEvent& evt) { - if (evt.intent == InputEvent::NetworkStatus) { + if (evt.intent == InputEvent::NetworkStatus && evt.asInt()) { Log.notice("Booting OTA"); m_online = true; ArduinoOTA.begin(); @@ -176,19 +173,17 @@ DrainAnimation drain{Task::Stopped}; Flashlight flashlight{Task::Stopped}; Sequencer sequencer{{ - {"Idle", {"Solid", "MPU5060", "Pulse", "Hackerbots", "Kieryn", "CircadianRhythm"}}, + {"Idle", {"Solid", "MPU5060", "Pulse", "IdleColors", "CircadianRhythm"}}, {"Solid", {"Solid", "MPU5060", "Pulse", "CircadianRhythm"}}, - {"Interactive", {"Drain", "CircadianRhythm"}}, + {"Interactive", {"Drain", "MPU5060", "CircadianRhythm"}}, {"Flashlight", {"Flashlight"}}, - {"Nightlight", {"Drain", "Pulse", "Noisebridge"}}, - {"Gay", {"Solid", "Pulse", "Rainbow", "Hackerbots", "Kieryn"}}, - {"Acid", {"Chimes", "Pulse", "MPU5060", "Hackerbots", "Rainbow"}}, + {"Gay", {"Solid", "Pulse", "Rainbow", "Rainbow"}}, + {"Acid", {"Chimes", "Pulse", "MPU5060", "IdleColors", "Rainbow"}}, }}; // Render all layers to the displays Renderer renderer{ - //{&dpy, &neckDisplay}, {&dpy}, { &chimes, @@ -207,29 +202,22 @@ Renderer configRenderer{ }; // Cycle some random colors -ColorSequenceInput<7> noisebridgeCycle{{colorForName("Red").rgb}, "Noisebridge", Task::Stopped}; - -ColorSequenceInput<13> kierynCycle{{ +ColorSequenceInput<9> idleCycle{{ CRGB(0, 123, 167), // Cerulean CRGB(80, 200, 120), // Emerald CRGB(207, 113, 175), // Sky Magenta -}, "Kieryn", Task::Running}; - -ColorSequenceInput<7> rainbowCycle{{ - colorForName("Red").rgb, - colorForName("Orange").rgb, - colorForName("Yellow").rgb, - colorForName("Green").rgb, - colorForName("Blue").rgb, - colorForName("Purple").rgb, - colorForName("White").rgb, -}, "Rainbow", Task::Stopped}; - -ColorSequenceInput<7> hackerbotsCycle{{ CRGB(128, 0, 128), // Purple CRGB(255, 255, 255), // White CRGB(0, 255, 255), // Cyan -}, "Hackerbots", Task::Running}; +}, "IdleColors", Task::Running}; + +ColorSequenceInput<7> rainbowCycle{{ + CRGB(255, 0, 0), // Red + CRGB(255, 127, 0), // Yellow + CRGB(0, 255, 0), // Green + CRGB(0, 0, 255), // Blue + CRGB(128, 0, 128), // Purple +}, "Rainbow", Task::Stopped}; struct ConfigInputTask: public BufferedInputSource { public: @@ -324,48 +312,53 @@ std::array schedule{{ {23, 20} }}; -uint8_t brightnessForTime(uint8_t hour, uint8_t minute) { - ScheduleEntry start = schedule.back(); - ScheduleEntry end = schedule.front(); - for(ScheduleEntry cur : schedule) { - // Find the last hour that is prior to or equal to now - if (cur.hour <= hour) { - start = cur; - } else { - break; - } - } - for(ScheduleEntry cur : schedule) { - // Find the first hour that is after now - // If no such hour exists, we should automatically wrap back to hour 0 - if (cur.hour > hour) { - end = cur; - break; - } - } - - if (start.hour > end.hour) { - end.hour += 24; - } - - uint16_t startTime = start.hour * 60; - uint16_t endTime = end.hour * 60; - uint16_t nowTime = hour * 60 + minute; - - uint16_t duration = endTime - startTime; - uint16_t curDuration = nowTime - startTime; - - uint8_t frac = ((double)curDuration / (double)duration) * 255.0; - - return lerp8by8(start.brightness, end.brightness, frac); -} - class CircadianRhythm : public InputSource { private: bool needsUpdate = true; public: CircadianRhythm() : InputSource("CircadianRhythm") {} + void onStart() { + needsUpdate = true; + } + + uint8_t brightnessForTime(uint8_t hour, uint8_t minute) const { + ScheduleEntry start = schedule.back(); + ScheduleEntry end = schedule.front(); + for(ScheduleEntry cur : schedule) { + // Find the last hour that is prior to or equal to now + if (cur.hour <= hour) { + start = cur; + } else { + break; + } + } + for(ScheduleEntry cur : schedule) { + // Find the first hour that is after now + // If no such hour exists, we should automatically wrap back to hour 0 + if (cur.hour > hour) { + end = cur; + break; + } + } + + if (start.hour > end.hour) { + end.hour += 24; + } + + uint16_t startTime = start.hour * 60; + uint16_t endTime = end.hour * 60; + uint16_t nowTime = hour * 60 + minute; + + uint16_t duration = endTime - startTime; + uint16_t curDuration = nowTime - startTime; + + uint8_t frac = map8(curDuration, 0, duration); + + return lerp8by8(start.brightness, end.brightness, frac); + } + + InputEvent read() { EVERY_N_SECONDS(60) { needsUpdate = true; @@ -390,6 +383,8 @@ STATIC_ALLOC(CircadianRhythm); // A special mainloop app for configuring hardware settings that reboots the // device when the user is finished. MainLoop configApp{{ + Static::instance(), + // Manage read/write of configuration data Static::instance(), @@ -415,9 +410,45 @@ MainLoop configApp{{ &configRenderer, }}; +TaskFunc safeModeNag([]{ + static uint8_t frame = 0; + EVERY_N_SECONDS(30) { + Log.notice("I am running in safe mode!"); + } + EVERY_N_MILLISECONDS(16) { + frame++; + for(int i = 0; i < HardwareConfig::MAX_LED_NUM; i++) { + leds[i] = CRGB(0, 0, 0); + } + for(int idx = 0; idx < 3; idx++) { + uint8_t length = beatsin8(5, 3, HardwareConfig::MAX_LED_NUM, 0, idx * 5); + for(int i = 0; i < length; i++) { + leds[i] += CRGB(scale8(5, beatsin8(5 + i * 7, 0, 255, 0, i*3)), 0, 0); + } + } + FastLED.show(); + } +}); + +MainLoop safeModeApp({ + Static::instance(), + // ESP Wifi + Static::instance(), + // System logging + Static::instance(), + // MQTT + Static::instance(), + // OTA Updates + Static::instance(), + + &safeModeNag, +}); + // Turn on, MainLoop renderbugApp{{ + Static::instance(), + // Load/update graphics configuration from EEPROM Static::instance(), @@ -431,18 +462,28 @@ MainLoop renderbugApp{{ Static::instance(), #else // ESP Wifi - Static::instance(), + //Static::instance(), +#endif + +#ifdef BOARD_ESP32 + // ESP32 Bluetooth + Static::instance(), #endif // System logging Static::instance(), +#ifdef CONFIG_MPU5060 // Hardware drivers Static::instance(), +#endif + +#ifdef CONFIG_BUTTONS Static::instance(), // Map buttons to events &keyMap, +#endif // Pattern sequencer &sequencer, @@ -451,13 +492,11 @@ MainLoop renderbugApp{{ Static::instance(), // Periodic motion input - &randomPulse, + //&randomPulse, // Periodic color inputs - &noisebridgeCycle, - &kierynCycle, + &idleCycle, &rainbowCycle, - &hackerbotsCycle, // Animations &chimes, @@ -521,8 +560,13 @@ void setup() { Log.notice(u8"💡 Starting FastLED..."); Platform::addLEDs(leds, HardwareConfig::MAX_LED_NUM); - if (Platform::bootopts.isSetup) { - Log.notice(u8"🌌 Starting Figment in configuration mode..."); + if (Platform::bootopts.isSafeMode) { + Log.notice(u8"⚠️ Starting Figment in safe mode!!!"); + runner = safeModeApp; + FastLED.showColor(CRGB(5, 0, 0)); + FastLED.show(); + } else if (Platform::bootopts.isSetup) { + Log.notice(u8"🔧 Starting Figment in configuration mode..."); runner = configApp; } else { Log.notice(u8"🌌 Starting Figment..."); @@ -537,5 +581,6 @@ void setup() { // Drop out. void loop() { + //Platform::loop(); runner.loop(); } diff --git a/src/platform/arduino/BluetoothSerialTelemetry.cpp b/src/platform/arduino/BluetoothSerialTelemetry.cpp new file mode 100644 index 0000000..0aadb59 --- /dev/null +++ b/src/platform/arduino/BluetoothSerialTelemetry.cpp @@ -0,0 +1,86 @@ +#include "BluetoothSerialTelemetry.h" +#include "../../Static.h" +#include "../../Platform.h" +#include +#include "../../inputs/Buttons.h" + +#include + +BluetoothSerialTelemetry::BluetoothSerialTelemetry() : InputSource("Bluetooth") +{ + //m_serial.setPin("0000"); + m_serial.enableSSP(); +} + +InputEvent +BluetoothSerialTelemetry::read() +{ + bool didRead = false; + while (m_serial.available()) { + didRead = true; + char charRead = m_serial.read(); + m_ringbuf.insert(charRead); + if (charRead == '*') { + static char commandBuf[32]; + size_t cmdSize = m_ringbuf.write(commandBuf); + // Overwrite the '*' character, to leave us with a complete command + commandBuf[cmdSize-1] = 0; + + //Log.notice("Bluetooth read %s", commandBuf); + + if (commandBuf[0] == 'R') { + m_color = CRGB(std::atoi(&commandBuf[1]), m_color.g, m_color.b); + return InputEvent{InputEvent::SetColor, m_color}; + } else if (commandBuf[0] == 'G') { + m_color = CRGB(m_color.r, std::atoi(&commandBuf[1]), m_color.b); + return InputEvent{InputEvent::SetColor, m_color}; + } else if (commandBuf[0] == 'B') { + m_color = CRGB(m_color.r, m_color.g, std::atoi(&commandBuf[1])); + return InputEvent{InputEvent::SetColor, m_color}; + } else if (commandBuf[0] == 'O') { + return InputEvent{InputEvent::UserInput, Buttons::Circle}; + } else if (commandBuf[0] == 'S') { + return InputEvent{InputEvent::UserInput, Buttons::Triangle}; + } else if (commandBuf[0] == 'X') { + return InputEvent{InputEvent::UserInput, Buttons::Cross}; + } else if (commandBuf[0] == '+') { + return InputEvent{InputEvent::SetPower, 1}; + } else if (commandBuf[0] == '-') { + return InputEvent{InputEvent::SetPower, 0}; + } else if (commandBuf[0] == 'p') { + return InputEvent{InputEvent::SetPattern, &commandBuf[1]}; + } else if (commandBuf[0] == 'A') { + char* axisVal = strtok(&commandBuf[1], ","); + const uint8_t accelX = std::atof(axisVal) * 10; + axisVal = strtok(NULL, ","); + const uint8_t accelY = std::atof(axisVal) * 10; + axisVal = strtok(NULL, ","); + const uint8_t accelZ = std::atof(axisVal) * 10; + 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}; + } + } + } + } + if (didRead) { + return InputEvent::NetworkActivity; + } else { + return InputEvent{}; + } +} + +void +BluetoothSerialTelemetry::onStart() +{ + Log.notice("Starting up Bluetooth..."); + if (m_serial.begin(Platform::deviceName())) { + Log.notice("Bluetooth started!"); + } else { + Log.warning("Bluetooth could not be started!"); + } +} + +STATIC_ALLOC(BluetoothSerialTelemetry); diff --git a/src/platform/arduino/BluetoothSerialTelemetry.h b/src/platform/arduino/BluetoothSerialTelemetry.h new file mode 100644 index 0000000..45a5707 --- /dev/null +++ b/src/platform/arduino/BluetoothSerialTelemetry.h @@ -0,0 +1,41 @@ +#include +#include +#include + +class BluetoothSerialTelemetry : public InputSource { + public: + BluetoothSerialTelemetry(); + void onStart() override; + InputEvent read() override; + + template + struct Averager { + std::array buf; + unsigned int idx = 0; + unsigned int count = 0; + + void add(const T &value) { + buf[idx] = value; + idx = (idx + 1) % Size; + if (count < Size) { + count += 1; + } + } + + T value() const { + if (count == 0) { + return T{}; + } + long long int sum = 0; + for(unsigned int i = 0; i < count; i++) { + sum += buf[i]; + } + return sum / count; + } + }; + private: + BluetoothSerial m_serial; + Ringbuf m_ringbuf; + CRGB m_color; + Averager m_value; +}; diff --git a/src/platform/arduino/MQTTTelemetry.cpp b/src/platform/arduino/MQTTTelemetry.cpp index 10def86..ca27810 100644 --- a/src/platform/arduino/MQTTTelemetry.cpp +++ b/src/platform/arduino/MQTTTelemetry.cpp @@ -1,107 +1,190 @@ #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; +struct MQTTDevice { + const String id; + const String name; + const String model; + const String softwareVersion; + const String manufacturer; + const String availabilityTopic; + + void toJson(const JsonObject& json) const { + json["name"] = name; + json["mdl"] = model; + json["sw"] = softwareVersion; + json["mf"] = manufacturer; + json["ids"][0] = id; + } +}; + +const String availTopic = String("renderbug/") + Platform::deviceID() + "/availability"; + +const MQTTDevice Device{ + Platform::deviceID(), + Platform::deviceName(), + Platform::model(), +#ifdef BOARD_ESP8266 + ESP.getSketchMD5(), +#else + "", +#endif + "Phong Robotics", + availTopic +}; + +struct MQTTEntity { + const MQTTDevice& device; + String name; + String entityId; + String rootTopic; + + MQTTEntity(const String& domain, const MQTTDevice& device, const String& name) : device(device), name(Platform::deviceName() + " " + name) { + entityId = String(device.id) + "-" + name; + rootTopic = String("homeassistant/") + domain + String("/renderbug/") + entityId; + } + + String configTopic() const { + return rootTopic + "/config"; + } + + String commandTopic() const { + return rootTopic + "/set"; + } + + String heartbeatTopic() const { + return String("renderbug/") + Device.id + "/heartbeat"; + } + + String statTopic() const { + return rootTopic + "/state"; + } + + bool isCommandTopic(const char* topic) const { + if (strncmp(topic, rootTopic.c_str(), rootTopic.length()) == 0) { + return strncmp(&topic[rootTopic.length()], "/set", sizeof("/set")) == 0; + } + return false; + } + + void toJson(JsonDocument& jsonBuf, bool isInteractive = true) const { + jsonBuf["~"] = rootTopic.c_str(); + jsonBuf["name"] = name; + jsonBuf["unique_id"] = entityId; + if (isInteractive) { + jsonBuf["cmd_t"] = "~/set"; + jsonBuf["ret"] = true; + jsonBuf["schema"] = "json"; + } else { + } + jsonBuf["stat_t"] = "~/state"; + jsonBuf["json_attr_t"] = heartbeatTopic(); + jsonBuf["avty_t"] = device.availabilityTopic; + device.toJson(jsonBuf.createNestedObject("dev")); + } +}; + +const MQTTEntity Lightswitch { + "light", Device, "lightswitch" +}; + +const MQTTEntity flashlightSwitch { + "switch", Device, "flashlight" +}; + +const MQTTEntity FPSSensor { + "sensor", Device, "fps" +}; MQTTTelemetry::MQTTTelemetry() : BufferedInputSource("MQTT"), - m_mqtt(PubSubClient(wifiClient)), + m_mqtt(m_wifi), m_logPrinter(this) -{} +{ + m_debugTopic = String("renderbug/") + Platform::deviceID(); +} 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("Connecting to MQTT as %s on %s...", Platform::deviceID(), Device.availabilityTopic.c_str()); + if (m_mqtt.connect(Platform::deviceID(), NULL, NULL, Device.availabilityTopic.c_str(), 0, true, "offline")) { 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; + + Lightswitch.toJson(configJson); int i = 0; for(const Sequencer::Scene& scene : m_sequencer->scenes()) { configJson["fx_list"][i++] = scene.name; } + configJson["brightness"] = true; + configJson["rgb"] = true; - 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, true); - m_mqtt.subscribe(m_cmdTopic); + + Log.verbose("Publish %s %s", Lightswitch.configTopic().c_str(), buf); + m_mqtt.publish(Lightswitch.configTopic().c_str(), (uint8_t*)buf, strlen(buf), true); + m_mqtt.subscribe(Lightswitch.commandTopic().c_str()); + + configJson.clear(); + flashlightSwitch.toJson(configJson, false); + configJson["cmd_t"] = "~/set"; + configJson["ret"] = true; + serializeJson(configJson, buf, sizeof(buf)); + m_mqtt.publish(flashlightSwitch.configTopic().c_str(), (uint8_t*)buf, strlen(buf), true); + m_mqtt.subscribe(flashlightSwitch.commandTopic().c_str()); + + configJson.clear(); + FPSSensor.toJson(configJson, false); + configJson["unit_of_meas"] = "Frames/s"; + serializeJson(configJson, buf, sizeof(buf)); + + Log.verbose("Publish %s %s", FPSSensor.configTopic().c_str(), buf); + m_mqtt.publish(FPSSensor.configTopic().c_str(), (uint8_t*)buf, strlen(buf), true); + m_mqtt.subscribe(FPSSensor.commandTopic().c_str()); + +#ifdef BOARD_ESP8266 + struct rst_info resetInfo = *ESP.getResetInfoPtr(); + if (resetInfo.reason != 0) { + char buff[200]; + sprintf(&buff[0], "Fatal exception:%d flag:%d (%s) epc1:0x%08x epc2:0x%08x epc3:0x%08x excvaddr:0x%08x depc:0x%08x", resetInfo.exccause, resetInfo.reason, (resetInfo.reason == 0 ? "DEFAULT" : resetInfo.reason == 1 ? "WDT" : resetInfo.reason == 2 ? "EXCEPTION" : resetInfo.reason == 3 ? "SOFT_WDT" : resetInfo.reason == 4 ? "SOFT_RESTART" : resetInfo.reason == 5 ? "DEEP_SLEEP_AWAKE" : resetInfo.reason == 6 ? "EXT_SYS_RST" : "???"), resetInfo.epc1, resetInfo.epc2, resetInfo.epc3, resetInfo.excvaddr, resetInfo.depc); + Log.warning("Previous crash detected! %s", buff); + } +#endif } else { Log.warning("Could not connect to MQTT"); } } else { - if (evt.intent == InputEvent::SetPower) { + String statTopic = Lightswitch.statTopic(); + if (evt.intent == InputEvent::StopThing && String(evt.asString()) == "Flashlight") { + String flashlightStatTopic = flashlightSwitch.statTopic(); + m_mqtt.publish(flashlightStatTopic.c_str(), "OFF"); + } else if (evt.intent == InputEvent::StartThing && String(evt.asString()) == "Flashlight") { + String flashlightStatTopic = flashlightSwitch.statTopic(); + m_mqtt.publish(flashlightStatTopic.c_str(), "ON"); + } else if (evt.intent == InputEvent::SetPower) { StaticJsonDocument<256> doc; char buf[256]; - doc["state"] = evt.asInt() ? "ON" : "OFF"; + m_isOn = evt.asInt() ? true : false; + doc["state"] = m_isOn ? "ON" : "OFF"; serializeJson(doc, buf, sizeof(buf)); - m_mqtt.publish(m_statTopic, buf); + m_mqtt.publish(statTopic.c_str(), buf); } else if (evt.intent == InputEvent::SetBrightness) { StaticJsonDocument<256> doc; char buf[256]; doc["brightness"] = evt.asInt(); - doc["state"] = "ON"; + doc["state"] = m_isOn ? "ON" : "OFF"; serializeJson(doc, buf, sizeof(buf)); - m_mqtt.publish(m_statTopic, buf); + m_mqtt.publish(statTopic.c_str(), buf); } else if (evt.intent == InputEvent::SetColor) { StaticJsonDocument<256> doc; char buf[256]; @@ -109,18 +192,19 @@ MQTTTelemetry::handleEventOnline(const InputEvent& evt) doc["color"]["r"] = color.r; doc["color"]["g"] = color.g; doc["color"]["b"] = color.b; - doc["state"] = "ON"; + doc["state"] = m_isOn ? "ON" : "OFF"; serializeJson(doc, buf, sizeof(buf)); - m_mqtt.publish(m_statTopic, buf); + m_mqtt.publish(statTopic.c_str(), buf); } else if (evt.intent == InputEvent::SetPattern) { StaticJsonDocument<256> doc; char buf[256]; doc["effect"] = evt.asString(); - doc["state"] = "ON"; + doc["state"] = m_isOn ? "ON" : "OFF"; serializeJson(doc, buf, sizeof(buf)); - m_mqtt.publish(m_statTopic, buf); + m_mqtt.publish(statTopic.c_str(), buf); } else if (evt.intent == InputEvent::FirmwareUpdate) { - m_mqtt.publish("renderbug/debug/firmware", "firmware update!"); + String updateTopic = m_debugTopic + "/firmware"; + m_mqtt.publish(updateTopic.c_str(), "firmware update!"); } } } @@ -132,6 +216,23 @@ MQTTTelemetry::loop() OnlineTaskMixin::loop(); } +void +MQTTTelemetry::onOnline() +{ + const IPAddress server(10, 0, 0, 2); + + m_needHeartbeat = true; + m_mqtt.setServer(server, 1883); + m_mqtt.setBufferSize(1024); + m_mqtt.setCallback(&MQTTTelemetry::s_callback); +} + +void +MQTTTelemetry::onOffline() +{ + m_mqtt.disconnect(); +} + void MQTTTelemetry::loopOnline() { @@ -142,89 +243,126 @@ MQTTTelemetry::loopOnline() 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["device_id"] = Platform::deviceID(); response["sketch_version"] = ESP.getSketchMD5(); + response["os_version"] = ESP.getSdkVersion(); + response["localip"] = WiFi.localIP().toString(); + response["pixelCount"] = Static::instance()->coordMap()->pixelCount; + response["startPixel"] = Static::instance()->coordMap()->startPixel; + response["RSSI"] = WiFi.RSSI(); + response["free_ram"] = ESP.getFreeHeap(); + response["fps"] = FastLED.getFPS(); serializeJson(response, buf, sizeof(buf)); - m_mqtt.publish(m_attrTopic, buf); - m_mqtt.publish(m_heartbeatTopic, buf); - Log.notice("Heartbeat: %s", buf); + String availTopic = m_rootTopic + "/available"; + m_mqtt.publish(Lightswitch.heartbeatTopic().c_str(), buf); + m_mqtt.publish(Device.availabilityTopic.c_str(), "online"); + //Log.notice("Heartbeat: %s", buf); + + String fpsCounter = String(FastLED.getFPS()); + m_mqtt.publish(FPSSensor.statTopic().c_str(), fpsCounter.c_str()); - 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) +MQTTTelemetry::callback(char* topic, const char* payload) { - 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}); + setEvent(InputEvent::NetworkActivity); + if (flashlightSwitch.isCommandTopic(topic)) { + if (!strncmp((char*)payload, "ON", sizeof("ON"))) { + Log.notice("Turning on flashlight"); + setEvent(InputEvent{InputEvent::SetPower, true}); + setEvent(InputEvent{InputEvent::SetPattern, "Flashlight"}); + setEvent(InputEvent{InputEvent::SetBrightness, 255}); + } else if (!strncmp((char*)payload, "OFF", sizeof("OFF"))) { + Log.notice("Turning off flashlight"); + setEvent(InputEvent{InputEvent::SetPattern, "Idle"}); } - } + } else if (Lightswitch.isCommandTopic(topic)) { + StaticJsonDocument<512> doc; + deserializeJson(doc, payload); - if (doc.containsKey("pixelCount")) { - setEvent(InputEvent{InputEvent::SetDisplayLength, (int)doc["pixelCount"]}); - } + if (doc.containsKey("state")) { + if (doc["state"] == "ON") { + Log.notice("Turning on power"); + setEvent(InputEvent{InputEvent::SetPower, true}); + } else if (doc["state"] == "OFF") { + Log.notice("Turning off power"); + setEvent(InputEvent{InputEvent::SetPattern, "Idle"}); + setEvent(InputEvent{InputEvent::SetPower, false}); + } + } - if (doc.containsKey("startPixel")) { - setEvent(InputEvent{InputEvent::SetDisplayOffset, (int)doc["startPixel"]}); - } + if (doc.containsKey("start")) { + strcpy(m_patternBuf, doc["start"].as()); + setEvent(InputEvent{InputEvent::StartThing, m_patternBuf}); + } - if (doc.containsKey("save")) { - setEvent(InputEvent{InputEvent::SaveConfigurationRequest}); - } + 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("effect")) { - strcpy(m_patternBuf, doc["effect"].as()); - setEvent(InputEvent{InputEvent::SetPattern, m_patternBuf}); - } + if (doc.containsKey("pixelCount")) { + setEvent(InputEvent{InputEvent::SetDisplayLength, (int)doc["pixelCount"]}); + } - 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("startPixel")) { + setEvent(InputEvent{InputEvent::SetDisplayOffset, (int)doc["startPixel"]}); + } - if (doc.containsKey("brightness")) { - setEvent(InputEvent{InputEvent::SetBrightness, (int)doc["brightness"]}); + if (doc.containsKey("save")) { + setEvent(InputEvent{InputEvent::SaveConfigurationRequest}); + } + + if (doc.containsKey("restart")) { +#ifdef BOARD_ESP8266 + ESP.wdtDisable(); + ESP.restart(); +#endif + } + + if (doc.containsKey("reconnect")) { + m_mqtt.disconnect(); + } + + if (doc.containsKey("ping")) { + m_needHeartbeat = true; + Log.notice("Queuing up heartbeat"); + } + + 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); + char topicBuf[128]; + char payloadBuf[512]; + strcpy(topicBuf, topic); + memcpy(payloadBuf, payload, length); + payloadBuf[std::min(sizeof(payloadBuf) - 1, length)] = 0; + Static::instance()->callback(topicBuf, payloadBuf); } STATIC_ALLOC(MQTTTelemetry); diff --git a/src/platform/arduino/MQTTTelemetry.h b/src/platform/arduino/MQTTTelemetry.h index 3bc5f8b..14fc929 100644 --- a/src/platform/arduino/MQTTTelemetry.h +++ b/src/platform/arduino/MQTTTelemetry.h @@ -6,6 +6,13 @@ #include "../../Sequencer.h" +#ifdef BOARD_ESP8266 +#include +#elif defined(BOARD_ESP32) +#include +#endif + + class MQTTTelemetry : public BufferedInputSource, OnlineTaskMixin { public: MQTTTelemetry(); @@ -22,7 +29,9 @@ class MQTTTelemetry : public BufferedInputSource, OnlineTaskMixin { if (byte == '\n') { size_t bufSize = buf.write(outBuf); outBuf[std::min(sizeof(outBuf), bufSize)] = 0; - telemetry->m_mqtt.publish(telemetry->m_logTopic, outBuf); + Serial.println(outBuf); + String logTopic = telemetry->m_debugTopic + "/log"; + telemetry->m_mqtt.publish(logTopic.c_str(), outBuf); } else { buf.insert(byte); } @@ -39,20 +48,22 @@ class MQTTTelemetry : public BufferedInputSource, OnlineTaskMixin { 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 onOnline() override; + void onOffline() override; - void callback(char* topic, byte* payload, unsigned int length); + private: + String m_rootTopic; + String m_debugTopic; + + void callback(char* topic, const char* payload); static void s_callback(char* topic, byte* payload, unsigned int length); char m_patternBuf[48]; bool m_needHeartbeat = false; + bool m_isOn = true; Sequencer *m_sequencer = 0; + WiFiClient m_wifi; PubSubClient m_mqtt; LogPrinter m_logPrinter; }; diff --git a/src/sprites/Blob.h b/src/sprites/Blob.h index 916251c..2937ec6 100644 --- a/src/sprites/Blob.h +++ b/src/sprites/Blob.h @@ -55,13 +55,16 @@ public: // Grab the physical pixel we'll start with PhysicalCoordinates startPos = map->virtualToPhysicalCoords({m_pos, 0}); PhysicalCoordinates endPos = map->virtualToPhysicalCoords({m_pos + width, 0}); - int scaledWidth = std::abs(endPos.x - startPos.x); + uint8_t scaledWidth = std::abs(endPos.x - startPos.x); //Log.notice("blob w=%d x=%d", scaledWidth, startPos.x); for(uint8_t i = 0;i < scaledWidth; i++) { // Blobs desaturate towards their tail //Log.notice("blob i=%d w=%d x=%d", i, scaledWidth, startPos.x); - CHSV blobColor(m_hue, m_saturation, quadwave8((i / (double)scaledWidth) * m_brightness)); + uint8_t scalePct = map8(i, 0, scaledWidth); + uint8_t val = lerp8by8(0, m_brightness, scalePct); + //CHSV blobColor(m_hue, m_saturation, quadwave8((i / (double)scaledWidth) * m_brightness)); + CHSV blobColor(m_hue, m_saturation, quadwave8(val)); PhysicalCoordinates pos{startPos.x + (i*m_fadeDir), 0};