diff --git a/usermods/usermod_v2_auto_save/readme.md b/usermods/usermod_v2_auto_save/readme.md new file mode 100644 index 00000000..5c835c60 --- /dev/null +++ b/usermods/usermod_v2_auto_save/readme.md @@ -0,0 +1,45 @@ +# Auto Save + +v2 Usermod to automatically save settings +to preset number AUTOSAVE_PRESET_NUM after a change to any of + +* brightness +* effect speed +* effect intensity +* mode (effect) +* palette + +but it will wait for AUTOSAVE_SETTLE_MS milliseconds, a "settle" +period in case there are other changes (any change will +extend the "settle" window). + +It will additionally load preset AUTOSAVE_PRESET_NUM at startup. +during the first `loop()`. Reasoning below. + +AutoSaveUsermod is standalone, but if FourLineDisplayUsermod is installed, it will notify the user of the saved changes. + +Note: I don't love that WLED doesn't respect the brightness of the preset being auto loaded, so the AutoSaveUsermod will set the AUTOSAVE_PRESET_NUM preset in the first loop, so brightness IS honored. This means WLED will effectively ignore Default brightness and Apply N preset at boot when the AutoSaveUsermod is installed. + +## Installation + +Copy and update the example `platformio_override.ini.sample` +from the Rotary Encoder UI usermode folder to the root directory of your particular build. +This file should be placed in the same directory as `platformio.ini`. + +### Define Your Options + +* `USERMOD_AUTO_SAVE` - define this to have this the Auto Save usermod included wled00\usermods_list.cpp +* `USERMOD_FOUR_LINE_DISLAY` - define this to have this the Four Line Display mod included wled00\usermods_list.cpp - also tells this usermod that the display is available (see the Four Line Display usermod `readme.md` for more details) +* `AUTOSAVE_SETTLE_MS` - Minimum time to wave before auto saving, defaults to 10000 (10s) +* `AUTOSAVE_PRESET_NUM` - Preset number to auto-save to, auto-load at startup from, defaults to 99 + +### PlatformIO requirements + +No special requirements. + +Note: the Four Line Display usermod requires the libraries `U8g2` and `Wire`. + +## Change Log + +2021-02 +* First public release diff --git a/usermods/usermod_v2_auto_save/usermod_v2_auto_save.h b/usermods/usermod_v2_auto_save/usermod_v2_auto_save.h new file mode 100644 index 00000000..bd7ea6d8 --- /dev/null +++ b/usermods/usermod_v2_auto_save/usermod_v2_auto_save.h @@ -0,0 +1,192 @@ +#pragma once + +#include "wled.h" + +// +// v2 Usermod to automatically save settings +// to preset number AUTOSAVE_PRESET_NUM after a change to any of +// +// * brightness +// * effect speed +// * effect intensity +// * mode (effect) +// * palette +// +// but it will wait for AUTOSAVE_SETTLE_MS milliseconds, a "settle" +// period in case there are other changes (any change will +// extend the "settle" window). +// +// It will additionally load preset AUTOSAVE_PRESET_NUM at startup. +// during the first `loop()`. Reasoning below. +// +// AutoSaveUsermod is standalone, but if FourLineDisplayUsermod +// is installed, it will notify the user of the saved changes. +// +// Note: I don't love that WLED doesn't respect the brightness +// of the preset being auto loaded, so the AutoSaveUsermod +// will set the AUTOSAVE_PRESET_NUM preset in the first loop, +// so brightness IS honored. This means WLED will effectively +// ignore Default brightness and Apply N preset at boot when +// the AutoSaveUsermod is installed. + +//How long to wait after settings change to auto-save +#ifndef AUTOSAVE_SETTLE_MS +#define AUTOSAVE_SETTLE_MS 10*1000 +#endif + +//Preset number to save to +#ifndef AUTOSAVE_PRESET_NUM +#define AUTOSAVE_PRESET_NUM 99 +#endif + +// "Auto save MM-DD HH:MM:SS" +#define PRESET_NAME_BUFFER_SIZE 25 + +class AutoSaveUsermod : public Usermod { + private: + // If we've detected the need to auto save, this will + // be non zero. + unsigned long autoSaveAfter = 0; + + char presetNameBuffer[PRESET_NAME_BUFFER_SIZE]; + + bool firstLoop = true; + + uint8_t knownBrightness = 0; + uint8_t knownEffectSpeed = 0; + uint8_t knownEffectIntensity = 0; + uint8_t knownMode = 0; + uint8_t knownPalette = 0; + +#ifdef USERMOD_FOUR_LINE_DISLAY + FourLineDisplayUsermod* display; +#endif + + public: + // gets called once at boot. Do all initialization that doesn't depend on + // network here + void setup() { +#ifdef USERMOD_FOUR_LINE_DISLAY + // This Usermod has enhanced funcionality if + // FourLineDisplayUsermod is available. + display = (FourLineDisplayUsermod*) usermods.lookup(USERMOD_ID_FOUR_LINE_DISP); +#endif + } + + // gets called every time WiFi is (re-)connected. Initialize own network + // interfaces here + void connected() {} + + /** + * Da loop. + */ + void loop() { + unsigned long now = millis(); + uint8_t currentMode = strip.getMode(); + uint8_t currentPalette = strip.getSegment(0).palette; + if (firstLoop) { + firstLoop = false; + applyPreset(AUTOSAVE_PRESET_NUM); + knownBrightness = bri; + knownEffectSpeed = effectSpeed; + knownEffectIntensity = effectIntensity; + knownMode = currentMode; + knownPalette = currentPalette; + return; + } + + unsigned long wouldAutoSaveAfter = now + AUTOSAVE_SETTLE_MS; + if (knownBrightness != bri) { + knownBrightness = bri; + autoSaveAfter = wouldAutoSaveAfter; + } else if (knownEffectSpeed != effectSpeed) { + knownEffectSpeed = effectSpeed; + autoSaveAfter = wouldAutoSaveAfter; + } else if (knownEffectIntensity != effectIntensity) { + knownEffectIntensity = effectIntensity; + autoSaveAfter = wouldAutoSaveAfter; + } else if (knownMode != currentMode) { + knownMode = currentMode; + autoSaveAfter = wouldAutoSaveAfter; + } else if (knownPalette != currentPalette) { + knownPalette = currentPalette; + autoSaveAfter = wouldAutoSaveAfter; + } + + if (autoSaveAfter && now > autoSaveAfter) { + autoSaveAfter = 0; + // Time to auto save. You may have some flickry? + saveSettings(); + displayOverlay(); + } + } + + void saveSettings() { + updateLocalTime(); + sprintf(presetNameBuffer, + "Auto save %02d-%02d %02d:%02d:%02d", + month(localTime), day(localTime), + hour(localTime), minute(localTime), second(localTime)); + savePreset(AUTOSAVE_PRESET_NUM, true, presetNameBuffer); + } + + void displayOverlay() { +#ifdef USERMOD_FOUR_LINE_DISLAY + if (display != nullptr) { + display->wakeDisplay(); + display->overlay("Settings", "Auto Saved", 1500); + } +#endif + } + + /* + * addToJsonState() can be used to add custom entries to the /json/state part of the JSON API (state object). + * Values in the state object may be modified by connected clients + */ + void addToJsonState(JsonObject& root) { + } + + /* + * readFromJsonState() can be used to receive data clients send to the /json/state part of the JSON API (state object). + * Values in the state object may be modified by connected clients + */ + void readFromJsonState(JsonObject& root) { + } + + /* + * addToConfig() can be used to add custom persistent settings to the cfg.json file in the "um" (usermod) object. + * It will be called by WLED when settings are actually saved (for example, LED settings are saved) + * If you want to force saving the current state, use serializeConfig() in your loop(). + * + * CAUTION: serializeConfig() will initiate a filesystem write operation. + * It might cause the LEDs to stutter and will cause flash wear if called too often. + * Use it sparingly and always in the loop, never in network callbacks! + * + * addToConfig() will also not yet add your setting to one of the settings pages automatically. + * To make that work you still have to add the setting to the HTML, xml.cpp and set.cpp manually. + * + * I highly recommend checking out the basics of ArduinoJson serialization and deserialization in order to use custom settings! + */ + void addToConfig(JsonObject& root) { + } + + /* + * readFromConfig() can be used to read back the custom settings you added with addToConfig(). + * This is called by WLED when settings are loaded (currently this only happens once immediately after boot) + * + * readFromConfig() is called BEFORE setup(). This means you can use your persistent values in setup() (e.g. pin assignments, buffer sizes), + * but also that if you want to write persistent values to a dynamic buffer, you'd need to allocate it here instead of in setup. + * If you don't know what that is, don't fret. It most likely doesn't affect your use case :) + */ + void readFromConfig(JsonObject& root) { + } + + /* + * getId() allows you to optionally give your V2 usermod an unique ID (please define it in const.h!). + * This could be used in the future for the system to determine whether your usermod is installed. + */ + uint16_t getId() { + return USERMOD_ID_AUTO_SAVE; + } + +}; \ No newline at end of file diff --git a/usermods/usermod_v2_four_line_display/readme.md b/usermods/usermod_v2_four_line_display/readme.md new file mode 100644 index 00000000..3198b2be --- /dev/null +++ b/usermods/usermod_v2_four_line_display/readme.md @@ -0,0 +1,39 @@ +# Rotary Encoder UI Usermod + +First, thanks to the authors of the ssd11306_i2c_oled_u8g2 mod. + +This usermod provides a four line display using either +128x32 or 128x64 OLED displays. +It's can operate independently, but starts to provide +a relatively complete on-device UI when paired with the +Rotary Encoder UI usermod. I strongly encourage you to use +them together. + +[See the pair of usermods in action](https://www.youtube.com/watch?v=tITQY80rIOA) + +## Installation + +Copy and update the example `platformio_override.ini.sample` +from the Rotary Encoder UI usermode folder to the root directory of your particular build. +This file should be placed in the same directory as `platformio.ini`. + +### Define Your Options + +* `USERMOD_FOUR_LINE_DISLAY` - define this to have this the Four Line Display mod included wled00\usermods_list.cpp - also tells Rotary Encoder usermod, if installed, that the display is available +* `FLD_PIN_SCL` - The display SCL pin, defaults to 5 +* `FLD_PIN_SDA` - The display SDA pin, defaults to 4 +* `FLIP_MODE` - Set to 0 or 1 +* `LINE_HEIGHT` - Set to 1 or 2 + +There are other `#define` values in the Usermod that might be of interest. + +### PlatformIO requirements + +This usermod requires the `U8g2` and `Wire` libraries. See the +`platformio_override.ini.sample` found in the Rotary Encoder +UI usermod folder for how to include these using `platformio_override.ini`. + +## Change Log + +2021-02 +* First public release diff --git a/usermods/usermod_v2_four_line_display/usermod_v2_four_line_display.h b/usermods/usermod_v2_four_line_display/usermod_v2_four_line_display.h new file mode 100644 index 00000000..9f471621 --- /dev/null +++ b/usermods/usermod_v2_four_line_display/usermod_v2_four_line_display.h @@ -0,0 +1,530 @@ +#pragma once + +#include "wled.h" +#include // from https://github.com/olikraus/u8g2/ + +// +// Insired by the v1 usermod: ssd1306_i2c_oled_u8g2 +// +// v2 usermod for using 128x32 or 128x64 i2c +// OLED displays to provide a four line display +// for WLED. +// +// This Usermod works best, by far, when coupled with RotaryEncoderUIUsermod. +// +// Make sure to enable NTP and set your time zone in WLED Config | Time. +// +// REQUIREMENT: You must add the following requirements to +// REQUIREMENT: "lib_deps" within platformio.ini / platformio_override.ini +// REQUIREMENT: * U8g2 (the version already in platformio.ini is fine) +// REQUIREMENT: * Wire +// + +//The SCL and SDA pins are defined here. +#ifndef FLD_PIN_SCL +#define FLD_PIN_SCL 5 +#endif + +#ifndef FLD_PIN_SDA +#define FLD_PIN_SDA 4 +#endif + +// U8X8_SSD1306_128X32_UNIVISION_HW_I2C u8x8( +// U8X8_PIN_NONE, FLD_PIN_SCL, FLD_PIN_SDA); +U8X8_SH1106_128X64_WINSTAR_HW_I2C u8x8( + U8X8_PIN_NONE, FLD_PIN_SCL, FLD_PIN_SDA); + +// Screen upside down? Change to 0 or 1 +#ifndef FLIP_MODE +#define FLIP_MODE 0 +#endif + +// LINE_HEIGHT 1 is single height, for 128x32 displays. +// LINE_HEIGHT 2 makes the 128x64 screen display at double height. +#ifndef LINE_HEIGHT +#define LINE_HEIGHT 2 +#endif + +// If you aren't also including RotaryEncoderUIUsermod +// you probably want to set both +// SLEEP_MODE_ENABLED false +// CLOCK_MODE_ENABLED false +// as you will never be able wake the display / disable the clock. +#ifdef USERMOD_ROTARY_ENCODER_UI +#ifndef SLEEP_MODE_ENABLED +#define SLEEP_MODE_ENABLED true +#endif +#ifndef CLOCK_MODE_ENABLED +#define CLOCK_MODE_ENABLED true +#endif +#else +#define SLEEP_MODE_ENABLED false +#define CLOCK_MODE_ENABLED false +#endif + +// When to time out to the clock or blank the screen +// if SLEEP_MODE_ENABLED. +#define SCREEN_TIMEOUT_MS 15*1000 + +#define TIME_INDENT 0 +#define DATE_INDENT 2 + +// Minimum time between redrawing screen in ms +#define USER_LOOP_REFRESH_RATE_MS 1000 + +#if LINE_HEIGHT == 2 +#define DRAW_STRING draw1x2String +#define DRAW_GLYPH draw1x2Glyph +#define DRAW_BIG_STRING draw2x2String +#else +#define DRAW_STRING drawString +#define DRAW_GLYPH drawGlyph +#define DRAW_BIG_STRING draw2x2String +#endif + +// Extra char (+1) for null +#define LINE_BUFFER_SIZE 16+1 +#define FLD_LINE_3_BRIGHTNESS 0 +#define FLD_LINE_3_EFFECT_SPEED 1 +#define FLD_LINE_3_EFFECT_INTENSITY 2 +#define FLD_LINE_3_PALETTE 3 + +#if LINE_HEIGHT == 2 +#define TIME_LINE 1 +#else +#define TIME_LINE 0 +#endif + +class FourLineDisplayUsermod : public Usermod { + private: + unsigned long lastTime = 0; + + // needRedraw marks if redraw is required to prevent often redrawing. + bool needRedraw = true; + + // Next variables hold the previous known values to determine if redraw is + // required. + String knownSsid = ""; + IPAddress knownIp; + uint8_t knownBrightness = 0; + uint8_t knownEffectSpeed = 0; + uint8_t knownEffectIntensity = 0; + uint8_t knownMode = 0; + uint8_t knownPalette = 0; + uint8_t knownMinute = 99; + uint8_t knownHour = 99; + + bool displayTurnedOff = false; + long lastUpdate = 0; + long lastRedraw = 0; + long overlayUntil = 0; + byte lineThreeType = FLD_LINE_3_BRIGHTNESS; + // Set to 2 or 3 to mark lines 2 or 3. Other values ignored. + byte markLineNum = 0; + + char lineBuffer[LINE_BUFFER_SIZE]; + + // If display does not work or looks corrupted check the + // constructor reference: + // https://github.com/olikraus/u8g2/wiki/u8x8setupcpp + // or check the gallery: + // https://github.com/olikraus/u8g2/wiki/gallery + public: + + // gets called once at boot. Do all initialization that doesn't depend on + // network here + void setup() { + u8x8.begin(); + u8x8.setFlipMode(FLIP_MODE); + u8x8.setPowerSave(0); + u8x8.setContrast(10); //Contrast setup will help to preserve OLED lifetime. In case OLED need to be brighter increase number up to 255 + u8x8.setFont(u8x8_font_chroma48medium8_r); + u8x8.DRAW_STRING(0, 0*LINE_HEIGHT, "Loading..."); + } + + // gets called every time WiFi is (re-)connected. Initialize own network + // interfaces here + void connected() {} + + /** + * Da loop. + */ + void loop() { + if (millis() - lastUpdate < USER_LOOP_REFRESH_RATE_MS) { + return; + } + lastUpdate = millis(); + + redraw(false); + } + + /** + * Redraw the screen (but only if things have changed + * or if forceRedraw). + */ + void redraw(bool forceRedraw) { + if (overlayUntil > 0) { + if (millis() >= overlayUntil) { + // Time to display the overlay has elapsed. + overlayUntil = 0; + forceRedraw = true; + } + else { + // We are still displaying the overlay + // Don't redraw. + return; + } + } + + // Check if values which are shown on display changed from the last time. + if (forceRedraw) { + needRedraw = true; + } else if (((apActive) ? String(apSSID) : WiFi.SSID()) != knownSsid) { + needRedraw = true; + } else if (knownIp != (apActive ? IPAddress(4, 3, 2, 1) : WiFi.localIP())) { + needRedraw = true; + } else if (knownBrightness != bri) { + needRedraw = true; + } else if (knownEffectSpeed != effectSpeed) { + needRedraw = true; + } else if (knownEffectIntensity != effectIntensity) { + needRedraw = true; + } else if (knownMode != strip.getMode()) { + needRedraw = true; + } else if (knownPalette != strip.getSegment(0).palette) { + needRedraw = true; + } + + if (!needRedraw) { + // Nothing to change. + // Turn off display after 3 minutes with no change. + if(SLEEP_MODE_ENABLED && !displayTurnedOff && + (millis() - lastRedraw > SCREEN_TIMEOUT_MS)) { + // We will still check if there is a change in redraw() + // and turn it back on if it changed. + sleepOrClock(true); + } + else if (displayTurnedOff && CLOCK_MODE_ENABLED) { + showTime(); + } + return; + } + needRedraw = false; + lastRedraw = millis(); + + if (displayTurnedOff) + { + // Turn the display back on + sleepOrClock(false); + } + + // Update last known values. + #if defined(ESP8266) + knownSsid = apActive ? WiFi.softAPSSID() : WiFi.SSID(); + #else + knownSsid = WiFi.SSID(); + #endif + knownIp = apActive ? IPAddress(4, 3, 2, 1) : WiFi.localIP(); + knownBrightness = bri; + knownMode = strip.getMode(); + knownPalette = strip.getSegment(0).palette; + knownEffectSpeed = effectSpeed; + knownEffectIntensity = effectIntensity; + + // Do the actual drawing + u8x8.clear(); + u8x8.setFont(u8x8_font_chroma48medium8_r); + + // First row with Wifi name + String ssidString = knownSsid.substring(0, u8x8.getCols() > 1 ? u8x8.getCols() - 2 : 0); + u8x8.DRAW_STRING(1, 0*LINE_HEIGHT, ssidString.c_str()); + // Print `~` char to indicate that SSID is longer, than owr dicplay + if (knownSsid.length() > u8x8.getCols()) { + u8x8.DRAW_STRING(u8x8.getCols() - 1, 0*LINE_HEIGHT, "~"); + } + + // Second row with IP or Psssword + // Print password in AP mode and if led is OFF. + if (apActive && bri == 0) { + u8x8.DRAW_STRING(1, 1*LINE_HEIGHT, apPass); + } + else { + String ipString = knownIp.toString(); + u8x8.DRAW_STRING(1, 1*LINE_HEIGHT, ipString.c_str()); + } + + // Third row with mode name + showCurrentEffectOrPalette(JSON_mode_names, 2, knownMode); + + switch(lineThreeType) { + case FLD_LINE_3_BRIGHTNESS: + sprintf(lineBuffer, "Brightness %d", bri); + u8x8.DRAW_STRING(1, 3*LINE_HEIGHT, lineBuffer); + break; + case FLD_LINE_3_EFFECT_SPEED: + sprintf(lineBuffer, "FX Speed %d", effectSpeed); + u8x8.DRAW_STRING(1, 3*LINE_HEIGHT, lineBuffer); + break; + case FLD_LINE_3_EFFECT_INTENSITY: + sprintf(lineBuffer, "FX Intense %d", effectIntensity); + u8x8.DRAW_STRING(1, 3*LINE_HEIGHT, lineBuffer); + break; + case FLD_LINE_3_PALETTE: + showCurrentEffectOrPalette(JSON_palette_names, 3, knownPalette); + break; + } + + u8x8.setFont(u8x8_font_open_iconic_arrow_1x1); + u8x8.DRAW_GLYPH(0, markLineNum*LINE_HEIGHT, 66); // arrow icon + + u8x8.setFont(u8x8_font_open_iconic_embedded_1x1); + u8x8.DRAW_GLYPH(0, 0*LINE_HEIGHT, 80); // wifi icon + u8x8.DRAW_GLYPH(0, 1*LINE_HEIGHT, 68); // home icon + } + + /** + * Display the current effect or palette (desiredEntry) + * on the appropriate line (row). + * + * TODO: Should we cache the current effect and + * TODO: palette name? This seems expensive. + */ + void showCurrentEffectOrPalette(const char json[], uint8_t row, uint8_t desiredEntry) { + uint8_t qComma = 0; + bool insideQuotes = false; + // advance past the mark for markLineNum that may exist. + uint8_t printedChars = 1; + char singleJsonSymbol; + + // Find the mode name in JSON + for (size_t i = 0; i < strlen_P(json); i++) { + singleJsonSymbol = pgm_read_byte_near(json + i); + switch (singleJsonSymbol) { + case '"': + insideQuotes = !insideQuotes; + break; + case '[': + case ']': + break; + case ',': + qComma++; + default: + if (!insideQuotes || (qComma != desiredEntry)) { + break; + } + u8x8.DRAW_GLYPH(printedChars, row * LINE_HEIGHT, singleJsonSymbol); + printedChars++; + } + if ((qComma > desiredEntry) || (printedChars > u8x8.getCols() - 2)) { + break; + } + } + } + + /** + * If there screen is off or in clock is displayed, + * this will return true. This allows us to throw away + * the first input from the rotary encoder but + * to wake up the screen. + */ + bool wakeDisplay() { + if (displayTurnedOff) { + // Turn the display back on + sleepOrClock(false); + redraw(true); + return true; + } + return false; + } + + /** + * Allows you to show up to two lines as overlay for a + * period of time. + * Clears the screen and prints on the middle two lines. + */ + void overlay(const char* line1, const char *line2, long showHowLong) { + if (displayTurnedOff) { + // Turn the display back on + sleepOrClock(false); + } + + // Print the overlay + u8x8.clear(); + u8x8.setFont(u8x8_font_chroma48medium8_r); + if (line1) { + u8x8.DRAW_STRING(0, 1*LINE_HEIGHT, line1); + } + if (line2) { + u8x8.DRAW_STRING(0, 2*LINE_HEIGHT, line2); + } + overlayUntil = millis() + showHowLong; + } + + /** + * Specify what data should be defined on line 3 + * (the last line). + */ + void setLineThreeType(byte newLineThreeType) { + if (newLineThreeType == FLD_LINE_3_BRIGHTNESS || + newLineThreeType == FLD_LINE_3_EFFECT_SPEED || + newLineThreeType == FLD_LINE_3_EFFECT_INTENSITY || + newLineThreeType == FLD_LINE_3_PALETTE) { + lineThreeType = newLineThreeType; + } + else { + // Unknown value. + lineThreeType = FLD_LINE_3_BRIGHTNESS; + } + } + + /** + * Line 2 or 3 (last two lines) can be marked with an + * arrow in the first column. Pass 2 or 3 to this to + * specify which line to mark with an arrow. + * Any other values are ignored. + */ + void setMarkLine(byte newMarkLineNum) { + if (newMarkLineNum == 2 || newMarkLineNum == 3) { + markLineNum = newMarkLineNum; + } + else { + markLineNum = 0; + } + } + + /* + * addToJsonInfo() can be used to add custom entries to the /json/info part of the JSON API. + * Creating an "u" object allows you to add custom key/value pairs to the Info section of the WLED web UI. + * Below it is shown how this could be used for e.g. a light sensor + */ + /* + void addToJsonInfo(JsonObject& root) + { + int reading = 20; + //this code adds "u":{"Light":[20," lux"]} to the info object + JsonObject user = root["u"]; + if (user.isNull()) user = root.createNestedObject("u"); + + JsonArray lightArr = user.createNestedArray("Light"); //name + lightArr.add(reading); //value + lightArr.add(" lux"); //unit + } + */ + + /** + * Enable sleep (turn the display off) or clock mode. + */ + void sleepOrClock(bool enabled) { + if (enabled) { + if (CLOCK_MODE_ENABLED) { + showTime(); + } + else { + u8x8.setPowerSave(1); + } + displayTurnedOff = true; + } + else { + if (!CLOCK_MODE_ENABLED) { + u8x8.setPowerSave(0); + } + displayTurnedOff = false; + } + } + + /** + * Display the current date and time in large characters + * on the middle rows. Based 24 or 12 hour depending on + * the useAMPM configuration. + */ + void showTime() { + updateLocalTime(); + byte minuteCurrent = minute(localTime); + byte hourCurrent = hour(localTime); + if (knownMinute == minuteCurrent && knownHour == hourCurrent) { + // Time hasn't changed. + return; + } + knownMinute = minuteCurrent; + knownHour = hourCurrent; + + u8x8.clear(); + u8x8.setFont(u8x8_font_chroma48medium8_r); + + int currentMonth = month(localTime); + sprintf(lineBuffer, "%s %d", monthShortStr(currentMonth), day(localTime)); + u8x8.DRAW_BIG_STRING(DATE_INDENT, TIME_LINE*LINE_HEIGHT, lineBuffer); + + byte showHour = hourCurrent; + boolean isAM = false; + if (useAMPM) { + if (showHour == 0) { + showHour = 12; + isAM = true; + } + else if (showHour > 12) { + showHour -= 12; + isAM = false; + } + else { + isAM = true; + } + } + + sprintf(lineBuffer, "%02d:%02d %s", showHour, minuteCurrent, useAMPM ? (isAM ? "AM" : "PM") : ""); + // For time, we always use LINE_HEIGHT of 2 since + // we are printing it big. + u8x8.DRAW_BIG_STRING(TIME_INDENT + (useAMPM ? 0 : 2), (TIME_LINE + 1) * 2, lineBuffer); + } + + /* + * addToJsonState() can be used to add custom entries to the /json/state part of the JSON API (state object). + * Values in the state object may be modified by connected clients + */ + void addToJsonState(JsonObject& root) { + } + + /* + * readFromJsonState() can be used to receive data clients send to the /json/state part of the JSON API (state object). + * Values in the state object may be modified by connected clients + */ + void readFromJsonState(JsonObject& root) { + } + + /* + * addToConfig() can be used to add custom persistent settings to the cfg.json file in the "um" (usermod) object. + * It will be called by WLED when settings are actually saved (for example, LED settings are saved) + * If you want to force saving the current state, use serializeConfig() in your loop(). + * + * CAUTION: serializeConfig() will initiate a filesystem write operation. + * It might cause the LEDs to stutter and will cause flash wear if called too often. + * Use it sparingly and always in the loop, never in network callbacks! + * + * addToConfig() will also not yet add your setting to one of the settings pages automatically. + * To make that work you still have to add the setting to the HTML, xml.cpp and set.cpp manually. + * + * I highly recommend checking out the basics of ArduinoJson serialization and deserialization in order to use custom settings! + */ + void addToConfig(JsonObject& root) { + } + + /* + * readFromConfig() can be used to read back the custom settings you added with addToConfig(). + * This is called by WLED when settings are loaded (currently this only happens once immediately after boot) + * + * readFromConfig() is called BEFORE setup(). This means you can use your persistent values in setup() (e.g. pin assignments, buffer sizes), + * but also that if you want to write persistent values to a dynamic buffer, you'd need to allocate it here instead of in setup. + * If you don't know what that is, don't fret. It most likely doesn't affect your use case :) + */ + void readFromConfig(JsonObject& root) { + } + + /* + * getId() allows you to optionally give your V2 usermod an unique ID (please define it in const.h!). + * This could be used in the future for the system to determine whether your usermod is installed. + */ + uint16_t getId() { + return USERMOD_ID_FOUR_LINE_DISP; + } + +}; \ No newline at end of file diff --git a/usermods/usermod_v2_rotary_encoder_ui/platformio_override.ini.sample b/usermods/usermod_v2_rotary_encoder_ui/platformio_override.ini.sample new file mode 100644 index 00000000..485d67f6 --- /dev/null +++ b/usermods/usermod_v2_rotary_encoder_ui/platformio_override.ini.sample @@ -0,0 +1,46 @@ +[platformio] +default_envs = d1_mini +; default_envs = esp32dev + +[env:esp32dev] +board = esp32dev +platform = espressif32@2.0 +build_unflags = ${common.build_unflags} +build_flags = + ${common.build_flags_esp32} + -D USERMOD_FOUR_LINE_DISLAY -D FLD_PIN_SCL=22 -D FLD_PIN_SDA=21 + -D USERMOD_ROTARY_ENCODER_UI -D ENCODER_DT_PIN=18 -D ENCODER_CLK_PIN=5 -D ENCODER_SW_PIN=19 + -D USERMOD_AUTO_SAVE -D AUTOSAVE_PRESET_NUM=1 + -D LEDPIN=16 -D BTNPIN=13 +upload_speed = 460800 +lib_ignore = + ESPAsyncTCP + ESPAsyncUDP + +[env:d1_mini] +board = d1_mini +platform = ${common.platform_wled_default} +platform_packages = ${common.platform_packages} +upload_speed = 460800 +board_build.ldscript = ${common.ldscript_4m1m} +build_unflags = ${common.build_unflags} +build_flags = + ${common.build_flags_esp8266} + -D USERMOD_FOUR_LINE_DISLAY -D FLD_PIN_SCL=5 -D FLD_PIN_SDA=4 + -D USERMOD_ROTARY_ENCODER_UI -D ENCODER_DT_PIN=12 -D ENCODER_CLK_PIN=14 -D ENCODER_SW_PIN=13 + -D USERMOD_AUTO_SAVE -D AUTOSAVE_PRESET_NUM=1 + -D LEDPIN=3 -D BTNPIN=0 +monitor_filters = esp8266_exception_decoder + +[env] +lib_deps = + fastled/FastLED @ 3.3.2 + NeoPixelBus @ 2.6.0 + ESPAsyncTCP @ 1.2.0 + ESPAsyncUDP + AsyncTCP @ 1.0.3 + IRremoteESP8266 @ 2.7.3 + https://github.com/lorol/LITTLEFS.git + https://github.com/Aircoookie/ESPAsyncWebServer.git @ ~2.0.0 + U8g2@~2.27.2 + Wire diff --git a/usermods/usermod_v2_rotary_encoder_ui/readme.md b/usermods/usermod_v2_rotary_encoder_ui/readme.md new file mode 100644 index 00000000..4477590b --- /dev/null +++ b/usermods/usermod_v2_rotary_encoder_ui/readme.md @@ -0,0 +1,33 @@ +# Rotary Encoder UI Usermod + +First, thanks to the authors of other Rotary Encoder usermods. + +This usermod starts to provide a relatively complete on-device +UI when paired with the Four Line Display usermod. I strongly +encourage you to try them together. + +[See the pair of usermods in action](https://www.youtube.com/watch?v=tITQY80rIOA) + +## Installation + +Copy and update the example `platformio_override.ini.sample` to the root directory of your particular build. +This file should be placed in the same directory as `platformio.ini`. + +### Define Your Options + +* `USERMOD_ROTARY_ENCODER_UI` - define this to have this user mod included wled00\usermods_list.cpp +* `USERMOD_FOUR_LINE_DISLAY` - define this to have this the Four Line Display mod included wled00\usermods_list.cpp - also tells this usermod that the display is available (see the Four Line Display usermod `readme.md` for more details) +* `ENCODER_DT_PIN` - The encoders DT pin, defaults to 12 +* `ENCODER_CLK_PIN` - The encoders CLK pin, defaults to 14 +* `ENCODER_SW_PIN` - The encoders SW pin, defaults to 13 + +### PlatformIO requirements + +No special requirements. + +Note: the Four Line Display usermod requires the libraries `U8g2` and `Wire`. + +## Change Log + +2021-02 +* First public release diff --git a/usermods/usermod_v2_rotary_encoder_ui/usermod_v2_rotary_encoder_ui.h b/usermods/usermod_v2_rotary_encoder_ui/usermod_v2_rotary_encoder_ui.h new file mode 100644 index 00000000..59cabfc4 --- /dev/null +++ b/usermods/usermod_v2_rotary_encoder_ui/usermod_v2_rotary_encoder_ui.h @@ -0,0 +1,420 @@ +#pragma once + +#include "wled.h" + +// +// Inspired by the v1 usermods +// * rotary_encoder_change_brightness +// * rotary_encoder_change_effect +// +// v2 usermod that provides a rotary encoder-based UI. +// +// This Usermod works best coupled with FourLineDisplayUsermod. +// +// This usermod allows you to control: +// +// * Brightness +// * Selected Effect +// * Effect Speed +// * Effect Intensity +// * Palette +// +// Change between modes by pressing a button. +// + +#ifndef ENCODER_DT_PIN +#define ENCODER_DT_PIN 12 +#endif + +#ifndef ENCODER_CLK_PIN +#define ENCODER_CLK_PIN 14 +#endif + +#ifndef ENCODER_SW_PIN +#define ENCODER_SW_PIN 13 +#endif + +#ifndef USERMOD_FOUR_LINE_DISLAY +// These constants won't be defined if we aren't using FourLineDisplay. +#define FLD_LINE_3_BRIGHTNESS 0 +#define FLD_LINE_3_EFFECT_SPEED 0 +#define FLD_LINE_3_EFFECT_INTENSITY 0 +#define FLD_LINE_3_PALETTE 0 +#endif + +// The last UI state +#define LAST_UI_STATE 4 + +/** + * Array of mode indexes in alphabetical order. + * Should be ordered from JSON_mode_names array in FX.h. + * + * NOTE: If JSON_mode_names changes, this will need to be updated. + */ +const byte modes_alpha_order[] = { + 0, 27, 38, 115, 1, 26, 91, 68, 2, 88, 102, 114, 28, 31, 32, + 30, 29, 111, 52, 34, 8, 74, 67, 112, 18, 19, 96, 7, 117, 12, + 69, 66, 45, 42, 90, 89, 110, 87, 46, 53, 82, 100, 58, 64, 75, + 41, 57, 47, 44, 76, 77, 59, 70, 71, 72, 73, 107, 62, 101, 65, + 98, 105, 109, 97, 48, 49, 95, 63, 78, 43, 9, 33, 5, 79, 99, + 15, 37, 16, 10, 11, 40, 60, 108, 92, 93, 94, 103, 83, 84, 20, + 21, 22, 85, 86, 39, 61, 23, 25, 24, 104, 6, 36, 13, 14, 35, + 54, 56, 55, 116, 17, 81, 80, 106, 51, 50, 113, 3, 4 }; + +/** + * Array of palette indexes in alphabetical order. + * Should be ordered from JSON_palette_names array in FX.h. + * + * NOTE: If JSON_palette_names changes, this will need to be updated. + */ +const byte palettes_alpha_order[] = { + 0, 1, 2, 3, 4, 5, 18, 46, 51, 50, 55, 39, 26, 22, 15, + 48, 52, 53, 7, 37, 24, 30, 35, 10, 32, 28, 29, 36, 31, + 25, 8, 38, 40, 41, 9, 44, 47, 6, 20, 11, 12, 16, 33, + 14, 49, 27, 19, 13, 21, 54, 34, 45, 23, 43, 17, 42 }; + +class RotaryEncoderUIUsermod : public Usermod { +private: + int fadeAmount = 10; // Amount to change every step (brightness) + unsigned long currentTime; + unsigned long loopTime; + const int pinA = ENCODER_DT_PIN; // DT from encoder + const int pinB = ENCODER_CLK_PIN; // CLK from encoder + const int pinC = ENCODER_SW_PIN; // SW from encoder + unsigned char select_state = 0; // 0: brightness, 1: effect, 2: effect speed + unsigned char button_state = HIGH; + unsigned char prev_button_state = HIGH; + +#ifdef USERMOD_FOUR_LINE_DISLAY + FourLineDisplayUsermod* display; +#else + void* display = nullptr; +#endif + unsigned char Enc_A; + unsigned char Enc_B; + unsigned char Enc_A_prev = 0; + + bool currentEffectAndPaleeteInitialized = false; + uint8_t effectCurrentIndex = 0; + uint8_t effectPaletteIndex = 0; + +public: + /* + * setup() is called once at boot. WiFi is not yet connected at this point. + * You can use it to initialize variables, sensors or similar. + */ + void setup() + { + pinMode(pinA, INPUT_PULLUP); + pinMode(pinB, INPUT_PULLUP); + pinMode(pinC, INPUT_PULLUP); + currentTime = millis(); + loopTime = currentTime; + +#ifdef USERMOD_FOUR_LINE_DISLAY + // This Usermod uses FourLineDisplayUsermod for the best experience. + // But it's optional. But you want it. + display = (FourLineDisplayUsermod*) usermods.lookup(USERMOD_ID_FOUR_LINE_DISP); + if (display != nullptr) { + display->setLineThreeType(FLD_LINE_3_BRIGHTNESS); + display->setMarkLine(3); + } +#endif + } + + /* + * connected() is called every time the WiFi is (re)connected + * Use it to initialize network interfaces + */ + void connected() + { + //Serial.println("Connected to WiFi!"); + } + + /* + * loop() is called continuously. Here you can check for events, read sensors, etc. + * + * Tips: + * 1. You can use "if (WLED_CONNECTED)" to check for a successful network connection. + * Additionally, "if (WLED_MQTT_CONNECTED)" is available to check for a connection to an MQTT broker. + * + * 2. Try to avoid using the delay() function. NEVER use delays longer than 10 milliseconds. + * Instead, use a timer check as shown here. + */ + void loop() + { + currentTime = millis(); // get the current elapsed time + + // Initialize effectCurrentIndex and effectPaletteIndex to + // current state. We do it here as (at least) effectCurrent + // is not yet initialized when setup is called. + if (!currentEffectAndPaleeteInitialized) { + findCurrentEffectAndPalette(); + } + + if (currentTime >= (loopTime + 2)) // 2ms since last check of encoder = 500Hz + { + button_state = digitalRead(pinC); + if (prev_button_state != button_state) + { + if (button_state == LOW) + { + prev_button_state = button_state; + + char newState = select_state + 1; + if (newState > LAST_UI_STATE) newState = 0; + + bool changedState = true; + if (display != nullptr) { + switch(newState) { + case 0: + changedState = changeState("Brightness", FLD_LINE_3_BRIGHTNESS, 3); + break; + case 1: + changedState = changeState("Select FX", FLD_LINE_3_EFFECT_SPEED, 2); + break; + case 2: + changedState = changeState("FX Speed", FLD_LINE_3_EFFECT_SPEED, 3); + break; + case 3: + changedState = changeState("FX Intensity", FLD_LINE_3_EFFECT_INTENSITY, 3); + break; + case 4: + changedState = changeState("Palette", FLD_LINE_3_PALETTE, 3); + break; + } + } + if (changedState) { + select_state = newState; + } + } + else + { + prev_button_state = button_state; + } + } + int Enc_A = digitalRead(pinA); // Read encoder pins + int Enc_B = digitalRead(pinB); + if ((!Enc_A) && (Enc_A_prev)) + { // A has gone from high to low + if (Enc_B == HIGH) + { // B is high so clockwise + switch(select_state) { + case 0: + changeBrightness(true); + break; + case 1: + changeEffect(true); + break; + case 2: + changeEffectSpeed(true); + break; + case 3: + changeEffectIntensity(true); + break; + case 4: + changePalette(true); + break; + } + } + else if (Enc_B == LOW) + { // B is low so counter-clockwise + switch(select_state) { + case 0: + changeBrightness(false); + break; + case 1: + changeEffect(false); + break; + case 2: + changeEffectSpeed(false); + break; + case 3: + changeEffectIntensity(false); + break; + case 4: + changePalette(false); + break; + } + } + } + Enc_A_prev = Enc_A; // Store value of A for next time + loopTime = currentTime; // Updates loopTime + } + } + + void findCurrentEffectAndPalette() { + currentEffectAndPaleeteInitialized = true; + for (uint8_t i = 0; i < strip.getModeCount(); i++) { + byte value = modes_alpha_order[i]; + if (modes_alpha_order[i] == effectCurrent) { + effectCurrentIndex = i; + break; + } + } + + for (uint8_t i = 0; i < strip.getPaletteCount(); i++) { + byte value = palettes_alpha_order[i]; + if (palettes_alpha_order[i] == strip.getSegment(0).palette) { + effectPaletteIndex = i; + break; + } + } + } + + boolean changeState(const char *stateName, byte lineThreeMode, byte markedLine) { +#ifdef USERMOD_FOUR_LINE_DISLAY + if (display != nullptr) { + if (display->wakeDisplay()) { + // Throw away wake up input + return false; + } + display->overlay("Mode change", stateName, 1500); + display->setLineThreeType(lineThreeMode); + display->setMarkLine(markedLine); + } + #endif + return true; + } + + void lampUdated() { + bool fxChanged = strip.setEffectConfig(effectCurrent, effectSpeed, effectIntensity, effectPalette); + + //call for notifier -> 0: init 1: direct change 2: button 3: notification 4: nightlight 5: other (No notification) + // 6: fx changed 7: hue 8: preset cycle 9: blynk 10: alexa + colorUpdated(NOTIFIER_CALL_MODE_DIRECT_CHANGE); + updateInterfaces(NOTIFIER_CALL_MODE_DIRECT_CHANGE); + } + + void changeBrightness(bool increase) { +#ifdef USERMOD_FOUR_LINE_DISLAY + if (display && display->wakeDisplay()) { + // Throw away wake up input + return; + } +#endif + if (increase) { + bri = (bri + fadeAmount <= 255) ? (bri + fadeAmount) : 255; + } + else { + bri = (bri - fadeAmount >= 0) ? (bri - fadeAmount) : 0; + } + lampUdated(); + } + + void changeEffect(bool increase) { +#ifdef USERMOD_FOUR_LINE_DISLAY + if (display && display->wakeDisplay()) { + // Throw away wake up input + return; + } +#endif + if (increase) { + effectCurrentIndex = (effectCurrentIndex + 1 >= strip.getModeCount()) ? 0 : (effectCurrentIndex + 1); + } + else { + effectCurrentIndex = (effectCurrentIndex - 1 < 0) ? (strip.getModeCount() - 1) : (effectCurrentIndex - 1); + } + effectCurrent = modes_alpha_order[effectCurrentIndex]; + lampUdated(); + } + + void changeEffectSpeed(bool increase) { +#ifdef USERMOD_FOUR_LINE_DISLAY + if (display && display->wakeDisplay()) { + // Throw away wake up input + return; + } +#endif + if (increase) { + effectSpeed = (effectSpeed + fadeAmount <= 255) ? (effectSpeed + fadeAmount) : 255; + } + else { + effectSpeed = (effectSpeed - fadeAmount >= 0) ? (effectSpeed - fadeAmount) : 0; + } + lampUdated(); + } + + void changeEffectIntensity(bool increase) { +#ifdef USERMOD_FOUR_LINE_DISLAY + if (display && display->wakeDisplay()) { + // Throw away wake up input + return; + } +#endif + if (increase) { + effectIntensity = (effectIntensity + fadeAmount <= 255) ? (effectIntensity + fadeAmount) : 255; + } + else { + effectIntensity = (effectIntensity - fadeAmount >= 0) ? (effectIntensity - fadeAmount) : 0; + } + lampUdated(); + } + + void changePalette(bool increase) { +#ifdef USERMOD_FOUR_LINE_DISLAY + if (display && display->wakeDisplay()) { + // Throw away wake up input + return; + } +#endif + if (increase) { + effectPaletteIndex = (effectPaletteIndex + 1 >= strip.getPaletteCount()) ? 0 : (effectPaletteIndex + 1); + } + else { + effectPaletteIndex = (effectPaletteIndex - 1 < 0) ? (strip.getPaletteCount() - 1) : (effectPaletteIndex - 1); + } + effectPalette = palettes_alpha_order[effectPaletteIndex]; + lampUdated(); + } + + /* + * addToJsonInfo() can be used to add custom entries to the /json/info part of the JSON API. + * Creating an "u" object allows you to add custom key/value pairs to the Info section of the WLED web UI. + * Below it is shown how this could be used for e.g. a light sensor + */ + /* + void addToJsonInfo(JsonObject& root) + { + int reading = 20; + //this code adds "u":{"Light":[20," lux"]} to the info object + JsonObject user = root["u"]; + if (user.isNull()) user = root.createNestedObject("u"); + + JsonArray lightArr = user.createNestedArray("Light"); //name + lightArr.add(reading); //value + lightArr.add(" lux"); //unit + } + */ + + /* + * addToJsonState() can be used to add custom entries to the /json/state part of the JSON API (state object). + * Values in the state object may be modified by connected clients + */ + void addToJsonState(JsonObject &root) + { + //root["user0"] = userVar0; + } + + /* + * readFromJsonState() can be used to receive data clients send to the /json/state part of the JSON API (state object). + * Values in the state object may be modified by connected clients + */ + void readFromJsonState(JsonObject &root) + { + userVar0 = root["user0"] | userVar0; //if "user0" key exists in JSON, update, else keep old value + //if (root["bri"] == 255) Serial.println(F("Don't burn down your garage!")); + } + + /* + * getId() allows you to optionally give your V2 usermod an unique ID (please define it in const.h!). + * This could be used in the future for the system to determine whether your usermod is installed. + */ + uint16_t getId() + { + return USERMOD_ID_ROTARY_ENC_UI; + } + + //More methods can be added in the future, this example will then be extended. + //Your usermod will remain compatible as it does not need to implement all methods from the Usermod base class! +}; diff --git a/wled00/const.h b/wled00/const.h index a7a155b6..d2cf05a3 100644 --- a/wled00/const.h +++ b/wled00/const.h @@ -21,6 +21,9 @@ #define USERMOD_ID_FIXNETSERVICES 4 //Usermod "usermod_Fix_unreachable_netservices.h" #define USERMOD_ID_PIRSWITCH 5 //Usermod "usermod_PIR_sensor_switch.h" #define USERMOD_ID_IMU 6 //Usermod "usermod_mpu6050_imu.h" +#define USERMOD_ID_FOUR_LINE_DISP 7 //Usermod "usermod_v2_four_line_display.h +#define USERMOD_ID_ROTARY_ENC_UI 8 //Usermod "usermod_v2_rotary_encoder_ui.h" +#define USERMOD_ID_AUTO_SAVE 9 //Usermod "usermod_v2_auto_save.h" //Access point behavior #define AP_BEHAVIOR_BOOT_NO_CONN 0 //Open AP when no connection after boot diff --git a/wled00/fcn_declare.h b/wled00/fcn_declare.h index c789211b..9d053e27 100644 --- a/wled00/fcn_declare.h +++ b/wled00/fcn_declare.h @@ -221,6 +221,7 @@ class UsermodManager { void readFromConfig(JsonObject& obj); bool add(Usermod* um); + Usermod* lookup(uint16_t mod_id); byte getModCount(); }; diff --git a/wled00/um_manager.cpp b/wled00/um_manager.cpp index cec7d613..51fcd0ec 100644 --- a/wled00/um_manager.cpp +++ b/wled00/um_manager.cpp @@ -15,6 +15,18 @@ void UsermodManager::readFromJsonState(JsonObject& obj) { for (byte i = 0; i < n void UsermodManager::addToConfig(JsonObject& obj) { for (byte i = 0; i < numMods; i++) ums[i]->addToConfig(obj); } void UsermodManager::readFromConfig(JsonObject& obj) { for (byte i = 0; i < numMods; i++) ums[i]->readFromConfig(obj); } +/* + * Enables usermods to lookup another Usermod. + */ +Usermod* UsermodManager::lookup(uint16_t mod_id) { + for (byte i = 0; i < numMods; i++) { + if (ums[i]->getId() == mod_id) { + return ums[i]; + } + } + return nullptr; +} + bool UsermodManager::add(Usermod* um) { if (numMods >= WLED_MAX_USERMODS || um == nullptr) return false; diff --git a/wled00/usermods_list.cpp b/wled00/usermods_list.cpp index c5f106b0..bfff2d77 100644 --- a/wled00/usermods_list.cpp +++ b/wled00/usermods_list.cpp @@ -21,6 +21,16 @@ #include "usermod_v2_SensorsToMqtt.h" #endif +#ifdef USERMOD_FOUR_LINE_DISLAY +#include "../usermods/usermod_v2_four_line_display/usermod_v2_four_line_display.h" +#endif +#ifdef USERMOD_ROTARY_ENCODER_UI +#include "../usermods/usermod_v2_rotary_encoder_ui/usermod_v2_rotary_encoder_ui.h" +#endif +#ifdef USERMOD_AUTO_SAVE +#include "../usermods/usermod_v2_auto_save/usermod_v2_auto_save.h" +#endif + void registerUsermods() { /* @@ -39,4 +49,14 @@ void registerUsermods() #ifdef USERMOD_SENSORSTOMQTT usermods.add(new UserMod_SensorsToMQTT()); #endif + +#ifdef USERMOD_FOUR_LINE_DISLAY + usermods.add(new FourLineDisplayUsermod()); +#endif +#ifdef USERMOD_ROTARY_ENCODER_UI + usermods.add(new RotaryEncoderUIUsermod()); +#endif +#ifdef USERMOD_AUTO_SAVE + usermods.add(new AutoSaveUsermod()); +#endif } \ No newline at end of file