Repository: wled/WLED Branch: main Commit: a7212642058b Files: 490 Total size: 4.6 MB Directory structure: gitextract_clh_9b3z/ ├── .devcontainer/ │ ├── Dockerfile │ └── devcontainer.json ├── .envrc ├── .github/ │ ├── FUNDING.yml │ ├── ISSUE_TEMPLATE/ │ │ ├── bug.yml │ │ ├── config.yml │ │ └── feature_request.md │ ├── copilot-instructions.md │ └── workflows/ │ ├── build.yml │ ├── nightly.yml │ ├── pr-merge.yaml │ ├── release.yml │ ├── stale.yml │ ├── test.yaml │ ├── usermods.yml │ └── wled-ci.yml ├── .gitignore ├── .gitpod.Dockerfile ├── .gitpod.yml ├── .nvmrc ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── boards/ │ ├── adafruit_matrixportal_esp32s3_wled.json │ ├── lilygo-t7-s3.json │ └── lolin_s3_mini.json ├── images/ │ └── Readme.md ├── include/ │ └── README ├── lib/ │ ├── ESP8266PWM/ │ │ └── src/ │ │ └── core_esp8266_waveform_phase.cpp │ ├── NeoESP32RmtHI/ │ │ ├── include/ │ │ │ └── NeoEsp32RmtHIMethod.h │ │ ├── library.json │ │ └── src/ │ │ ├── NeoEsp32RmtHI.S │ │ └── NeoEsp32RmtHIMethod.cpp │ └── README ├── package.json ├── pio-scripts/ │ ├── build_ui.py │ ├── dynarray.py │ ├── load_usermods.py │ ├── obj-dump.py │ ├── output_bins.py │ ├── set_metadata.py │ ├── strip-floats.py │ ├── user_config_copy.py │ └── validate_modules.py ├── platformio.ini ├── platformio_override.sample.ini ├── readme.md ├── requirements.in ├── requirements.txt ├── test/ │ └── README ├── tools/ │ ├── AutoCubeMap.xlsx │ ├── WLED_ESP32-wrover_4MB.csv │ ├── WLED_ESP32_16MB.csv │ ├── WLED_ESP32_16MB_9MB_FS.csv │ ├── WLED_ESP32_2MB_noOTA.csv │ ├── WLED_ESP32_32MB.csv │ ├── WLED_ESP32_4MB_1MB_FS.csv │ ├── WLED_ESP32_4MB_256KB_FS.csv │ ├── WLED_ESP32_4MB_512KB_FS.csv │ ├── WLED_ESP32_4MB_700k_FS.csv │ ├── WLED_ESP32_8MB.csv │ ├── all_xml.sh │ ├── cdata-test.js │ ├── cdata.js │ ├── dynarray_espressif32.ld │ ├── fps_test.htm │ ├── json_test.htm │ ├── multi-update.cmd │ ├── multi-update.sh │ ├── partitions-16MB_spiffs-tinyuf2.csv │ ├── partitions-4MB_spiffs-tinyuf2.csv │ ├── partitions-8MB_spiffs-tinyuf2.csv │ ├── stress_test.sh │ ├── udp_test.py │ └── wled-tools ├── usermods/ │ ├── ADS1115_v2/ │ │ ├── ADS1115_v2.cpp │ │ ├── ChannelSettings.h │ │ ├── library.json │ │ └── readme.md │ ├── AHT10_v2/ │ │ ├── AHT10_v2.cpp │ │ ├── README.md │ │ └── library.json │ ├── Analog_Clock/ │ │ ├── Analog_Clock.cpp │ │ └── library.json │ ├── Animated_Staircase/ │ │ ├── Animated_Staircase.cpp │ │ ├── README.md │ │ └── library.json │ ├── Artemis_reciever/ │ │ ├── readme.md │ │ └── usermod.cpp │ ├── BH1750_v2/ │ │ ├── BH1750_v2.cpp │ │ ├── BH1750_v2.h │ │ ├── library.json │ │ └── readme.md │ ├── BME280_v2/ │ │ ├── BME280_v2.cpp │ │ ├── README.md │ │ └── library.json │ ├── BME68X_v2/ │ │ ├── BME68X_v2.cpp │ │ ├── README.md │ │ └── library.json │ ├── Battery/ │ │ ├── Battery.cpp │ │ ├── UMBattery.h │ │ ├── battery_defaults.h │ │ ├── library.json │ │ ├── readme.md │ │ └── types/ │ │ ├── LionUMBattery.h │ │ ├── LipoUMBattery.h │ │ └── UnkownUMBattery.h │ ├── Cronixie/ │ │ ├── Cronixie.cpp │ │ ├── library.json │ │ └── readme.md │ ├── DHT/ │ │ ├── DHT.cpp │ │ ├── library.json │ │ └── readme.md │ ├── EXAMPLE/ │ │ ├── library.json │ │ ├── readme.md │ │ └── usermod_v2_example.cpp │ ├── EleksTube_IPS/ │ │ ├── ChipSelect.h │ │ ├── EleksTube_IPS.cpp │ │ ├── Hardware.h │ │ ├── TFTs.h │ │ ├── User_Setup.h │ │ ├── library.json.disabled │ │ └── readme.md │ ├── Enclosure_with_OLED_temp_ESP07/ │ │ ├── assets/ │ │ │ └── readme.md │ │ ├── readme.md │ │ ├── usermod.cpp │ │ └── usermod_bme280.cpp │ ├── Fix_unreachable_netservices_v2/ │ │ ├── library.json │ │ ├── readme.md │ │ └── usermod_Fix_unreachable_netservices.cpp │ ├── INA226_v2/ │ │ ├── INA226_v2.cpp │ │ ├── README.md │ │ └── library.json │ ├── Internal_Temperature_v2/ │ │ ├── Internal_Temperature_v2.cpp │ │ ├── library.json │ │ └── readme.md │ ├── JSON_IR_remote/ │ │ ├── 21-key_ir.json │ │ ├── 24-key_ir.json │ │ ├── 32-key_ir.json │ │ ├── 40-key-black_ir.json │ │ ├── 40-key-blue_ir.json │ │ ├── 44-key_ir.json │ │ ├── 6-key_ir.json │ │ ├── 9-key_ir.json │ │ ├── IR_Remote_Codes.xlsx │ │ ├── ir_json_maker.py │ │ └── readme.md │ ├── LD2410_v2/ │ │ ├── LD2410_v2.cpp │ │ ├── library.json │ │ └── readme.md │ ├── LDR_Dusk_Dawn_v2/ │ │ ├── LDR_Dusk_Dawn_v2.cpp │ │ ├── README.md │ │ └── library.json │ ├── MAX17048_v2/ │ │ ├── MAX17048_v2.cpp │ │ ├── library.json │ │ └── readme.md │ ├── MY9291/ │ │ ├── MY9291.cpp │ │ ├── MY92xx.h │ │ └── library.json │ ├── PIR_sensor_switch/ │ │ ├── PIR_Highlight_Standby │ │ ├── PIR_sensor_switch.cpp │ │ ├── library.json │ │ └── readme.md │ ├── PS_Comet/ │ │ ├── PS_Comet.cpp │ │ ├── README.md │ │ └── library.json │ ├── PWM_fan/ │ │ ├── PWM_fan.cpp │ │ ├── library.json │ │ ├── readme.md │ │ └── setup_deps.py │ ├── RTC/ │ │ ├── RTC.cpp │ │ ├── library.json │ │ └── readme.md │ ├── RelayBlinds/ │ │ ├── index.htm │ │ ├── presets.json │ │ ├── readme.md │ │ └── usermod.cpp │ ├── SN_Photoresistor/ │ │ ├── SN_Photoresistor.cpp │ │ ├── SN_Photoresistor.h │ │ ├── library.json │ │ └── readme.md │ ├── ST7789_display/ │ │ ├── README.md │ │ ├── ST7789_display.cpp │ │ └── library.json.disabled │ ├── Si7021_MQTT_HA/ │ │ ├── Si7021_MQTT_HA.cpp │ │ ├── library.json │ │ └── readme.md │ ├── TTGO-T-Display/ │ │ ├── README.md │ │ └── usermod.cpp │ ├── Temperature/ │ │ ├── Temperature.cpp │ │ ├── UsermodTemperature.h │ │ ├── library.json │ │ └── readme.md │ ├── TetrisAI_v2/ │ │ ├── TetrisAI_v2.cpp │ │ ├── gridbw.h │ │ ├── gridcolor.h │ │ ├── library.json │ │ ├── pieces.h │ │ ├── rating.h │ │ ├── readme.md │ │ ├── tetrisai.h │ │ ├── tetrisaigame.h │ │ └── tetrisbag.h │ ├── VL53L0X_gestures/ │ │ ├── VL53L0X_gestures.cpp │ │ ├── library.json │ │ └── readme.md │ ├── Wemos_D1_mini+Wemos32_mini_shield/ │ │ ├── readme.md │ │ ├── usermod.cpp │ │ └── usermod_bme280.cpp │ ├── audioreactive/ │ │ ├── audio_reactive.cpp │ │ ├── audio_source.h │ │ ├── library.json │ │ ├── override_sqrt.py │ │ └── readme.md │ ├── battery_keypad_controller/ │ │ ├── README.md │ │ └── wled06_usermod.ino │ ├── boblight/ │ │ ├── boblight.cpp │ │ ├── library.json │ │ └── readme.md │ ├── buzzer/ │ │ ├── buzzer.cpp │ │ └── library.json │ ├── deep_sleep/ │ │ ├── deep_sleep.cpp │ │ ├── library.json │ │ └── readme.md │ ├── mpu6050_imu/ │ │ ├── library.json │ │ ├── mpu6050_imu.cpp │ │ ├── readme.md │ │ └── usermod_gyro_surge.h │ ├── multi_relay/ │ │ ├── library.json │ │ ├── multi_relay.cpp │ │ └── readme.md │ ├── photoresistor_sensor_mqtt_v1/ │ │ ├── README.md │ │ └── usermod.cpp │ ├── pixels_dice_tray/ │ │ ├── BLE_REQUIREMENT.md │ │ ├── README.md │ │ ├── WLED_ESP32_4MB_64KB_FS.csv │ │ ├── dice_state.h │ │ ├── generate_roll_info.py │ │ ├── led_effects.h │ │ ├── mqtt_client/ │ │ │ ├── mqtt_logger.py │ │ │ ├── mqtt_plotter.py │ │ │ └── requirements.txt │ │ ├── pixels_dice_tray.cpp │ │ ├── platformio_override.ini.sample │ │ ├── roll_info.h │ │ └── tft_menu.h │ ├── platformio_override.usermods.ini │ ├── pov_display/ │ │ ├── README.md │ │ ├── bmpimage.cpp │ │ ├── bmpimage.h │ │ ├── library.json │ │ ├── pov.cpp │ │ ├── pov.h │ │ └── pov_display.cpp │ ├── project_cars_shiftlight/ │ │ ├── readme.md │ │ └── wled06_usermod.ino │ ├── pwm_outputs/ │ │ ├── library.json │ │ ├── pwm_outputs.cpp │ │ └── readme.md │ ├── quinled-an-penta/ │ │ ├── library.json │ │ ├── quinled-an-penta.cpp │ │ └── readme.md │ ├── readme.md │ ├── rgb-rotary-encoder/ │ │ ├── library.json │ │ ├── readme.md │ │ └── rgb-rotary-encoder.cpp │ ├── rotary_encoder_change_effect/ │ │ └── wled06_usermod.ino │ ├── sd_card/ │ │ ├── library.json │ │ ├── readme.md │ │ └── sd_card.cpp │ ├── sensors_to_mqtt/ │ │ ├── library.json │ │ ├── readme.md │ │ └── sensors_to_mqtt.cpp │ ├── seven_segment_display/ │ │ ├── library.json │ │ ├── readme.md │ │ └── seven_segment_display.cpp │ ├── seven_segment_display_reloaded/ │ │ ├── library.json │ │ ├── readme.md │ │ ├── setup_deps.py │ │ └── seven_segment_display_reloaded.cpp │ ├── sht/ │ │ ├── ShtUsermod.h │ │ ├── library.json │ │ ├── readme.md │ │ └── sht.cpp │ ├── smartnest/ │ │ ├── library.json │ │ ├── readme.md │ │ └── smartnest.cpp │ ├── stairway_wipe_basic/ │ │ ├── library.json │ │ ├── readme.md │ │ └── stairway_wipe_basic.cpp │ ├── udp_name_sync/ │ │ ├── library.json │ │ └── udp_name_sync.cpp │ ├── user_fx/ │ │ ├── README.md │ │ ├── library.json │ │ └── user_fx.cpp │ ├── usermod_rotary_brightness_color/ │ │ ├── README.md │ │ ├── library.json │ │ └── usermod_rotary_brightness_color.cpp │ ├── usermod_v2_HttpPullLightControl/ │ │ ├── library.json │ │ ├── readme.md │ │ ├── usermod_v2_HttpPullLightControl.cpp │ │ └── usermod_v2_HttpPullLightControl.h │ ├── usermod_v2_RF433/ │ │ ├── library.json │ │ ├── readme.md │ │ ├── remote433.json │ │ └── usermod_v2_RF433.cpp │ ├── usermod_v2_animartrix/ │ │ ├── library.json │ │ ├── readme.md │ │ └── usermod_v2_animartrix.cpp │ ├── usermod_v2_auto_save/ │ │ ├── library.json │ │ ├── readme.md │ │ └── usermod_v2_auto_save.cpp │ ├── usermod_v2_brightness_follow_sun/ │ │ ├── README.md │ │ ├── library.json │ │ └── usermod_v2_brightness_follow_sun.cpp │ ├── usermod_v2_four_line_display_ALT/ │ │ ├── 4LD_wled_fonts.h │ │ ├── library.json │ │ ├── platformio_override.sample.ini │ │ ├── readme.md │ │ ├── usermod_v2_four_line_display.h │ │ └── usermod_v2_four_line_display_ALT.cpp │ ├── usermod_v2_klipper_percentage/ │ │ ├── library.json │ │ ├── readme.md │ │ └── usermod_v2_klipper_percentage.cpp │ ├── usermod_v2_ping_pong_clock/ │ │ ├── library.json │ │ ├── readme.md │ │ └── usermod_v2_ping_pong_clock.cpp │ ├── usermod_v2_rotary_encoder_ui_ALT/ │ │ ├── library.json │ │ ├── platformio_override.sample.ini │ │ ├── readme.md │ │ ├── setup_deps.py │ │ └── usermod_v2_rotary_encoder_ui_ALT.cpp │ ├── usermod_v2_word_clock/ │ │ ├── library.json │ │ ├── readme.md │ │ └── usermod_v2_word_clock.cpp │ ├── wireguard/ │ │ ├── library.json │ │ ├── readme.md │ │ └── wireguard.cpp │ ├── wizlights/ │ │ ├── library.json │ │ ├── readme.md │ │ └── wizlights.cpp │ └── word-clock-matrix/ │ ├── Word Clock Baffle.stl │ ├── library.json │ ├── readme.md │ └── word-clock-matrix.cpp └── wled00/ ├── FX.cpp ├── FX.h ├── FX_2Dfcn.cpp ├── FX_fcn.cpp ├── FXparticleSystem.cpp ├── FXparticleSystem.h ├── NodeStruct.h ├── alexa.cpp ├── bus_manager.cpp ├── bus_manager.h ├── bus_wrapper.h ├── button.cpp ├── cfg.cpp ├── colors.cpp ├── colors.h ├── const.h ├── data/ │ ├── 404.htm │ ├── common.js │ ├── cpal/ │ │ └── cpal.htm │ ├── dmxmap.htm │ ├── edit.htm │ ├── icons-ui/ │ │ ├── HowTo_AddNewIcons.txt │ │ ├── Read Me.txt │ │ ├── demo-files/ │ │ │ ├── demo.css │ │ │ └── demo.js │ │ ├── demo.html │ │ ├── selection.json │ │ └── style.css │ ├── index.css │ ├── index.htm │ ├── index.js │ ├── iro.js │ ├── liveview.htm │ ├── liveviewws2D.htm │ ├── msg.htm │ ├── pixart/ │ │ ├── boxdraw.js │ │ ├── getPixelValues.js │ │ ├── pixart.css │ │ ├── pixart.htm │ │ ├── pixart.js │ │ ├── site.webmanifest │ │ └── statics.js │ ├── pixelforge/ │ │ ├── omggif.js │ │ └── pixelforge.htm │ ├── pxmagic/ │ │ └── pxmagic.htm │ ├── rangetouch.js │ ├── settings.htm │ ├── settings_2D.htm │ ├── settings_dmx.htm │ ├── settings_leds.htm │ ├── settings_pin.htm │ ├── settings_pininfo.htm │ ├── settings_sec.htm │ ├── settings_sync.htm │ ├── settings_time.htm │ ├── settings_ui.htm │ ├── settings_um.htm │ ├── settings_wifi.htm │ ├── style.css │ ├── update.htm │ ├── usermod.htm │ └── welcome.htm ├── dmx_input.cpp ├── dmx_input.h ├── dmx_output.cpp ├── dynarray.h ├── e131.cpp ├── fcn_declare.h ├── file.cpp ├── hue.cpp ├── image_loader.cpp ├── improv.cpp ├── ir.cpp ├── ir_codes.h ├── json.cpp ├── led.cpp ├── lx_parser.cpp ├── mqtt.cpp ├── my_config_sample.h ├── net_debug.cpp ├── net_debug.h ├── network.cpp ├── ntp.cpp ├── ota_update.cpp ├── ota_update.h ├── overlay.cpp ├── palettes.cpp ├── pin_manager.cpp ├── pin_manager.h ├── playlist.cpp ├── presets.cpp ├── remote.cpp ├── set.cpp ├── src/ │ ├── dependencies/ │ │ ├── dmx/ │ │ │ ├── ESPDMX.cpp │ │ │ ├── ESPDMX.h │ │ │ ├── LICENSE.md │ │ │ ├── SparkFunDMX.cpp │ │ │ └── SparkFunDMX.h │ │ ├── e131/ │ │ │ ├── ESPAsyncE131.cpp │ │ │ └── ESPAsyncE131.h │ │ ├── espalexa/ │ │ │ ├── Espalexa.h │ │ │ ├── EspalexaDevice.cpp │ │ │ ├── EspalexaDevice.h │ │ │ └── LICENSE │ │ ├── json/ │ │ │ ├── ArduinoJson-v6.h │ │ │ └── AsyncJson-v6.h │ │ ├── network/ │ │ │ ├── Network.cpp │ │ │ └── Network.h │ │ ├── time/ │ │ │ ├── DS1307RTC.cpp │ │ │ ├── DS1307RTC.h │ │ │ ├── DateStrings.cpp │ │ │ ├── LICENSE.txt │ │ │ ├── Readme.txt │ │ │ ├── Time.cpp │ │ │ ├── TimeLib.h │ │ │ ├── library.json │ │ │ └── library.properties │ │ ├── timezone/ │ │ │ ├── LICENSE.md │ │ │ ├── ReadMe.md │ │ │ ├── Timezone.cpp │ │ │ └── Timezone.h │ │ ├── toki/ │ │ │ └── Toki.h │ │ └── ws2812fx/ │ │ ├── LICENSE.txt │ │ └── readme.txt │ └── font/ │ ├── console_font_4x6.h │ ├── console_font_5x12.h │ ├── console_font_5x8.h │ ├── console_font_6x8.h │ └── console_font_7x9.h ├── udp.cpp ├── um_manager.cpp ├── usermod.cpp ├── util.cpp ├── wled.cpp ├── wled.h ├── wled_ethernet.h ├── wled_main.cpp ├── wled_math.cpp ├── wled_metadata.cpp ├── wled_metadata.h ├── wled_serial.cpp ├── wled_server.cpp ├── ws.cpp └── xml.cpp ================================================ FILE CONTENTS ================================================ ================================================ FILE: .devcontainer/Dockerfile ================================================ # See here for image contents: https://github.com/microsoft/vscode-dev-containers/tree/v0.148.1/containers/python-3/.devcontainer/base.Dockerfile # [Choice] Python version: 3, 3.9, 3.8, 3.7, 3.6 ARG VARIANT="3" FROM mcr.microsoft.com/devcontainers/python:0-${VARIANT} # [Optional] If your pip requirements rarely change, uncomment this section to add them to the image. # COPY requirements.txt /tmp/pip-tmp/ # RUN pip3 --disable-pip-version-check --no-cache-dir install -r /tmp/pip-tmp/requirements.txt \ # && rm -rf /tmp/pip-tmp # [Optional] Uncomment this section to install additional OS packages. # RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ # && apt-get -y install --no-install-recommends # [Optional] Uncomment this line to install global node packages. # RUN su vscode -c "source /usr/local/share/nvm/nvm.sh && npm install -g " 2>&1 ================================================ FILE: .devcontainer/devcontainer.json ================================================ { "name": "Python 3", "build": { "dockerfile": "Dockerfile", "context": "..", "args": { // Update 'VARIANT' to pick a Python version: 3, 3.6, 3.7, 3.8, 3.9 "VARIANT": "3" } }, // To give the container access to a device serial port, you can uncomment one of the following lines. // Note: If running on Windows, you will have to do some additional steps: // https://stackoverflow.com/questions/68527888/how-can-i-use-a-usb-com-port-inside-of-a-vscode-development-container // // You can explicitly just forward the port you want to connect to. Replace `/dev/ttyACM0` with the serial port for // your device. This will only work if the device is plugged in from the start without reconnecting. Adding the // `dialout` group is needed if read/write permisions for the port are limitted to the dialout user. // "runArgs": ["--device=/dev/ttyACM0", "--group-add", "dialout"], // // Alternatively, you can give more comprehensive access to the host system. This will expose all the host devices to // the container. Adding the `dialout` group is needed if read/write permisions for the port are limitted to the // dialout user. This could allow the container to modify unrelated serial devices, which would be a similar level of // risk to running the build directly on the host. // "runArgs": ["--privileged", "-v", "/dev/bus/usb:/dev/bus/usb", "--group-add", "dialout"], "customizations": { "vscode": { "settings": { "terminal.integrated.shell.linux": "/bin/bash", "python.pythonPath": "/usr/local/bin/python", "python.linting.enabled": true, "python.linting.pylintEnabled": true, "python.formatting.autopep8Path": "/usr/local/py-utils/bin/autopep8", "python.formatting.blackPath": "/usr/local/py-utils/bin/black", "python.formatting.yapfPath": "/usr/local/py-utils/bin/yapf", "python.linting.banditPath": "/usr/local/py-utils/bin/bandit", "python.linting.flake8Path": "/usr/local/py-utils/bin/flake8", "python.linting.mypyPath": "/usr/local/py-utils/bin/mypy", "python.linting.pycodestylePath": "/usr/local/py-utils/bin/pycodestyle", "python.linting.pydocstylePath": "/usr/local/py-utils/bin/pydocstyle", "python.linting.pylintPath": "/usr/local/py-utils/bin/pylint" }, "extensions": [ "ms-python.python", "platformio.platformio-ide" ] } }, // Use 'forwardPorts' to make a list of ports inside the container available locally. // "forwardPorts": [], // Use 'postCreateCommand' to run commands after the container is created. "postCreateCommand": "bash -i -c 'nvm install && npm ci'", // Comment out connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root. "remoteUser": "vscode" } ================================================ FILE: .envrc ================================================ layout python-venv python3 ================================================ FILE: .github/FUNDING.yml ================================================ github: [DedeHai,lost-hope,willmmiles,netmindz,softhack007] custom: ['https://paypal.me/Aircoookie','https://paypal.me/blazoncek'] ================================================ FILE: .github/ISSUE_TEMPLATE/bug.yml ================================================ name: Bug Report description: File a bug report labels: ["bug"] body: - type: markdown attributes: value: | Please quickly search existing issues first before submitting a bug. - type: textarea id: what-happened attributes: label: What happened? description: A clear and concise description of what the bug is. placeholder: Tell us what the problem is. validations: required: true - type: textarea id: how-to-reproduce attributes: label: To Reproduce Bug description: Steps to reproduce the behavior, if consistently possible. placeholder: Tell us how to make the bug appear. validations: required: true - type: textarea id: expected-behavior attributes: label: Expected Behavior description: A clear and concise description of what you expected to happen. placeholder: Tell us what you expected to happen. validations: required: true - type: dropdown id: install_format attributes: label: Install Method description: How did you install WLED? options: - Binary from WLED.me - Self-Compiled validations: required: true - type: input id: version attributes: label: What version of WLED? description: |- Find this by going to ⚙️ ConfigSecurity & Updates → Scroll to Bottom. Copy and paste the rest of the line that begins “Installed version: ”, or, for older versions, the entire line after “Server message”. placeholder: "e.g. WLED 0.13.1 (build 2203150)" validations: required: true - type: dropdown id: Board attributes: label: Which microcontroller/board are you seeing the problem on? multiple: true options: - ESP8266 - ESP32 - ESP32-S3 - ESP32-S2 - ESP32-C3 - Other - ESP32-C6 (experimental) - ESP32-C5 (experimental) validations: required: true - type: textarea id: logs attributes: label: Relevant log/trace output description: Please copy and paste any relevant log output if you have it. This will be automatically formatted into code, so no need for backticks. render: shell - type: textarea attributes: label: Anything else? description: | Links? References? Anything that will give us more context about the issue you are encountering! Tip: You can attach images or log files by clicking this area to highlight it and then dragging files in. validations: required: false - type: checkboxes id: terms attributes: label: Code of Conduct description: By submitting this issue, you agree to follow our [Code of Conduct](https://github.com/wled-dev/WLED/blob/main/CODE_OF_CONDUCT.md) options: - label: I agree to follow this project's Code of Conduct required: true ================================================ FILE: .github/ISSUE_TEMPLATE/config.yml ================================================ blank_issues_enabled: false contact_links: - name: WLED Discord community url: https://discord.gg/KuqP7NE about: Please ask and answer questions and discuss setup issues here! - name: WLED community forum url: https://wled.discourse.group/ about: For issues and ideas that might need longer discussion. - name: kno.wled.ge base url: https://kno.wled.ge/basics/faq/ about: Take a look at the frequently asked questions and documentation, perhaps your question is already answered! ================================================ FILE: .github/ISSUE_TEMPLATE/feature_request.md ================================================ --- name: Feature request about: Suggest an improvement idea for WLED! title: '' labels: enhancement assignees: '' --- **Is your feature request related to a problem? Please describe.** A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] **Describe the solution you'd like** A clear and concise description of what you want to happen. **Describe alternatives you've considered** A clear and concise description of any alternative solutions or features you've considered. **Additional context** Add any other context or screenshots about the feature request here. Thank you for your ideas for making WLED better! ================================================ FILE: .github/copilot-instructions.md ================================================ # WLED - ESP32/ESP8266 LED Controller Firmware WLED is a fast and feature-rich implementation of an ESP32 and ESP8266 webserver to control NeoPixel (WS2812B, WS2811, SK6812) LEDs and SPI-based chipsets. The project consists of C++ firmware for microcontrollers and a modern web interface. Always reference these instructions first and fallback to search or bash commands only when you encounter unexpected information that does not match the info here. ## Working Effectively ### Initial Setup - Install Node.js 20+ (specified in `.nvmrc`): Check your version with `node --version` - Install dependencies: `npm ci` (takes ~5 seconds) - Install PlatformIO for hardware builds: `pip install -r requirements.txt` (takes ~60 seconds) ### Build and Test Workflow - **ALWAYS build web UI first**: `npm run build` -- takes 3 seconds. NEVER CANCEL. - **Run tests**: `npm test` -- takes 40 seconds. NEVER CANCEL. Set timeout to 2+ minutes. - **Development mode**: `npm run dev` -- monitors file changes and auto-rebuilds web UI - **Hardware firmware build**: `pio run -e [environment]` -- takes 15+ minutes. NEVER CANCEL. Set timeout to 30+ minutes. ### Build Process Details The build has two main phases: 1. **Web UI Generation** (`npm run build`): - Processes files in `wled00/data/` (HTML, CSS, JS) - Minifies and compresses web content - Generates `wled00/html_*.h` files with embedded web content - **CRITICAL**: Must be done before any hardware build 2. **Hardware Compilation** (`pio run`): - Compiles C++ firmware for various ESP32/ESP8266 targets - Common environments: `nodemcuv2`, `esp32dev`, `esp8266_2m` - List all targets: `pio run --list-targets` ## Before Finishing Work **CRITICAL: You MUST complete ALL of these steps before marking your work as complete:** 1. **Run the test suite**: `npm test` -- Set timeout to 2+ minutes. NEVER CANCEL. - All tests MUST pass - If tests fail, fix the issue before proceeding 2. **Build at least one hardware environment**: `pio run -e esp32dev` -- Set timeout to 30+ minutes. NEVER CANCEL. - Choose `esp32dev` as it's a common, representative environment - See "Hardware Compilation" section above for the full list of common environments - The build MUST complete successfully without errors - If the build fails, fix the issue before proceeding - **DO NOT skip this step** - it validates that firmware compiles with your changes 3. **For web UI changes only**: Manually test the interface - See "Manual Testing Scenarios" section below - Verify the UI loads and functions correctly **If any of these validation steps fail, you MUST fix the issues before finishing. Do NOT mark work as complete with failing builds or tests.** ## Validation and Testing ### Web UI Testing - **ALWAYS validate web UI changes manually**: - Start local server: `cd wled00/data && python3 -m http.server 8080` - Open `http://localhost:8080/index.htm` in browser - Test basic functionality: color picker, effects, settings pages - **Check for JavaScript errors** in browser console ### Code Validation - **No automated linting configured** - follow existing code style in files you edit - **Code style**: Use tabs for web files (.html/.css/.js), spaces (2 per level) for C++ files - **Language**: The repository language is English (british, american, canadian, or australian). If you find other languages, suggest a translation into English. - **C++ formatting available**: `clang-format` is installed but not in CI - **Always run tests before finishing**: `npm test` - **MANDATORY: Always run a hardware build before finishing** (see "Before Finishing Work" section below) ### Manual Testing Scenarios After making changes to web UI, always test: - **Load main interface**: Verify index.htm loads without errors - **Navigation**: Test switching between main page and settings pages - **Color controls**: Verify color picker and brightness controls work - **Effects**: Test effect selection and parameter changes - **Settings**: Test form submission and validation ## Common Tasks ### Repository Structure ``` wled00/ # Main firmware source (C++) ├── data/ # Web interface files │ ├── index.htm # Main UI │ ├── settings*.htm # Settings pages │ └── *.js/*.css # Frontend resources ├── *.cpp/*.h # Firmware source files └── html_*.h # Auto-generated embedded web files (DO NOT EDIT, DO NOT COMMIT) tools/ # Build tools (Node.js) ├── cdata.js # Web UI build script └── cdata-test.js # Test suite platformio.ini # Hardware build configuration package.json # Node.js dependencies and scripts .github/workflows/ # CI/CD pipelines ``` ### Key Files and Their Purpose - `wled00/data/index.htm` - Main web interface - `wled00/data/settings*.htm` - Configuration pages - `tools/cdata.js` - Converts web files to C++ headers - `wled00/wled.h` - Main firmware configuration - `platformio.ini` - Hardware build targets and settings ### Development Workflow (applies to agent mode only) 1. **For web UI changes**: - Edit files in `wled00/data/` - Run `npm run build` to regenerate headers - Test with local HTTP server - Run `npm test` to validate build system 2. **For firmware changes**: - Edit files in `wled00/` (but NOT `html_*.h` files) - Ensure web UI is built first (`npm run build`) - Build firmware: `pio run -e [target]` - Flash to device: `pio run -e [target] --target upload` 3. **For both web and firmware**: - Always build web UI first - Test web interface manually - Build and test firmware if making firmware changes ## Build Timing and Timeouts **IMPORTANT: Use these timeout values when running builds:** - **Web UI build** (`npm run build`): 3 seconds typical - Set timeout to 30 seconds minimum - **Test suite** (`npm test`): 40 seconds typical - Set timeout to 120 seconds (2 minutes) minimum - **Hardware builds** (`pio run -e [target]`): 15-20 minutes typical for first build - Set timeout to 1800 seconds (30 minutes) minimum - Subsequent builds are faster due to caching - First builds download toolchains and dependencies which takes significant time - **NEVER CANCEL long-running builds** - PlatformIO downloads and compilation require patience **When validating your changes before finishing, you MUST wait for the hardware build to complete successfully. Set the timeout appropriately and be patient.** ## Troubleshooting ### Common Issues - **Build fails with missing html_*.h**: Run `npm run build` first - **Web UI looks broken**: Check browser console for JavaScript errors - **PlatformIO network errors**: Try again, downloads can be flaky - **Node.js version issues**: Ensure Node.js 20+ is installed (check `.nvmrc`) ### When Things Go Wrong - **Clear generated files**: `rm -f wled00/html_*.h` then rebuild - **Force web UI rebuild**: `npm run build -- --force` or `npm run build -- -f` - **Clean PlatformIO cache**: `pio run --target clean` - **Reinstall dependencies**: `rm -rf node_modules && npm install` ## Important Notes - **Always commit source files** - **Web UI re-built is part of the platformio firmware compilation** - **do not commit generated html_*.h files** - **DO NOT edit `wled00/html_*.h` files** - they are auto-generated. If needed, modify Web UI files in `wled00/data/`. - **Test web interface manually after any web UI changes** - When reviewing a PR: the PR author does not need to update/commit generated html_*.h files - these files will be auto-generated when building the firmware binary. - If updating Web UI files in `wled00/data/`, make use of common functions availeable in `wled00/data/common.js` where possible. - **Use VS Code with PlatformIO extension for best development experience** - **Hardware builds require appropriate ESP32/ESP8266 development board** ## CI/CD Pipeline **The GitHub Actions CI workflow will:** 1. Installs Node.js and Python dependencies 2. Runs `npm test` to validate build system (MUST pass) 3. Builds web UI with `npm run build` (automatically run by PlatformIO) 4. Compiles firmware for ALL hardware targets listed in `default_envs` (MUST succeed for all) 5. Uploads build artifacts **To ensure CI success, you MUST locally:** - Run `npm test` and ensure it passes - Run `pio run -e esp32dev` (or another common environment from "Hardware Compilation" section) and ensure it completes successfully - If either fails locally, it WILL fail in CI **Match this workflow in your local development to ensure CI success. Do not mark work complete until you have validated builds locally.** ================================================ FILE: .github/workflows/build.yml ================================================ name: WLED Build # Only included into other workflows on: workflow_call: jobs: get_default_envs: name: Gather Environments runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: python-version: '3.12' cache: 'pip' - name: Install PlatformIO run: pip install -r requirements.txt - name: Get default environments id: envs run: | echo "environments=$(pio project config --json-output | jq -cr '.[0][1][0][1]')" >> $GITHUB_OUTPUT outputs: environments: ${{ steps.envs.outputs.environments }} build: name: Build Environments runs-on: ubuntu-latest needs: get_default_envs strategy: fail-fast: false matrix: environment: ${{ fromJSON(needs.get_default_envs.outputs.environments) }} steps: - uses: actions/checkout@v4 - name: Set up Node.js uses: actions/setup-node@v4 with: node-version-file: '.nvmrc' cache: 'npm' - run: | npm ci VERSION=`date +%y%m%d0` sed -i -r -e "s/define VERSION .+/define VERSION $VERSION/" wled00/wled.h - name: Cache PlatformIO uses: actions/cache@v4 with: path: | ~/.platformio/.cache ~/.buildcache build_output key: pio-${{ runner.os }}-${{ matrix.environment }}-${{ hashFiles('platformio.ini', 'pio-scripts/output_bins.py') }}-${{ hashFiles('wled00/**', 'usermods/**') }} restore-keys: pio-${{ runner.os }}-${{ matrix.environment }}-${{ hashFiles('platformio.ini', 'pio-scripts/output_bins.py') }}- - name: Set up Python uses: actions/setup-python@v5 with: python-version: '3.12' cache: 'pip' - name: Install PlatformIO run: pip install -r requirements.txt - name: Build firmware run: pio run -e ${{ matrix.environment }} - uses: actions/upload-artifact@v4 with: name: firmware-${{ matrix.environment }} path: | build_output/release/*.bin build_output/release/*_ESP02*.bin.gz testCdata: name: Test cdata.js runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Use Node.js uses: actions/setup-node@v4 with: node-version-file: '.nvmrc' cache: 'npm' - run: npm ci - run: npm test ================================================ FILE: .github/workflows/nightly.yml ================================================ name: Deploy Nightly on: # This can be used to automatically publish nightlies at UTC nighttime schedule: - cron: '0 2 * * *' # run at 2 AM UTC # This can be used to allow manually triggering nightlies from the web interface workflow_dispatch: jobs: wled_build: uses: ./.github/workflows/build.yml nightly: name: Deploy nightly runs-on: ubuntu-latest needs: wled_build steps: - name: Download artifacts uses: actions/download-artifact@v4 with: merge-multiple: true - name: Show Files run: ls -la - name: "✏️ Generate release changelog" id: changelog uses: janheinrichmerker/action-github-changelog-generator@v2.4 with: token: ${{ secrets.GITHUB_TOKEN }} sinceTag: v0.15.0 output: CHANGELOG_NIGHTLY.md # Exclude issues that were closed without resolution from changelog excludeLabels: 'stale,wontfix,duplicate,invalid,external,question,use-as-is,not_planned' - name: Update Nightly Release uses: andelf/nightly-release@main env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: tag_name: nightly name: 'Nightly Release $$' prerelease: true body_path: CHANGELOG_NIGHTLY.md files: | *.bin *.bin.gz - name: Repository Dispatch uses: peter-evans/repository-dispatch@v3 with: repository: wled/WLED-WebInstaller event-type: release-nightly token: ${{ secrets.PAT_PUBLIC }} ================================================ FILE: .github/workflows/pr-merge.yaml ================================================ name: Notify Discord on PR Merge on: workflow_dispatch: pull_request_target: types: [closed] jobs: notify: runs-on: ubuntu-latest if: github.event.pull_request.merged == true steps: - name: Get User Permission id: checkAccess uses: actions-cool/check-user-permission@v2 with: require: write username: ${{ github.triggering_actor }} env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Check User Permission if: steps.checkAccess.outputs.require-result == 'false' run: | echo "${{ github.triggering_actor }} does not have permissions on this repo." echo "Current permission level is ${{ steps.checkAccess.outputs.user-permission }}" echo "Job originally triggered by ${{ github.actor }}" exit 1 - name: Send Discord notification env: PR_NUMBER: ${{ github.event.pull_request.number }} PR_TITLE: ${{ github.event.pull_request.title }} PR_URL: ${{ github.event.pull_request.html_url }} ACTOR: ${{ github.actor }} run: | jq -n \ --arg content "Pull Request #${PR_NUMBER} \"${PR_TITLE}\" merged by ${ACTOR} ${PR_URL} . It will be included in the next nightly builds, please test" \ '{content: $content}' \ | curl -H "Content-Type: application/json" -d @- ${{ secrets.DISCORD_WEBHOOK_BETA_TESTERS }} ================================================ FILE: .github/workflows/release.yml ================================================ name: WLED Release CI on: push: tags: - '*' jobs: wled_build: uses: ./.github/workflows/build.yml release: name: Create Release runs-on: ubuntu-latest needs: wled_build steps: - uses: actions/download-artifact@v4 with: merge-multiple: true - name: "✏️ Generate release changelog" id: changelog uses: janheinrichmerker/action-github-changelog-generator@v2.4 with: token: ${{ secrets.GITHUB_TOKEN }} sinceTag: v0.15.0 maxIssues: 500 # Exclude issues that were closed without resolution from changelog excludeLabels: 'stale,wontfix,duplicate,invalid,external,question,use-as-is,not_planned' - name: Create draft release uses: softprops/action-gh-release@v1 with: body: ${{ steps.changelog.outputs.changelog }} draft: True files: | *.bin *.bin.gz ================================================ FILE: .github/workflows/stale.yml ================================================ name: 'Close stale issues and PRs' on: schedule: - cron: '0 12 * * *' workflow_dispatch: jobs: stale: runs-on: ubuntu-latest steps: - uses: actions/stale@v9 with: days-before-stale: 120 days-before-close: 7 stale-issue-label: 'stale' stale-pr-label: 'stale' exempt-issue-labels: 'pinned,keep,enhancement,confirmed' exempt-pr-labels: 'pinned,keep,enhancement,confirmed' exempt-all-milestones: true operations-per-run: 1000 stale-issue-message: > Hey! This issue has been open for quite some time without any new comments now. It will be closed automatically in a week if no further activity occurs. Thank you for using WLED! ✨ stale-pr-message: > Hey! This pull request has been open for quite some time without any new comments now. It will be closed automatically in a week if no further activity occurs. Thank you for contributing to WLED! ❤️ ================================================ FILE: .github/workflows/test.yaml ================================================ on: workflow_dispatch: jobs: dispatch: runs-on: ubuntu-latest steps: - name: Repository Dispatch uses: peter-evans/repository-dispatch@v3 with: repository: wled/WLED-WebInstaller event-type: release-nightly token: ${{ secrets.PAT_PUBLIC }} ================================================ FILE: .github/workflows/usermods.yml ================================================ name: Usermod CI on: pull_request: paths: - usermods/** jobs: get_usermod_envs: # Only run for pull requests from forks (not from branches within wled/WLED) if: github.event.pull_request.head.repo.full_name != github.repository name: Gather Usermods runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: python-version: '3.12' cache: 'pip' - name: Install PlatformIO run: pip install -r requirements.txt - name: Get default environments id: envs run: | echo "usermods=$(find usermods/ -name library.json | xargs dirname | xargs -n 1 basename | jq -R | grep -v PWM_fan | grep -v BME68X_v2| grep -v pixels_dice_tray | jq --slurp -c)" >> $GITHUB_OUTPUT outputs: usermods: ${{ steps.envs.outputs.usermods }} build: # Only run for pull requests from forks (not from branches within wled/WLED) if: github.event.pull_request.head.repo.full_name != github.repository name: Build Enviornments runs-on: ubuntu-latest needs: get_usermod_envs strategy: fail-fast: false matrix: usermod: ${{ fromJSON(needs.get_usermod_envs.outputs.usermods) }} environment: [usermods_esp32, usermods_esp32c3, usermods_esp32s2, usermods_esp32s3] steps: - uses: actions/checkout@v4 - name: Set up Node.js uses: actions/setup-node@v4 with: node-version-file: '.nvmrc' cache: 'npm' - run: npm ci - name: Cache PlatformIO uses: actions/cache@v4 with: path: | ~/.platformio/.cache ~/.buildcache build_output key: pio-${{ runner.os }}-${{ matrix.environment }}-${{ hashFiles('platformio.ini', 'pio-scripts/output_bins.py') }}-${{ hashFiles('wled00/**', 'usermods/**') }} restore-keys: pio-${{ runner.os }}-${{ matrix.environment }}-${{ hashFiles('platformio.ini', 'pio-scripts/output_bins.py') }}- - name: Set up Python uses: actions/setup-python@v5 with: python-version: '3.12' cache: 'pip' - name: Install PlatformIO run: pip install -r requirements.txt - name: Add usermods environment run: | cp -v usermods/platformio_override.usermods.ini platformio_override.ini echo >> platformio_override.ini echo "custom_usermods = ${{ matrix.usermod }}" >> platformio_override.ini cat platformio_override.ini - name: Build firmware run: pio run -e ${{ matrix.environment }} ================================================ FILE: .github/workflows/wled-ci.yml ================================================ name: WLED CI on: push: branches: - '*' pull_request: jobs: wled_build: uses: ./.github/workflows/build.yml ================================================ FILE: .gitignore ================================================ .cache .clang-format .direnv .DS_Store .idea .pio .pioenvs .piolibdeps .vscode compile_commands.json __pycache__/ /.dummy /dependencies.lock /managed_components esp01-update.sh platformio_override.ini replace_fs.py wled-update.sh /build_output/ /node_modules/ /logs/ /wled00/extLibs /wled00/LittleFS /wled00/my_config.h /wled00/Release /wled00/wled00.ino.cpp /wled00/html_*.h /wled00/js_*.h ================================================ FILE: .gitpod.Dockerfile ================================================ FROM gitpod/workspace-full USER gitpod ================================================ FILE: .gitpod.yml ================================================ tasks: - command: pip3 install -U platformio && platformio run image: file: .gitpod.Dockerfile vscode: extensions: - Atishay-Jain.All-Autocomplete - esbenp.prettier-vscode - shardulm94.trailing-spaces ================================================ FILE: .nvmrc ================================================ 20.18 ================================================ FILE: CHANGELOG.md ================================================ ## WLED changelog #### Build 2410270 - WLED 0.15.0-b7 release - Re-license the WLED project from MIT to EUPL (#4194 by @Aircoookie) - Fix alexa devices invisible/uncontrollable (#4214 by @Svennte) - Add visual expand button on hover (#4172) - Usermod: Audioreactive tuning and performance enhancements (by @softhack007) - `/json/live` (JSON live data/peek) only enabled when WebSockets are disabled - Various bugfixes and optimisations: #4179, #4215, #4219, #4222, #4223, #4224, #4228, #4230 #### Build 2410140 - WLED 0.15.0-b6 release - Added BRT timezone (#4188 by @LuisFadini) - Fixed the positioning of the "Download the latest binary" button (#4184 by @maxi4329) - Add WLED_AUTOSEGMENTS compile flag (#4183 by @PaoloTK) - New 512kB FS parition map for 4MB devices - Internal API change: Static PinManager & UsermodManager - Change in Improv chip ID and version generation - Various optimisations, bugfixes and enhancements (#4005, #4174 & #4175 by @Xevel, #4180, #4168, #4154, #4189 by @dosipod) #### Build 2409170 - UI: Introduce common.js in settings pages (size optimisation) - Add the ability to toggle the reception of palette synchronizations (#4137 by @felddy) - Usermod/FX: Temperature usermod added Temperature effect (example usermod effect by @blazoncek) - Fix AsyncWebServer version pin #### Build 2409140 - Configure different kinds of busses at compile (#4107 by @PaoloTK) - BREAKING: removes LEDPIN and DEFAULT_LED_TYPE compile overrides - Fetch LED types from Bus classes (dynamic UI) (#4129 by @netmindz, @blazoncek, @dedehai) - Temperature usermod: update OneWire to 2.3.8 (#4131 by @iammattcoleman) #### Build 2409100 - WLED 0.15.0-b5 release - Audioreactive usermod included by default in all compatible builds (including ESP8266) - Demystified some byte definitions of WiZmote ESP-NOW message (#4114 by @ChuckMash) - Update usermod "Battery" improved MQTT support (#4110 by @itCarl) - Added a usermod for interacting with BLE Pixels Dice (#4093 by @axlan) - Allow lower values for touch threshold (#4081 by @RobinMeis) - Added POV image effect usermod (#3539 by @Liliputech) - Remove repeating code to fetch audio data (#4103 by @netmindz) - Loxone JSON parser doesn't handle lx=0 correctly (#4104 by @FreakyJ, fixes #3809) - Rename wled00.ino to wled_main.cpp (#4090 by @willmmiles) - SM16825 chip support including WW & CW channel swap (#4092) - Add stress testing scripts (#4088 by @willmmiles) - Improve jsonBufferLock management (#4089 by @willmmiles) - Fix incorrect PWM bit depth on Esp32 with XTAL clock (#4082 by @PaoloTK) - Devcontainer args (#4073 by @axlan) - Effect: Fire2012 optional blur amount (#4078 by @apanteleev) - Effect: GEQ fix bands (#4077 by @adrianschroeter) - Boot delay option (#4060 by @DedeHai) - ESP8266 Audioreactive sync (#3962 by @gaaat98, @netmindz, @softhack007) - ESP8266 PWM crash fix (#4035 by @willmmiles) - Usermod: Battery fix (#4051 by @Nickbert7) - Usermod: Mpu6050 usermod crash fix (#4048 by @willmmiles) - Usermod: Internal Temperature V2 (#4033 by @adamsthws) - Various fixes and improvements (including build environments to emulate 0.14.0 for ESP8266) #### Build 2407070 - Various fixes and improvements (mainly LED settings fix) #### Build 2406290 - WLED 0.15.0-b4 release - LED settings bus management update (WARNING: only allows available outputs) - Add ETH support for LILYGO-POE-Pro (#4030 by @rorosaurus) - Update usermod_sn_photoresistor (#4017 by @xkvmoto) - Several internal fixes and optimisations - move LED_BUILTIN handling to BusManager class - reduce max panels (web server limitation) - edit WiFi TX power (ESP32) - keep current ledmap ID in UI - limit outputs in UI based on length - wifi.ap addition to JSON Info (JSON API) - relay pin init bugfix - file editor button in UI - ESP8266: update was restarting device on some occasions - a bit of throttling in UI (for ESP8266) #### Build 2406120 - Update NeoPixelBus to v2.8.0 - Increased LED outputs one ESP32 using parallel I2S (up to 17) - use single/mono I2S + 4x RMT for 5 outputs or less - use parallel x8 I2S + 8x RMT for >5 outputs (limit of 300 LEDs per output) - Fixed code of Smartnest and updated documentation (#4001 by @DevilPro1) - ESP32-S3 WiFi fix (#4010 by @cstruck) - TetrisAI usermod fix (#3897 by @muebau) - ESP-NOW usermod hook - Update wled.h regarding OTA Password (#3993 by @gsieben) - Usermod BME68X Sensor Implementation (#3994 by @gsieben) - Add a usermod for AHT10, AHT15 and AHT20 temperature/humidity sensors (#3977 by @LordMike) - Update Battery usermod documentation (#3968 by @adamsthws) - Add INA226 usermod for reading current and power over i2c (#3986 by @LordMike) - Bugfixes: #3991 - Several internal fixes and optimisations (WARNING: some effects may be broken that rely on overflow/narrow width) - replace uint8_t and uint16_t with unsigned - replace in8_t and int16_t with int - reduces code by 1kB #### Build 2405180 - WLED 0.14.4 release - Fix for #3978 - Official 0.15.0-b3 release - Merge 0.14.3 fixes into 0_15 - Added Pinwheel Expand 1D->2D effect mapping mode (#3961 by @Brandon502) - Add changeable i2c address to BME280 usermod (#3966 by @LordMike) - Effect: Firenoise - add palette selection - Experimental parallel I2S support for ESP32 (compile time option) - increased outputs to 17 - increased max possible color order overrides - use WLED_USE_PARALLEL_I2S during compile WARNING: Do not set up more than 256 LEDs per output when using parallel I2S with NeoPixelBus less than 2.9.0 - Update Usermod: Battery (#3964 by @adamsthws) - Update Usermod: BME280 (#3965 by @LordMike) - TM1914 chip support (#3913) - Ignore brightness in Peek - Antialiased line & circle drawing functions - Enabled some audioreactive effects for single pixel strips/segments (#3942 by @gaaat98) - Usermod Battery: Added Support for different battery types, Optimized file structure (#3003 by @itCarl) - Skip playlist entry API (#3946 by @freakintoddles2) - various optimisations and bugfixes (#3987, #3978) #### Build 2405030 - Using brightness in analog clock overlay (#3944 by @paspiz85) - Add Webpage shortcuts (#3945 by @w00000dy) - ArtNet Poll reply (#3892 by @askask) - Improved brightness change via long button presses (#3933 by @gaaat98) - Relay open drain output (#3920 by @Suxsem) - NEW JSON API: release info (update page, `info.release`) - update esp32 platform to arduino-esp32 v2.0.9 (#3902) - various optimisations and bugfixes (#3952, #3922, #3878, #3926, #3919, #3904 @DedeHai) #### Build 2404120 - v0.15.0-b3 - fix for #3896 & WS2815 current saving - conditional compile for AA setPixelColor() #### Build 2404100 - Internals: #3859, #3862, #3873, #3875 - Prefer I2S1 over RMT on ESP32 - usermod for Adafruit MAX17048 (#3667 by @ccruz09) - Runtime detection of ESP32 PICO, general PSRAM support - Extend JSON API "info" object - add "clock" - CPU clock in MHz - add "flash" - flash size in MB - Fix for #3879 - Analog PWM fix for ESP8266 (#3887 by @gaaat98) - Fix for #3870 (#3880 by @DedeHai) - ESP32 S3/S2 touch fix (#3798 by @DedeHai) - PIO env. PSRAM fix for S3 & S3 with 4M flash - audioreactive always included for S3 & S2 - Fix for #3889 - BREAKING: Effect: modified KITT (Scanner) (#3763) #### Build 2404040 - WLED 0.14.3 release - Fix for transition 0 (#3854, #3832, #3720) - Fix for #3855 via #3873 (by @willmmiles) #### Build 2403280 - Individual color channel control for JSON API (fixes #3860) - "col":[int|string|object|array, int|string|object|array, int|string|object|array] int = Kelvin temperature or 0 for black string = hex representation of [WW]RRGGBB object = individual channel control {"r":0,"g":127,"b":255,"w":255}, each being optional (valid to send {}) array = direct channel values [r,g,b,w] (w element being optional) - runtime selection for CCT IC (Athom 15W bulb) - #3850 (by @w00000dy) - Rotary encoder palette count bugfix - bugfixes and optimisations #### Build 2403240 - v0.15.0-b2 - WS2805 support (RGB + WW + CW, 600kbps) - Unified PSRAM use - NeoPixelBus v2.7.9 (for future WS2805 support) - Ubiquitous PSRAM mode for all variants of ESP32 - SSD1309_64 I2C Support for FLD Usermod (#3836 by @THATDONFC) - Palette cycling fix (add support for `{"seg":[{"pal":"X~Y~"}]}` or `{"seg":[{"pal":"X~Yr"}]}`) - FW1906 Support (#3810 by @deece and @Robert-github-com) - ESPAsyncWebServer 2.2.0 (#3828 by @willmmiles) - Bugfixes: #3843, #3844 #### Build 2403190 - limit max PWM frequency (fix incorrect PWM resolution) - Segment UI bugfix - Updated AsyncWebServer (by @wlillmmiles) - Simpler boot preset (fix for #3806) - Effect: Fix for 2D Drift animation (#3816 by @BaptisteHudyma) - Effect: Add twin option to 2D Drift - MQTT cleanup - DDP: Support sources that don't push (#3833 by @willmmiles) - Usermod: Tetris AI usermod (#3711 by @muebau) #### Build 2403171 - merge 0.14.2 changes into 0.15 #### Build 2403070 - Add additional segment options when controlling over e1.31 (#3616 by @demophoon) - LockedJsonResponse: Release early if possible (#3760 by @willmmiles) - Update setup-node and cache usermods in wled-ci.yml (#3737 by @WoodyLetsCode) - Fix preset sorting (#3790 by @WoodyLetsCode) - compile time button configuration #3792 - remove IR config if not compiled - additional string optimisations - Better low brightness level PWM handling (fixes #2767, #2868) #### Build 2402290 - Multiple analog button fix for #3549 - Preset caching on chips with PSRAM (credit @akaricchi) - Fixing stairway usermod and adding buildflags (by @lost-hope) - ESP-NOW packet modification - JSON buffer lock error messages / Reduce wait time for lock to 100ms - Reduce string RAM usage for ESP8266 - Fixing a potential array bounds violation in ESPDMX - Move timezone table to PROGMEM (#3766 by @willmmiles) - Reposition upload warning message. (fixes #3778) - ABL display fix & optimisation - Add virtual Art-Net RGBW option (#3783 by @shammy642) #### Build 2402090 - Added new Ethernet controller RGB2Go Tetra (duplicate of ESP3DEUXQuattro) - Usermod: httpPullLightControl (#3560 by @roelbroersma) - DMX: S2 & C3 support via modified ESPDMX - Bugfix: prevent cleaning of JSON buffer after a failed lock attempt (BufferGuard) - Product/Brand override (API & AP SSID) (#3750 by @moustachauve) #### Build 2402060 - WLED version 0.15.0-b1 - Harmonic Random Cycle palette (#3729 by @dedehai) - Multi PIR sensor usermod (added support for attaching multiple PIR sensors) - Removed obsolete (and nonfunctional) usermods #### Build 2309120 till build 2402010 - WLED version 0.15.0-a0 - Multi-WiFi support. Add up to 3 (or more via cusom compile) WiFis to connect to (with help from @JPZV) - Temporary AP. Use your WLED in public with temporary AP. - Github CI build system enhancements (#3718 by @WoodyLetsCode) - Accessibility: Node list ( #3715 by @WoodyLetsCode) - Analog clock overlay enhancement (#3489 by @WoodyLetsCode) - ESP32-POE-WROVER from Olimex ethernet support (#3625 by @m-wachter) - APA106 support (#3580 by @itstefanjanos) - BREAKING: Effect: updated Palette effect to support 2D (#3683 by @TripleWhy) - "SuperSync" from WLED MM (by @MoonModules) - Effect: DNA Spiral Effect Speed Fix (#3723 by @Derek4aty1) - Fix for #3693 - Orange flash fix (#3196) for transitions - Add own background image upload (#3596 by @WoodyLetsCode) - WLED time overrides (`WLED_NTP_ENABLED`, `WLED_TIMEZONE`, `WLED_UTC_OFFSET`, `WLED_LAT` and `WLED_LON`) - Better sorting and naming of static palettes (by @WoodyLetsCode) - ANIMartRIX usermod and effects (#3673 by @netmindz) - Use canvas instead of CSS gradient for liveview (#3621 by @zanhecht) - Fix for #3672 - ColoOrderMap W channel swap (color order overrides now have W swap) - En-/disable LED maps when receiving realtime data (#3554 by @ezcGman) - Added PWM frequency selection to UI (Settings) - Automatically build UI before compiling (#3598, #3666 by @WoodyLetsCode) - Internal: Added *suspend* API to `strip` (`WS2812FX class`) - Possible fix for #3589 & partial fix for #3605 - MPU6050 upgrade (#3654 by @willmmiles) - UI internals (#3656 by @WoodyLetsCode) - ColorPicker fix (#3658 by @WoodyLetsCode) - Global JSON buffer guarding (#3648 by @willmmiles, resolves #3641, #3312, #3367, #3637, #3646, #3447) - Effect: Fireworks 1D (fix for matrix trailing strip) - BREAKING: Reduced number of segments (12) on ESP8266 due to less available RAM - Increased available effect data buffer (increases more if board has PSRAM) - Custom palette editor mobile UI enhancement (by @imeszaros) - Per port Auto Brightness Limiter (ABL) - Use PSRAM for JSON buffer (double size, larger ledmaps, up to 2k) - Reduced heap fragmentation by allocating ledmap array only once and not deallocating effect buffer - HTTP retries on failed UI load - UI Search: scroll to top (#3587 by @WoodyLetsCode) - Return to inline iro.js and rangetouch.js (#3597 by @WoodyLetsCode) - Better caching (#3591 by @WoodyLetsCode) - Do not send 404 for missing `skin.css` (#3590 by @WoodyLetsCode) - Simplified UI rework (#3511 by @WoodyLetsCode) - Domoticz device ID for PIR and Temperature usermods - Bugfix for UCS8904 `hasWhite()` - Better search in UI (#3540 by @WoodyLetsCode) - Seeding FastLED PRNG (#3552 by @TripleWhy) - WIZ Smart Button support (#3547 by @micw) - New button type (button switch, fix for #3537) - Pixel Magic Tool update (#3483 by @ajotanc) - Effect: 2D Matrix fix for gaps - Bugfix #3526, #3533, #3561 - Spookier Halloween Eyes (#3501) - Compile time options for Multi Relay usermod (#3498) - Effect: Fix for Dissolve (#3502) - Better reverse proxy support (nested paths) - Implement global JSON API boolean toggle (i.e. instead of "var":true or "var":false -> "var":"t"). - Sort presets by ID - Fix for #3641, #3312, #3367, #3637, #3646, #3447, #3632, #3496, #2922, #3593, #3514, #3522, #3578 (partial), #3606 (@WoodyLetsCode) - Improved random bg image and added random bg image options (@WoodyLetsCode, #3481) - Audio palettes (Audioreactive usermod, credit @netmindz) - Better UI tooltips (@ajotnac, #3464) - Better effect filters (filter dropdown) - UDP sync fix (for #3487) - Power button override (solves #3431) - Additional HTTP request throttling (ESP8266) - Additional UI/UX improvements - Segment class optimisations (internal) - ESP-NOW sync - ESP-NOW Wiz remote JSON overrides (similar to IR JSON) & bugfixes - Gamma correction for custom palettes (#3399). - Restore presets from browser local storage - Optional effect blending - Restructured UDP Sync (internal) - Remove sync receive - Sync clarification - Disallow 2D effects on non-2D segments - Return of 2 audio simulations - Bugfix in sync #3344 (internal) - remove excessive segments - ignore inactive segments if not syncing bounds - send UDP/WS on segment change - pop_back() when removing last segment #### Build 2403170 - WLED 0.14.2 release #### Build 2403110 - Beta WLED 0.14.2-b2 - New AsyncWebServer (improved performance and reduced memory use) - New builds for ESP8266 with 160MHz CPU clock - Fixing stairway usermod and adding buildflags (#3758 by @lost-hope) - Fixing a potential array bounds violation in ESPDMX - Reduced RAM usage (moved strings and TZ data (by @willmmiles) to PROGMEM) - LockedJsonResponse: Release early if possible (by @willmmiles) #### Build 2402120 - Beta WLED 0.14.2-b1 - Possible fix for #3589 & partial fix for #3605 - Prevent JSON buffer clear after failed lock attempt - Multiple analog button fix for #3549 - UM Audioreactive: add two compiler options (#3732 by @wled-install) - Fix for #3693 #### Build 2401141 - Official release of WLED 0.14.1 - Fix for #3566, #3665, #3672 - Sorting of palettes in custom palette editor (#3674 by @WoodyLetsCode) #### Build 2401060 - Version bump: 0.14.1-b3 - Global JSON buffer guarding (#3648 by @willmmiles, resolves #3641, #3312, #3367, #3637, #3646, #3447) - Fix for #3632 - Custom palette editor mobile UI enhancement (#3617 by @imeszaros) - changelog update #### Build 2312290 - Fix for #3622, #3613, #3609 - Various tweaks and fixes - changelog update #### Build 2312230 - Version bump: 0.14.1-b2 - Fix for Pixel Magic button - Fix for #2922 (option to force WiFi PHY mode to G on ESP8266) - Fix for #3601, #3400 (incorrect sunrise/sunset, #3612 by @softhack007) #### Build 2312180 - Bugfixes (#3593, #3490, #3573, #3517, #3561, #3555, #3541, #3536, #3515, #3522, #3533, #3508) - Various other internal cleanups and optimisations #### Build 2311160 - Version bump: 0.14.1-b1 - Bugfixes (#3526, #3502, #3496, #3484, #3487, #3445, #3466, #3296, #3382, #3312) - New feature: Sort presets by ID - New usermod: LDR sensor (#3490 by @JeffWDH) - Effect: Twinklefox & Tinklecat metadata fix - Effect: separate #HH and #MM for Scrolling Text (#3480) - SSDR usermod enhancements (#3368) - PWM fan usermod enhancements (#3414) #### Build 2310010, build 2310130 - Release of WLED version 0.14.0 "Hoshi" - Bugfixes for #3400, #3403, #3405 - minor HTML optimizations - audioreactive: bugfix for UDP sound sync (partly initialized packets) #### Build 2309240 - Release of WLED beta version 0.14.0-b6 "Hoshi" - Effect bugfixes and improvements (Meteor, Meteor Smooth, Scrolling Text) - audioreactive: bugfixes for ES8388 and ES7243 init; minor improvements for analog inputs #### Build 2309100 - Release of WLED beta version 0.14.0-b5 "Hoshi" - New standard esp32 build with audioreactive - Effect blending bugfixes, and minor optimizations #### Build 2309050 - Effect blending (#3311) (finally effect transitions!) *WARNING*: May not work well with ESP8266, with plenty of segments or usermods (low RAM condition)!!! - Added receive and send sync groups to JSON API (#3317) (you can change sync groups using preset) - Internal temperature usermod (#3246) - MQTT server and topic length overrides (#3354) (new build flags) - Animated Staircase usermod enhancement (#3348) (on/off toggle/relay control) - Added local time info to Info page (#3351) - New effect: Rolling Balls (a.k.a. linear bounce) (#1039) - Various bug fixes and enhancements. #### Build 2308110 - Release of WLED beta version 0.14.0-b4 "Hoshi" - Reset effect data immediately upon mode change #### Build 2308030 - Improved random palette handling and blending - Soap bugfix - Fix ESP-NOW crash with AP mode Always #### Build 2307180 - Bus-level global buffering (#3280) - Removed per-segment LED buffer (SEGMENT.leds) - various fixes and improvements (ESP variants platform 5.3.0, effect optimizations, /json/cfg pin allocation) #### Build 2307130 - larger `oappend()` stack buffer (3.5k) for ESP32 - Preset cycle bugfix (#3262) - Rotary encoder ALT fix for large LED count (#3276) - effect updates (2D Plasmaball), `blur()` speedup - On/Off toggle from nodes view (may show unknown device type on older versions) (#3291) - various fixes and improvements (ABL, crashes when changing presets with different segments) #### Build 2306270 - ESP-NOW remote support (#3237) - Pixel Magic tool (display pixel art) (#3249) - Websocket (peek) fallback when connection cannot be established, WS retries (#3267) - Add WiFi network scan RPC command to Improv Serial (#3271) - Longer (custom option available) segment name for ESP32 - various fixes and improvements #### Build 2306210 - 0.14.0-b3 release - respect global I2C in all usermods (no local initialization of I2C bus) - Multi relay usermod compile-time enabled option (-D MULTI_RELAY_ENABLED=true|false) #### Build 2306180 - Added client-side option for applying effect defaults from metadata - Improved ESP8266 stability by reducing WebSocket response resends - Updated ESP8266 core to 3.1.2 #### Build 2306141 - Lissajous improvements - Scrolling Text improvements (leading 0) #### Build 2306140 - Add settings PIN (un)locking to JSON post API #### Build 2306130 - Bumped version to 0.14-b3 (beta 3) - added pin dropdowns in LED preferences (not for LED pins) and usermods - introduced (unused ATM) NeoGammaWLEDMethod class - Reverse proxy support - PCF8754 support for Rotary encoder (requires wiring INT pin to ESP GPIO) - Rely on global I2C pins for usermods (breaking change) - various fixes and enhancements #### Build 2306020 - Support for segment sets (PR #3171) - Reduce sound simulation modes to 2 to facilitate segment sets - Trigger button immediately on press if all configured presets are the same (PR #3226) - Changes for allowing Alexa to change light color to White when auto-calculating from RGB (PR #3211) #### Build 2305280 - DDP protocol update (#3193) - added PCF8574 I2C port expander support for Multi relay usermod - MQTT multipacket (fragmented) message fix - added option to retain MQTT brightness and color messages - new ethernet board: @srg74 Ethernet Shield - new 2D effects: Soap (#3184) & Octopus & Waving cell (credit @St3P40 https://github.com/80Stepko08) - various fixes and enhancements #### Build 2305090 - new ethernet board: @Wladi ABC! WLED Eth - Battery usermod voltage calculation (#3116) - custom palette editor (#3164) - improvements in Dancing Shadows and Tartan effects - UCS389x support - switched to NeoPixelBus 2.7.5 (replaced NeoPixelBrightnessBus with NeoPixelBusLg) - SPI bus clock selection (for LEDs) (#3173) - DMX mode preset fix (#3134) - iOS fix for scroll (#3182) - Wordclock "Norddeutsch" fix (#3161) - various fixes and enhancements #### Build 2304090 - updated Arduino ESP8266 core to 4.1.0 (newer compiler) - updated NeoPixelBus to 2.7.3 (with support for UCS890x chipset) - better support for ESP32-C3, ESP32-S2 and ESP32-S3 (Arduino ESP32 core 5.2.0) - iPad/tablet with 1024 pixels width in landscape orientation PC mode support (#3153) - fix for Pixel Art Converter (#3155) #### Build 2303240 - Peek scaling of large 2D matrices - Added 0D (1 pixel) metadata for effects & enhance 0D (analog strip) UI handling - Added ability to disable ADAlight (-D WLED_DISABLE_ADALIGHT) - Fixed APA102 output on Ethernet enabled controllers - Added ArtNet virtual/network output (#3121) - Klipper usermod (#3106) - Remove DST from CST timezone - various fixes and enhancements #### Build 2302180 - Removed Blynk support (servers shut down on 31st Dec 2022) - Added `ledgap.json` to complement ledmaps for 2D matrices - Added support for white addressable strips (#3073) - Ability to use SHT temperature usermod with PWM fan usermod - Added `onStateChange()` callback to usermods (#3081) - Refactored `bus_manager` [internal] - Dual 1D & 2D mode (add 1D strip after the matrix) - Removed 1D -> 2D mapping for individual pixel control - effect tweak: Fireworks 1D - various bugfixes #### Build 2301240 - Version bump to v0.14.0-b2 "Hoshi" - PixelArt converter (convert any image to pixel art and display it on a matrix) (PR #3042) - various effect updates and optimisations - added Overlay option to some effects (allows overlapping segments) - added gradient text on Scrolling Text - added #DDMM, #MMDD & #HHMM date and time options for Scrolling Text effect (PR #2990) - deprecated: Dynamic Smooth, Dissolve Rnd, Solid Glitter - optimised & enhanced loading of default values - new effect: Distortion Waves (2D) - 2D support for Ripple effect - slower minimum speed for Railway effect - DMX effect mode & segment controls (PR #2891) - Optimisations for conditional compiles (further reduction of code size) - better UX with effect sliders (PR #3012) - enhanced support for ESP32 variants: C3, S2 & S3 - usermod enhancements (PIR, Temperature, Battery (PR #2975), Analog Clock (PR #2993)) - new usermod SHT (PR #2963) - 2D matrix set up with gaps or irregular panels (breaking change!) (PR #2892) - palette blending/transitions - random palette smooth changes - hex color notations in custom palettes - allow more virtual buses - plethora of bugfixes ### WLED release 0.14.0-b1 #### Build 2212222 - Version bump to v0.14.0-b1 "Hoshi" - 2D matrix support (including mapping 1D effects to 2D and 2D peek) - [internal] completely rewritten Segment & WS2812FX handling code - [internal] ability to add custom effects via usermods - [internal] set of 2D drawing functions - transitions on every segment (including ESP8266) - enhanced old and new 2D effects (metadata: default values) - custom palettes (up to 10; upload palette0.json, palette1.json, ...) - custom effect sliders and options, quick filters - global I2C and SPI GPIO allocation (for usermods) - usermod settings page enhancements (dropdown & info) - asynchronous preset loading (and added "pd" JSON API call for direct preset apply) - new usermod Boblight (PR #2917) - new usermod PWM Outputs (PR #2912) - new usermod Audioreactive - new usermod Word Clock Matrix (PR #2743) - new usermod Ping Pong Clock (PR #2746) - new usermod ADS1115 (PR #2752) - new usermod Analog Clock (PR #2736) - various usermod enhancements and updates - allow disabling pull-up resistors on buttons - SD card support (PR #2877) - enhanced HTTP API to support custom effect sliders & options (X1, X2, X3, M1, M2, M3) - multiple UDP sync message retries (PR #2830) - network debug printer (PR #2870) - automatic UI PC mode on large displays - removed support for upgrading from pre-0.10 (EEPROM) - support for setting GPIO level when LEDs are off (RMT idle level, ESP32 only) (PR #2478) - Pakistan time-zone (PKT) - ArtPoll support - TM1829 LED support - experimental support for ESP32 S2, S3 and C3 - general improvements and bugfixes ### WLED release 0.13.3 - Version bump to v0.13.3 "Toki" - Disable ESP watchdog by default (fixes flickering and boot issues on a fresh install) - Added support for LPD6803 ### WLED release 0.13.2 #### Build 2208140 - Version bump to v0.13.2 "Toki" - Added option to receive live data on the main segment only (PR #2601) - Enable ESP watchdog by default (PR #2657) - Fixed race condition when saving bus config - Better potentiometer filtering (PR #2693) - More suitable DMX libraries (PR #2652) - Fixed outgoing serial TPM2 message length (PR #2628) - Fixed next universe overflow and Art-Net DMX start address (PR #2607) - Fixed relative segment brightness (PR #2665) ### Builds between releases 0.13.1 and 0.13.2 #### Build 2203191 - Fixed sunrise/set calculation (once again) #### Build 2203190 - Fixed `/json/cfg` unable to set busses (#2589) - Fixed Peek with odd LED counts > 255 (#2586) #### Build 2203160 - Version bump to v0.13.2-a0 "Toki" - Add ability to skip up to 255 LEDs - Dependency version bumps ### WLED release 0.13.1 #### Build 2203150 - Version bump to v0.13.1 "Toki" - Fix persistent preset bug, preventing save of new presets ### WLED release 0.13.0 #### Build 2203142 - Release of WLED v0.13.0 "Toki" - Reduce APA102 hardware SPI frequency to 5Mhz - Remove `persistent` parameter in `savePreset()` ### Builds between releases 0.12.0 and 0.13.0 #### Build 2203140 - Added factory reset by pressing button 0 for >10 seconds - Added ability to set presets from DMX Effect mode - Simplified label hiding JS in user interface - Fixed JSON `{"live":true}` indefinite realtime mode #### Build 2203080 - Disabled auto white mode in segments with no RGB bus - Fixed hostname string not 0-terminated - Fixed Popcorn mode not lighting first LED on pop #### Build 2203060 - Dynamic hiding of unused color controls in UI (PR #2567) - Removed native Cronixie support and added Cronixie usermod - Fixed disabled timed preset expanding calendar - Fixed Color Order setting shown for analog busses - Fixed incorrect operator (#2566) #### Build 2203011 - IR rewrite (PR #2561), supports CCT - Added locate button to Time settings - CSS fixes and adjustments - Consistent Tab indentation in index JS and CSS - Added initial contribution style guideline #### Build 2202222 - Version bump to 0.13.0-b7 "Toki" - Fixed HTTP API commands not applying to all selected segments in some conditions - Blynk support is not compiled in by default on ESP32 builds #### Build 2202210 - Fixed HTTP API commands not applying to all selected segments if called from JSON - Improved Stream effects, no longer rely on LED state and won't fade out at low brightness #### Build 2202200 - Added `info.leds.seglc` per-segment light capability info (PR #2552) - Fixed `info.leds.rgbw` behavior - Segment bounds sync (PR #2547) - WebSockets auto reconnection and error handling - Disable relay pin by default (PR #2531) - Various fixes (ESP32 touch pin 33, floats, PR #2530, #2534, #2538) - Deprecated `info.leds.cct`, `info.leds.wv` and `info.leds.rgbw` - Deprecated `/url` endpoint #### Build 2202030 - Switched to binary format for WebSockets peek (PR #2516) - Playlist bugfix - Added `extractModeName()` utility function - Added serial out (PR #2517) - Added configurable baud rate #### Build 2201260 - Initial ESP32-C3 and ESP32-S2 support (PRs #2452, #2454, #2502) - Full segment sync (PR #2427) - Allow overriding of color order by ranges (PR #2463) - Added white channel to Peek #### Build 2112080 - Version bump to 0.13.0-b6 "Toki" - Added "ESP02" (ESP8266 with 2M of flash) to PIO/release binaries #### Build 2112070 - Added new effect "Fairy", replacing "Police All" - Added new effect "Fairytwinkle", replacing "Two Areas" - Static single JSON buffer (performance and stability improvement) (PR #2336) #### Build 2112030 - Fixed ESP32 crash on Colortwinkles brightness change - Fixed setting picker to black resetting hue and saturation - Fixed auto white mode not saved to config #### Build 2111300 - Added CCT and white balance correction support (PR #2285) - Unified UI slider style - Added LED settings config template upload #### Build 2111220 - Fixed preset cycle not working from preset called by UI - Reintroduced permanent min. and max. cycle bounds #### Build 2111190 - Changed default ESP32 LED pin from 16 to 2 - Renamed "Running 2" to "Chase 2" - Renamed "Tri Chase" to "Chase 3" #### Build 2111170 - Version bump to 0.13.0-b5 "Toki" - Improv Serial support (PR #2334) - Button improvements (PR #2284) - Added two time zones (PR #2264, 2311) - JSON in/decrementing support for brightness and presets - Fixed no gamma correction for JSON individual LED control - Preset cycle bugfix - Removed ledCount - LED settings buffer bugfix - Network pin conflict bugfix - Changed default ESP32 partition layout to 4M, 1M FS #### Build 2110110 - Version bump to 0.13.0-b4 "Toki" - Added option for bus refresh if off (PR #2259) - New auto segment logic - Fixed current calculations for virtual or non-linear configs (PR #2262) #### Build 2110060 - Added virtual network DDP busses (PR #2245) - Allow playlist as end preset in playlist - Improved bus start field UX - Pin reservations improvements (PR #2214) #### Build 2109220 - Version bump to 0.13.0-b3 "Toki" - Added segment names (PR #2184) - Improved Police and other effects (PR #2184) - Reverted PR #1902 (Live color correction - will be implemented as usermod) (PR #2175) - Added transitions for segment on/off - Improved number of sparks/stars in Fireworks effect with low number of segments - Fixed segment name edit pencil disappearing with request - Fixed color transition active even if the segment is off - Disallowed file upload with OTA lock active - Fixed analog invert option missing (PR #2219) #### Build 2109100 - Added an auto create segments per bus setting - Added 15 new palettes from SR branch (PR #2134) - Fixed segment runtime not reset on FX change via HTTP API - Changed AsyncTCP dependency to pbolduc fork v1.2.0 #### Build 2108250 - Added Sync groups (PR #2150) - Added JSON API over Serial support - Live color correction (PR #1902) #### Build 2108180 - Fixed JSON IR remote not working with codes greater than 0xFFFFFF (fixes #2135) - Fixed transition 0 edge case #### Build 2108170 - Added application level pong websockets reply (#2139) - Use AsyncTCP 1.0.3 as it mitigates the flickering issue from 0.13.0-b2 - Fixed transition manually updated in preset overridden by field value #### Build 2108050 - Fixed undesirable color transition from Orange to boot preset color on first boot - Removed misleading Delete button on new playlist with one entry - Updated NeoPixelBus to 2.6.7 and AsyncTCP to 1.1.1 #### Build 2107230 - Added skinning (extra custom CSS) (PR #2084) - Added presets/config backup/restore (PR #2084) - Added option for using length instead of Stop LED in UI (PR #2048) - Added custom `holidays.json` holiday list (PR #2048) #### Build 2107100 - Version bump to 0.13.0-b2 "Toki" - Accept hex color strings in individual LED API - Fixed transition property not applying unless power/bri/color changed next - Moved transition field below segments (temporarily) - Reduced unneeded websockets pushes #### Build 2107091 - Fixed presets using wrong call mode (e.g. causing buttons to send UDP under direct change type) - Increased hue buffer - Renamed `NOTIFIER_CALL_MODE_` to `CALL_MODE_` #### Build 2107090 - Busses extend total configured LEDs if required - Fixed extra button pins defaulting to 0 on first boot #### Build 2107080 - Made Peek use the main websocket connection instead of opening a second one - Temperature usermod fix (from @blazoncek's dev branch) #### Build 2107070 - More robust initial resource loading in UI - Added `getJsonValue()` for usermod config parsing (PR #2061) - Fixed preset saving over websocket - Alpha ESP32 S2 support (filesystem does not work) (PR #2067) #### Build 2107042 - Updated ArduinoJson to 6.18.1 - Improved Twinkleup effect - Fixed preset immediately deselecting when set via HTTP API `PL=` #### Build 2107041 - Restored support for "PL=~" mistakenly removed in 2106300 - JSON IR improvements #### Build 2107040 - Playlist entries are now more compact - Added the possibility to enter negative numbers for segment offset #### Build 2107021 - Added WebSockets support to UI #### Build 2107020 - Send websockets on every state change - Improved Aurora effect #### Build 2107011 - Added MQTT button feedback option (PR #2011) #### Build 2107010 - Added JSON IR codes (PR #1941) - Adjusted the width of WiFi and LED settings input fields - Fixed a minor visual issue with slider trail not reaching thumb on low values #### Build 2106302 - Fixed settings page broken by using "%" in input fields #### Build 2106301 - Fixed a problem with disabled buttons reverting to pin 0 causing conflict #### Build 2106300 - Version bump to 0.13.0-b0 "Toki" - BREAKING: Removed preset cycle (use playlists) - BREAKING: Removed `nl.fade`, `leds.pin` and `ccnf` from JSON API - Added playlist editor UI - Reordered segment UI and added offset field - Raised maximum MQTT password length to 64 (closes #1373) #### Build 2106290 - Added Offset to segments, allows shifting the LED considered first within a segment - Added `of` property to seg object in JSON API to set offset - Usermod settings improvements (PR #2043, PR #2045) #### Build 2106250 - Fixed preset only disabling on second effect/color change #### Build 2106241 - BREAKING: Added ability for usermods to force a config save if config incomplete. `readFromConfig()` needs to return a `bool` to indicate if the config is complete - Updated usermods implementing `readFromConfig()` - Auto-create segments based on configured busses #### Build 2106200 - Added 2 Ethernet boards and split Ethernet configs into separate file #### Build 2106180 - Fixed DOS on Chrome tab restore causing reboot #### Build 2106170 - Optimized JSON buffer usage (pre-serialized color arrays) #### Build 2106140 - Updated main logo - Reduced flash usage by 0.8kB by using 8-bit instead of 32-bit PNGs for welcome and 404 pages - Added a check to stop Alexa reporting an error if state set by macro differs from the expected state #### Build 2106100 - Added support for multiple buttons with various types (PR #1977) - Fixed infinite playlists (PR #2020) - Added `r` to playlist object, allows for shuffle regardless of the `repeat` value - Improved accuracy of NTP time sync - Added possibility for WLED UDP sync to sync system time - Improved UDP sync accuracy, if both sender and receiver are NTP synced - Fixed a cache issue with restored tabs - Cache CORS request - Disable WiFi sleep by default on ESP32 #### Build 2105230 - No longer retain MQTT `/v` topic to alleviate storage loads on MQTT broker - Fixed Sunrise calculation (atan_t approx. used outside of value range) #### Build 2105200 - Fixed WS281x output on ESP32 - Fixed potential out-of-bounds write in MQTT - Fixed IR pin not changeable if IR disabled - Fixed XML API containing -1 on Manual only RGBW mode (see #888, #1783) #### Build 2105171 - Always copy MQTT payloads to prevent non-0-terminated strings - Updated ArduinoJson to 6.18.0 - Added experimental support for `{"on":"t"}` to toggle on/off state via JSON #### Build 2105120 - Fixed possibility of non-0-terminated MQTT payloads - Fixed two warnings regarding integer comparison #### Build 2105112 - Usermod settings page no usermods message - Lowered min speed for Drip effect #### Build 2105111 - Fixed various Codacy code style and logic issues #### Build 2105110 - Added Usermod settings page and configurable usermods (PR #1951) - Added experimental `/json/cfg` endpoint for changing settings from JSON (see #1944, not part of official API) #### Build 2105070 - Fixed not turning on after pressing "Off" on IR remote twice (#1950) - Fixed OTA update file selection from Android app (TODO: file type verification in JS, since android can't deal with accept='.bin' attribute) #### Build 2104220 - Version bump to 0.12.1-b1 "Hikari" - Release and build script improvements (PR #1844) #### Build 2104211 - Replace default TV simulator effect with the version that saves 18k of flash and appears visually identical #### Build 2104210 - Added `tb` to JSON state, allowing setting the timebase (set tb=0 to start e.g. wipe effect from the beginning). Receive only. - Slightly raised Solid mode refresh rate to work with LEDs (TM1814) that require refresh rates of at least 2fps - Added sunrise and sunset calculation to the backup JSON time source #### Build 2104151 - `NUM_STRIPS` no longer required with compile-time strip defaults - Further optimizations in wled_math.h #### Build 2104150 - Added ability to add multiple busses as compile time defaults using the esp32_multistrip usermod define syntax #### Build 2104141 - Reduced memory usage by 540b by switching to a different trigonometric approximation #### Build 2104140 - Added dynamic location-based Sunrise/Sunset macros (PR #1889) - Improved seasonal background handling (PR #1890) - Fixed instance discovery not working if MQTT not compiled in - Fixed Button, IR, Relay pin not assigned by default (resolves #1891) #### Build 2104120 - Added switch support (button macro is switch closing action, long press macro switch opening) - Replaced Circus effect with new Running Dual effect (Circus is Tricolor Chase with Red/White/Black) - Fixed ledmap with multiple segments (PR #1864) #### Build 2104030 - Fixed ESP32 crash on Drip effect with reversed segment (#1854) - Added flag `WLED_DISABLE_BROWNOUT_DET` to disable ESP32 brownout detector (off by default) ### WLED release 0.12.0 #### Build 2104020 - Allow clearing button/IR/relay pin on platforms that don't support negative numbers - Removed AUX pin - Hid some easter eggs, only to be found at easter ### Development versions between 0.11.1 and 0.12.0 releases #### Build 2103310 - Version bump to 0.12.0 "Hikari" - Fixed LED settings submission in iOS app #### Build 2103300 - Version bump to 0.12.0-b5 "Hikari" - Update to core espressif32@3.2 - Fixed IR pin not configurable #### Build 2103290 - Version bump to 0.12.0-b4 "Hikari" - Experimental use of espressif32@3.1.1 - Fixed RGBW mode disabled after LED settings saved - Fixed infrared support not compiled in if IRPIN is not defined #### Build 2103230 - Fixed current estimation #### Build 2103220 - Version bump to 0.12.0-b2 "Hikari" - Worked around an issue causing a critical decrease in framerate (wled.cpp l.240 block) - Bump to Espalexa v2.7.0, fixing discovery #### Build 2103210 - Version bump to 0.12.0-b1 "Hikari" - More colors visible on Palette preview - Fixed chevron icon not included - Fixed color order override - Cleanup #### Build 2103200 - Version bump to 0.12.0-b0 "Hikari" - Added palette preview and search (PR #1637) - Added Reverse checkbox for PWM busses - reverses logic level for on - Fixed various problems with the Playlist feature (PR #1724) - Replaced "Layer" icon with "i" icon for Info button - Chunchun effect more fitting for various segment lengths (PR #1804) - Removed global reverse (in favor of individual bus reverse) - Removed some unused icons from UI icon font #### Build 2103130 - Added options for Auto Node discovery - Optimized strings (no string both F() and raw) #### Build 2103090 - Added Auto Node discovery (PR #1683) - Added tooltips to quick color selectors for accessibility #### Build 2103060 - Auto start field population in bus config #### Build 2103050 - Fixed incorrect over-memory indication in LED settings on ESP32 #### Build 2103041 - Added destructor for BusPwm (fixes #1789) #### Build 2103040 - Fixed relay mode inverted when upgrading from 0.11.0 - Fixed no more than 2 pins per bus configurable in UI - Changed to non-linear IR brightness steps (PR #1742) - Fixed various warnings (PR #1744) - Added UDP DNRGBW Mode (PR #1704) - Added dynamic LED mapping with ledmap.json file (PR #1738) - Added support for QuinLED-ESP32-Ethernet board - Added support for WESP32 ethernet board (PR #1764) - Added Caching for main UI (PR #1704) - Added Tetrix mode (PR #1729) - Removed Merry Christmas mode (use "Chase 2" - called Running 2 before 0.13.0) - Added memory check on Bus creation #### Build 2102050 - Version bump to 0.12.0-a0 "Hikari" - Added FPS indication in info - Bumped max outputs from 7 to 10 busses for ESP32 #### Build 2101310 - First alpha configurable multipin #### Build 2101130 - Added color transitions for all segments and slots and for segment brightness - Fixed bug that prevented setting a boot preset higher than 25 #### Build 2101040 - Replaced Red & Blue effect with Aurora effect (PR #1589) - Fixed HTTP changing segments uncommanded (#1618) - Updated copyright year and contributor page link #### Build 2012311 - Fixed Countdown mode #### Build 2012310 - (Hopefully actually) fixed display of usermod values in info screen #### Build 2012240 - Fixed display of usermod values in info screen - 4 more effects now use FRAMETIME - Remove unsupported environments from platformio.ini #### Build 2012210 - Split index.htm in separate CSS + JS files (PR #1542) - Minify UI HTML, saving >1.5kB flash - Fixed JShint warnings #### Build 2012180 - Boot brightness 0 will now use the brightness from preset - Add iOS scrolling momentum (from PR #1528) ### WLED release 0.11.1 #### Build 2012180 - Release of WLED 0.11.1 "Mirai" - Fixed AP hide not saving (fixes #1520) - Fixed MQTT password re-transmitted to HTML - Hide Update buttons while uploading, accept .bin - Make sure AP password is at least 8 characters long ### Development versions after 0.11.0 release #### Build 2012160 - Bump Espalexa to 2.5.0, fixing discovery (PR Espalexa/#152, originally PR #1497) #### Build 2012150 - Added Blends FX (PR #1491) - Fixed an issue that made it impossible to deactivate timed presets #### Build 2012140 - Added Preset ID quick display option (PR #1462) - Fixed LEDs not turning on when using gamma correct brightness and LEDPIN 2 (default) - Fixed notifier applying main segment to selected segments on notification with FX/Col disabled #### Build 2012130 - Fixed RGBW mode not saved between reboots (fixes #1457) - Added brightness scaling in palette function for default (PR #1484) #### Build 2012101 - Fixed preset cycle default duration rounded down to nearest 10sec interval (#1458) - Enabled E1.31/DDP/Art-Net in AP mode #### Build 2012100 - Fixed multi-segment preset cycle - Fixed EEPROM (pre-0.11 settings) not cleared on factory reset - Fixed an issue with intermittent crashes on FX change (PR #1465) - Added function to know if strip is updating (PR #1466) - Fixed using colorwheel sliding the UI (PR #1459) - Fixed analog clock settings not saving (PR #1448) - Added Temperature palette (PR #1430) - Added Candy cane FX (PR #1445) #### Build 2012020 - UDP `parsePacket()` with sync disabled (#1390) - Added Multi RGBW DMX mode (PR #1383) #### Build 2012010 - Fixed compilation for analog (PWM) LEDs ### WLED version 0.11.0 #### Build 2011290 - Release of WLED 0.11.0 "Mirai" - Workaround for weird empty %f Espalexa issue - Fixed crash on saving preset with HTTP API `PS` - Improved performance for color changes in non-main segment #### Build 2011270 - Added tooltips for speed and intensity sliders (PR #1378) - Moved color order to NpbWrapper.h - Added compile time define to override the color order for a specific range #### Build 2011260 - Add `live` property to state, allowing toggling of realtime (not incl. in state resp.) - PIO environment changes #### Build 2011230 - Version bump to 0.11.0 "Mirai" - Improved preset name sorting - Fixed Preset cycle not working beyond preset 16 ### Development versions between 0.10.2 and 0.11.0 releases #### Build 2011220 - Fixed invalid save when modifying preset before refresh (might be related to #1361) - Fixed brightness factor ignored on realtime timeout (fixes #1363) - Fixed Phase and Chase effects with LED counts >256 (PR #1366) #### Build 2011210 - Fixed Brightness slider beneath color wheel not working (fixes #1360) - Fixed invalid UI state after saving modified preset #### Build 2011200 - Added HEX color receiving to JSON API with `"col":["RRGGBBWW"]` format - Moved Kelvin color receiving in JSON API from `"col":[[val]]` to `"col":[val]` format _Notice:_ This is technically a breaking change. Since no release was made since the introduction and the Kelvin property was not previously documented in the wiki, impact should be minimal. - BTNPIN can now be disabled by setting to -1 (fixes #1237) #### Build 2011180 - Platformio.ini updates and streamlining (PR #1266) - my_config.h custom compile settings system (not yet used for much, adapted from PR #1266) - Added Hawaii timezone (HST) - Linebreak after 5 quick select buttons #### Build 2011154 - Fixed RGBW saved incorrectly - Fixed pmt caching requesting /presets.json too often - Fixed deEEP not copying the first segment of EEPROM preset 16 #### Build 2011153 - Fixed an ESP32 end-of-file issue - Fixed strip.isRgbw not read from cfg.json #### Build 2011152 - Version bump to 0.11.0p "Mirai" - Increased max. num of segments to 12 (ESP8266) / 16 (ESP32) - Up to 250 presets stored in the `presets.json` file in filesystem - Complete overhaul of the Presets UI tab - Updated iro.js to v5 (fixes black color wheel) - Added white temperature slider to color wheel - Add JSON settings serialization/deserialization to cfg.json and wsec.json - Added deEEP to convert the EEPROM settings and presets to files - Playlist support - JSON only for now - New v2 usermod methods `addToConfig()` and `readFromConfig()` (see EXAMPLE_v2 for doc) - Added Ethernet support for ESP32 (PR #1316) - IP addresses are now handled by the `Network` class - New `esp32_poe` PIO environment - Use EspAsyncWebserver Aircoookie fork v.2.0.0 (hiding wsec.json) - Removed `WLED_DISABLE_FILESYSTEM` and `WLED_ENABLE_FS_SERVING` defines as they are now required - Added pin manager - UI performance improvements (no drop shadows) - More explanatory error messages in UI - Improved candle brightness - Return remaining nightlight time `nl.rem` in JSON API (PR #1302) - UI sends timestamp with every command, allowing for timed presets without using NTP - Added gamma calculation (yet unused) - Added LED type definitions to const.h (yet unused) - Added nicer 404 page - Removed `NP` and `MS=` macro HTTP API commands - Removed macros from Time settings #### Build 2011120 - Added the ability for the /api MQTT topic to receive JSON API payloads #### Build 2011040 - Inverted Rain direction (fixes #1147) #### Build 2011010 - Re-added previous C9 palette - Renamed new C9 palette #### Build 2010290 - Colorful effect now supports palettes - Added C9 2 palette (#1291) - Improved C9 palette brightness by 12% - Disable onboard LED if LEDs are off (PR #1245) - Added optional status LED (PR #1264) - Realtime max. brightness now honors brightness factor (fixes #1271) - Updated ArduinoJSON to 6.17.0 #### Build 2010020 - Fixed interaction of `T` and `NL` HTTP API commands (#1214) - Fixed an issue where Sunrise mode nightlight does not activate if toggled on simultaneously #### Build 2009291 - Fixed MQTT bootloop (no F() macro, #1199) #### Build 2009290 - Added basic DDP protocol support - Added Washing Machine effect (PR #1208) #### Build 2009260 - Added Loxone parser (PR #1185) - Added support for kelvin input via `K=` HTTP and `"col":[[val]]` JSON API calls _Notice:_ `"col":[[val]]` removed in build 2011200, use `"col":[val]` - Added supplementary UDP socket (#1205) - TMP2.net receivable by default - UDP sockets accept HTTP and JSON API commands - Fixed missing timezones (#1201) #### Build 2009202 - Fixed LPD8806 compilation #### Build 2009201 - Added support for preset cycle toggling using CY=2 - Added ESP32 touch pin support (#1190) - Fixed modem sleep on ESP8266 (#1184) #### Build 2009200 - Increased available heap memory by 4kB - Use F() macro for the majority of strings - Restructure timezone code - Restructured settings saved code - Updated ArduinoJSON to 6.16.1 #### Build 2009170 - New WLED logo on Welcome screen (#1164) - Fixed 170th pixel dark in E1.31 #### Build 2009100 - Fixed sunrise mode not reinitializing - Fixed passwords not clearable #### Build 2009070 - New Segments are now initialized with default speed and intensity #### Build 2009030 - Fixed bootloop if mDNS is used on builds without OTA support ### WLED version 0.10.2 #### Build 2008310 - Added new logo - Maximum GZIP compression (#1126) - Enable WebSockets by default ### Development versions between 0.10.0 and 0.10.2 releases #### Build 2008300 - Added new UI customization options to UI settings - Added Dancing Shadows effect (#1108) - Preset cycle is now paused if lights turned off or nightlight active - Removed `esp01` and `esp01_ota` envs from travis build (need too much flash) #### Build 2008290 - Added individual LED control support to JSON API - Added internal Segment Freeze/Pause option #### Build 2008250 - Made `platformio_override.ini` example easier to use by including the `default_envs` property - FastLED uses `now` as timer, so effects using e.g. `beatsin88()` will sync correctly - Extended the speed range of Pacifica effect - Improved TPM2.net receiving (#1100) - Fixed exception on empty MQTT payload (#1101) #### Build 2008200 - Added segment mirroring to web UI - Fixed segment mirroring when in reverse mode #### Build 2008140 - Removed verbose live mode info from `` in HTTP API response #### Build 2008100 - Fixed Auto White mode setting (fixes #1088) #### Build 2008070 - Added segment mirroring (`mi` property) (#1017) - Fixed DMX settings page not displayed (#1070) - Fixed ArtNet multi universe and improve code style (#1076) - Renamed global var `local` to `localTime` (#1078) #### Build 2007190 - Fixed hostname containing illegal characters (#1035) #### Build 2006251 - Added `SV=2` to HTTP API, allow selecting single segment only #### Build 2006250 - Fix Alexa not turning off white channel (fixes #1012) #### Build 2006220 - Added Sunrise nightlight mode - Added Chunchun effect - Added `LO` (live override) command to HTTP API - Added `mode` to `nl` object of JSON state API, deprecating `fade` - Added light color scheme support to web UI (click sun next to brightness slider) - Added option to hide labels in web UI (click flame icon next to intensity slider) - Added hex color input (click palette icon next to palette select) (resolves #506) - Added support for RGB sliders (need to set in localstorage) - Added support for custom background color or image (need to set in localstorage) - Added option to hide bottom tab bar in PC mode (need to set in localstorage) - Fixed transition lag with multiple segments (fixes #985) - Changed Nightlight wording (resolves #940) #### Build 2006060 - Added five effects by Andrew Tuline (Phased, Phased Noise, Sine, Noise Pal and Twinkleup) - Added two new effects by Aircoookie (Sunrise and Flow) - Added US-style sequence to traffic light effect - Merged pull request #964 adding 9 key IR remote #### Build 2005280 - Added v2 usermod API - Added v2 example usermod `usermod_v2_example` in the usermods folder as prelimary documentation - Added DS18B20 Temperature usermod with Info page support - Disabled MQTT on ESP01 build to make room in flash #### Build 2005230 - Fixed TPM2 #### Build 2005220 - Added TPM2.NET protocol support (need to set WLED broadcast UDP port to 65506) - Added TPM2 protocol support via Serial - Support up to 6553 seconds preset cycle durations (backend, NOT yet in UI) - Merged pull request #591 fixing WS2801 color order - Merged pull request #858 adding fully featured travis builds - Merged pull request #862 adding DMX proxy feature #### Build 2005100 - Update to Espalexa v2.4.6 (+1.6kB free heap memory) - Added `m5atom` PlatformIO environment #### Build 2005090 - Default to ESP8266 Arduino core v2.7.1 in PlatformIO - Fixed Preset Slot 16 always indicating as empty (#891) - Disabled Alexa emulation by default (causes bootloop for some users) - Added BWLT11 and SHOJO_PCB defines to NpbWrapper - Merged pull request #898 adding Solid Glitter effect ### WLED version 0.10.0 #### Build 2005030 - DMX Single RGW and Single DRGB modes now support an additional white channel - Improved palettes derived from set colors and changed their names ### Development versions between 0.9.1 and 0.10.0 release #### Build 2005020 - Added ACST and ACST/ACDT timezones #### Build 2005010 - Added module info page to web UI - Added realtime override functionality to web UI - Added individual segment power and brightness to web UI - Added feature to one-click select single segment only by tapping segment name - Removed palette jumping to default if color is changed #### Build 2004300 - Added realtime override option and `lor` JSON property - Added `lm` (live mode) and `lip` (live IP) properties to info in JSON API - Added reset commands to APIs - Added `json/si`, returning state and info, but no FX or Palette lists - Added rollover detection to millis(). Can track uptimes longer than 49 days - Attempted to fix Wifi issues with Unifi brand APs #### Build 2004230 - Added brightness and power for individual segments - Added `on` and `bri` properties to Segment object in JSON API - Added `C3` an `SB` commands to HTTP get API - Merged pull request #865 for 5CH_Shojo_PCB environment #### Build 2004220 - Added Candle Multi effect - Added Palette capability to Pacifica effect #### Build 2004190 - Added TM1814 type LED defines #### Build 2004120 - Added Art-Net support - Added OTA platform to platformio.ini #### Build 2004100 - Fixed DMX output compilation - Added DMX start LED setting #### Build 2004061 - Fixed RBG and BGR getPixelColor (#825) - Improved formatting #### Build 2004060 - Consolidated global variables in wled.h #### Build 2003300 - Major change of project structure from .ino to .cpp and func_declare.h #### Build 2003262 - Fixed compilation for Analog LEDs - Fixed sync settings network port fields too small #### Build 2003261 - Fixed live preview not displaying whole light if over 255 LEDs #### Build 2003251 - Added Pacifica effect (tentative, doesn't yet support other colors) - Added Atlantica palette - Fixed ESP32 build of Espalexa #### Build 2003222 - Fixed Alexa Whites on non-RGBW lights (bump Espalexa to 2.4.5) #### Build 2003221 - Moved Cronixie driver from FX library to drawOverlay handler #### Build 2003211 - Added custom mapping compile define to FX_fcn.h - Merged pull request #784 by @TravisDean: Fixed initialization bug when toggling skip first - Added link to youtube videos by Room31 to readme #### Build 2003141 - Fixed color of main segment returned in JSON API during transition not being target color (closes #765) - Fixed arlsLock() being called after pixels set in E1.31 (closes #772) - Fixed HTTP API calls not having an effect if no segment selected (now applies to main segment) #### Build 2003121 - Created changelog.md - make tracking changes to code easier - Merged pull request #766 by @pille: Fix E1.31 out-of sequence detection ================================================ FILE: CODE_OF_CONDUCT.md ================================================ # Contributor Covenant Code of Conduct ## Our Pledge In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation. ## Our Standards Examples of behavior that contributes to creating a positive environment include: * Using welcoming and inclusive language * Being respectful of differing viewpoints and experiences * Gracefully accepting constructive criticism * Focusing on what is best for the community * Showing empathy towards other community members Examples of unacceptable behavior by participants include: * The use of sexualized language or imagery and unwelcome sexual attention or advances * Trolling, insulting/derogatory comments, and personal or political attacks * Public or private harassment * Publishing others' private information, such as a physical or electronic address, without explicit permission * Other conduct which could reasonably be considered inappropriate in a professional setting ## Our Responsibilities Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. ## Scope This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. ## Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at dev.aircoookie@gmail.com. All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. ## Attribution This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html [homepage]: https://www.contributor-covenant.org For answers to common questions about this code of conduct, see https://www.contributor-covenant.org/faq ================================================ FILE: CONTRIBUTING.md ================================================ # Thank you for making WLED better! WLED is a community-driven project, and every contribution matters! We appreciate your time and effort. Our maintainers are here for two things: **helping you** improve your code, and **keeping WLED** lean, efficient, and maintainable. We'll work with you to refine your contribution, but we'll also push back if something might create technical debt or add features without clear value. Don't take it personally - we're just protecting WLED's architecture while helping your contribution succeed! ## Getting Started Here are a few suggestions to make it easier for you to contribute: ### PR from a branch in your own fork Start your pull request (PR) in a branch of your own fork. Don't make a PR directly from your main branch. This lets you update your PR if needed, while you can work on other tasks in 'main' or in other branches. > [!TIP] > **The easiest way to start your first PR** > When viewing a file in `wled/WLED`, click on the "pen" icon and start making changes. > When you choose to 'Commit changes', GitHub will automatically create a PR from your fork. > > image: fork and edit ### Target branch for pull requests Please make all PRs against the `main` branch. ### Describing your PR Please add a description of your proposed code changes. A PR with no description or just a few words might not get accepted, simply because very basic information is missing. No need to write an essay! A good description helps us to review and understand your proposed changes. For example, you could say a few words about * What you try to achieve (new feature, fixing a bug, refactoring, security enhancements, etc.) * How your code works (short technical summary - focus on important aspects that might not be obvious when reading the code) * Testing you performed, known limitations, anything you couldn't quite solve. * Let us know if you'd like guidance from a maintainer (WLED is a big project 😉) ### Testing Your Changes Before submitting: - ✅ Does it compile? - ✅ Does your feature/fix actually work? - ✅ Did you break anything else? - ✅ Tested on actual hardware if possible? Mention your testing in the PR description (e.g., "Tested on ESP32 + WS2812B"). ## During Review We're all volunteers, so reviews can take some time (longer during busy times). Don't worry - we haven't forgotten you! Feel free to ping after a week if there's no activity. ### Updating your code While the PR is open, you can keep updating your branch - just push more commits! GitHub will automatically update your PR. You don't need to squash commits or clean up history - we'll handle that when merging. > [!CAUTION] > Do not use "force-push" while your PR is open! > It has many subtle and unexpected consequences on our GitHub repository. > For example, we regularly lose review comments when the PR author force-pushes code changes. Our review bot (coderabbit) may become unable to properly track changes, it gets confused or stops responding to questions. > So, pretty please, do not force-push. > [!TIP] > Use [cherry-picking](https://docs.github.com/en/desktop/managing-commits/cherry-picking-a-commit-in-github-desktop) to copy commits from one branch to another. ### Responding to Reviews When we ask for changes: - **Add new commits** - please don't amend or force-push - **Reply in the PR** - let us know when you've addressed comments - **Ask questions** - if something's unclear, just ask! - **Be patient** - we're all volunteers here 😊 You can reference feedback in commit messages: > ```text > Fix naming per @Aircoookie's suggestion > ``` ### Dealing with Merge Conflicts Got conflicts with `main`? No worries - here's how to fix them: **Using GitHub Desktop** (easier for beginners): 1. Click **Fetch origin**, then **Pull origin** 2. If conflicts exist, GitHub Desktop will warn you - click **View conflicts** 3. Open the conflicted files in your editor (VS Code, etc.) 4. Remove the conflict markers (`<<<<<<<`, `=======`, `>>>>>>>`) and keep the correct code 5. Save the files 6. Back in GitHub Desktop, commit the merge (it'll suggest a message) 7. Click **Push origin** **Using command line**: ```bash git fetch origin git merge origin/main # Fix conflicts in your editor git add . git commit git push ``` Either way works fine - pick what you're comfortable with! Merging is simpler than rebasing and keeps everything connected. #### When you MUST rebase (really rare!) Sometimes you might hit merge conflicts with `main` that are harder to solve. Here's what to try: 1. **Merge instead of rebase** (safest option): ```bash git fetch origin git merge origin/main git push ``` Keeps review comments attached and CI results visible! 2. **Use cherry-picking** to copy commits between branches without rewriting history - [here's how](https://docs.github.com/en/desktop/managing-commits/cherry-picking-a-commit-in-github-desktop). 3. **If all else fails, use `--force-with-lease`** (not plain `--force`): ```bash git rebase origin/main git push --force-with-lease ``` Then **leave a comment** explaining why you had to force-push, and be ready to re-address some feedback. ### Additional Resources Want to know more? Check out: - 📚 [GitHub Desktop documentation](https://docs.github.com/en/desktop) - if you prefer GUI tools - 🎓 [How to properly submit a PR](https://github.com/wled-dev/WLED/wiki/How-to-properly-submit-a-PR) - detailed tips and tricks ## After Approval Once approved, a maintainer will merge your PR (possibly squashing commits). Your contribution will be in the next WLED release - thank you! 🎉 ## Coding Guidelines ### Source Code from an AI agent or bot > [!IMPORTANT] > It's OK if you took help from an AI for writing your source code. > > AI tools can be very helpful, but as the contributor, **you're responsible for the code**. * Make sure you really understand the AI-generated code, don't just accept it because it "seems to work". * Don't let the AI change existing code without double-checking by you as the contributor. Often, the result will not be complete. For example, previous source code comments may be lost. * Remember that AI is still "Often-Wrong" ;-) * If you don't feel confident using English, you can use AI for translating code comments and descriptions into English. AI bots are very good at understanding language. However, always check if the results are correct. The translation might still have wrong technical terms, or errors in some details. #### Best Practice with AI AI tools are powerful but "often wrong" - your judgment is essential! 😊 - ✅ **Understand the code** - As the person contributing to WLED, make sure you understand exactly what the AI-generated source code does - ✅ **Review carefully** - AI can lose comments, introduce bugs, or make unnecessary changes - ✅ **Be transparent** - Add a comment like `// This section was AI-generated` for larger chunks - ✅ **Use AI for translation** - AI is great for translating comments to English (but verify technical terms!) ### Code style Don't stress too much about style! When in doubt, just match the style in the files you're editing. 😊 Here are our main guidelines: #### Indentation We use tabs for indentation in Web files (.html/.css/.js) and spaces (2 per indentation level) for all other files. You are all set if you have enabled `Editor: Detect Indentation` in VS Code. #### Blocks Whether the opening bracket of e.g. an `if` block is in the same line as the condition or in a separate line is up to your discretion. If there is only one statement, leaving out block brackets is acceptable. Good: ```cpp if (a == b) { doStuff(a); } ``` ```cpp if (a == b) doStuff(a); ``` Also acceptable (though the first style is usually easier to read): ```cpp if (a == b) { doStuff(a); } ``` There should always be a space between a keyword and its condition and between the condition and brace. Within the condition, no space should be between the parenthesis and variables. Spaces between variables and operators are up to the authors discretion. There should be no space between function names and their argument parenthesis. Good: ```cpp if (a == b) { doStuff(a); } ``` Not good: ```cpp if( a==b ){ doStuff ( a); } ``` #### Comments Comments should have a space between the delimiting characters (e.g. `//`) and the comment text. We're gradually adopting this style - don't worry if you see older code without spaces! Good: ```cpp // This is a short inline comment. /* * This is a longer comment * wrapping over multiple lines, * used in WLED for file headers and function explanations */ ``` ```css /* This is a CSS inline comment */ ``` ```html ``` There is no hard character limit for a comment within a line, though as a rule of thumb consider wrapping after 120 characters. Inline comments are OK if they describe that line only and are not exceedingly wide. ================================================ FILE: LICENSE ================================================ Copyright (c) 2016-present Christian Schwinne and individual WLED contributors Licensed under the EUPL v. 1.2 or later EUROPEAN UNION PUBLIC LICENCE v. 1.2 EUPL © the European Union 2007, 2016 This European Union Public Licence (the ‘EUPL’) applies to the Work (as defined below) which is provided under the terms of this Licence. Any use of the Work, other than as authorised under this Licence is prohibited (to the extent such use is covered by a right of the copyright holder of the Work). The Work is provided under the terms of this Licence when the Licensor (as defined below) has placed the following notice immediately following the copyright notice for the Work: Licensed under the EUPL or has expressed by any other means his willingness to license under the EUPL. 1. Definitions In this Licence, the following terms have the following meaning: - ‘The Licence’: this Licence. - ‘The Original Work’: the work or software distributed or communicated by the Licensor under this Licence, available as Source Code and also as Executable Code as the case may be. - ‘Derivative Works’: the works or software that could be created by the Licensee, based upon the Original Work or modifications thereof. This Licence does not define the extent of modification or dependence on the Original Work required in order to classify a work as a Derivative Work; this extent is determined by copyright law applicable in the country mentioned in Article 15. - ‘The Work’: the Original Work or its Derivative Works. - ‘The Source Code’: the human-readable form of the Work which is the most convenient for people to study and modify. - ‘The Executable Code’: any code which has generally been compiled and which is meant to be interpreted by a computer as a program. - ‘The Licensor’: the natural or legal person that distributes or communicates the Work under the Licence. - ‘Contributor(s)’: any natural or legal person who modifies the Work under the Licence, or otherwise contributes to the creation of a Derivative Work. - ‘The Licensee’ or ‘You’: any natural or legal person who makes any usage of the Work under the terms of the Licence. - ‘Distribution’ or ‘Communication’: any act of selling, giving, lending, renting, distributing, communicating, transmitting, or otherwise making available, online or offline, copies of the Work or providing access to its essential functionalities at the disposal of any other natural or legal person. 2. Scope of the rights granted by the Licence The Licensor hereby grants You a worldwide, royalty-free, non-exclusive, sublicensable licence to do the following, for the duration of copyright vested in the Original Work: - use the Work in any circumstance and for all usage, - reproduce the Work, - modify the Work, and make Derivative Works based upon the Work, - communicate to the public, including the right to make available or display the Work or copies thereof to the public and perform publicly, as the case may be, the Work, - distribute the Work or copies thereof, - lend and rent the Work or copies thereof, - sublicense rights in the Work or copies thereof. Those rights can be exercised on any media, supports and formats, whether now known or later invented, as far as the applicable law permits so. In the countries where moral rights apply, the Licensor waives his right to exercise his moral right to the extent allowed by law in order to make effective the licence of the economic rights here above listed. The Licensor grants to the Licensee royalty-free, non-exclusive usage rights to any patents held by the Licensor, to the extent necessary to make use of the rights granted on the Work under this Licence. 3. Communication of the Source Code The Licensor may provide the Work either in its Source Code form, or as Executable Code. If the Work is provided as Executable Code, the Licensor provides in addition a machine-readable copy of the Source Code of the Work along with each copy of the Work that the Licensor distributes or indicates, in a notice following the copyright notice attached to the Work, a repository where the Source Code is easily and freely accessible for as long as the Licensor continues to distribute or communicate the Work. 4. Limitations on copyright Nothing in this Licence is intended to deprive the Licensee of the benefits from any exception or limitation to the exclusive rights of the rights owners in the Work, of the exhaustion of those rights or of other applicable limitations thereto. 5. Obligations of the Licensee The grant of the rights mentioned above is subject to some restrictions and obligations imposed on the Licensee. Those obligations are the following: Attribution right: The Licensee shall keep intact all copyright, patent or trademarks notices and all notices that refer to the Licence and to the disclaimer of warranties. The Licensee must include a copy of such notices and a copy of the Licence with every copy of the Work he/she distributes or communicates. The Licensee must cause any Derivative Work to carry prominent notices stating that the Work has been modified and the date of modification. Copyleft clause: If the Licensee distributes or communicates copies of the Original Works or Derivative Works, this Distribution or Communication will be done under the terms of this Licence or of a later version of this Licence unless the Original Work is expressly distributed only under this version of the Licence — for example by communicating ‘EUPL v. 1.2 only’. The Licensee (becoming Licensor) cannot offer or impose any additional terms or conditions on the Work or Derivative Work that alter or restrict the terms of the Licence. Compatibility clause: If the Licensee Distributes or Communicates Derivative Works or copies thereof based upon both the Work and another work licensed under a Compatible Licence, this Distribution or Communication can be done under the terms of this Compatible Licence. For the sake of this clause, ‘Compatible Licence’ refers to the licences listed in the appendix attached to this Licence. Should the Licensee's obligations under the Compatible Licence conflict with his/her obligations under this Licence, the obligations of the Compatible Licence shall prevail. Provision of Source Code: When distributing or communicating copies of the Work, the Licensee will provide a machine-readable copy of the Source Code or indicate a repository where this Source will be easily and freely available for as long as the Licensee continues to distribute or communicate the Work. Legal Protection: This Licence does not grant permission to use the trade names, trademarks, service marks, or names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the copyright notice. 6. Chain of Authorship The original Licensor warrants that the copyright in the Original Work granted hereunder is owned by him/her or licensed to him/her and that he/she has the power and authority to grant the Licence. Each Contributor warrants that the copyright in the modifications he/she brings to the Work are owned by him/her or licensed to him/her and that he/she has the power and authority to grant the Licence. Each time You accept the Licence, the original Licensor and subsequent Contributors grant You a licence to their contributions to the Work, under the terms of this Licence. 7. Disclaimer of Warranty The Work is a work in progress, which is continuously improved by numerous Contributors. It is not a finished work and may therefore contain defects or ‘bugs’ inherent to this type of development. For the above reason, the Work is provided under the Licence on an ‘as is’ basis and without warranties of any kind concerning the Work, including without limitation merchantability, fitness for a particular purpose, absence of defects or errors, accuracy, non-infringement of intellectual property rights other than copyright as stated in Article 6 of this Licence. This disclaimer of warranty is an essential part of the Licence and a condition for the grant of any rights to the Work. 8. Disclaimer of Liability Except in the cases of wilful misconduct or damages directly caused to natural persons, the Licensor will in no event be liable for any direct or indirect, material or moral, damages of any kind, arising out of the Licence or of the use of the Work, including without limitation, damages for loss of goodwill, work stoppage, computer failure or malfunction, loss of data or any commercial damage, even if the Licensor has been advised of the possibility of such damage. However, the Licensor will be liable under statutory product liability laws as far such laws apply to the Work. 9. Additional agreements While distributing the Work, You may choose to conclude an additional agreement, defining obligations or services consistent with this Licence. However, if accepting obligations, You may act only on your own behalf and on your sole responsibility, not on behalf of the original Licensor or any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against such Contributor by the fact You have accepted any warranty or additional liability. 10. Acceptance of the Licence The provisions of this Licence can be accepted by clicking on an icon ‘I agree’ placed under the bottom of a window displaying the text of this Licence or by affirming consent in any other similar way, in accordance with the rules of applicable law. Clicking on that icon indicates your clear and irrevocable acceptance of this Licence and all of its terms and conditions. Similarly, you irrevocably accept this Licence and all of its terms and conditions by exercising any rights granted to You by Article 2 of this Licence, such as the use of the Work, the creation by You of a Derivative Work or the Distribution or Communication by You of the Work or copies thereof. 11. Information to the public In case of any Distribution or Communication of the Work by means of electronic communication by You (for example, by offering to download the Work from a remote location) the distribution channel or media (for example, a website) must at least provide to the public the information requested by the applicable law regarding the Licensor, the Licence and the way it may be accessible, concluded, stored and reproduced by the Licensee. 12. Termination of the Licence The Licence and the rights granted hereunder will terminate automatically upon any breach by the Licensee of the terms of the Licence. Such a termination will not terminate the licences of any person who has received the Work from the Licensee under the Licence, provided such persons remain in full compliance with the Licence. 13. Miscellaneous Without prejudice of Article 9 above, the Licence represents the complete agreement between the Parties as to the Work. If any provision of the Licence is invalid or unenforceable under applicable law, this will not affect the validity or enforceability of the Licence as a whole. Such provision will be construed or reformed so as necessary to make it valid and enforceable. The European Commission may publish other linguistic versions or new versions of this Licence or updated versions of the Appendix, so far this is required and reasonable, without reducing the scope of the rights granted by the Licence. New versions of the Licence will be published with a unique version number. All linguistic versions of this Licence, approved by the European Commission, have identical value. Parties can take advantage of the linguistic version of their choice. 14. Jurisdiction Without prejudice to specific agreement between parties, - any litigation resulting from the interpretation of this License, arising between the European Union institutions, bodies, offices or agencies, as a Licensor, and any Licensee, will be subject to the jurisdiction of the Court of Justice of the European Union, as laid down in article 272 of the Treaty on the Functioning of the European Union, - any litigation arising between other parties and resulting from the interpretation of this License, will be subject to the exclusive jurisdiction of the competent court where the Licensor resides or conducts its primary business. 15. Applicable Law Without prejudice to specific agreement between parties, - this Licence shall be governed by the law of the European Union Member State where the Licensor has his seat, resides or has his registered office, - this licence shall be governed by Belgian law if the Licensor has no seat, residence or registered office inside a European Union Member State. Appendix ‘Compatible Licences’ according to Article 5 EUPL are: - GNU General Public License (GPL) v. 2, v. 3 - GNU Affero General Public License (AGPL) v. 3 - Open Software License (OSL) v. 2.1, v. 3.0 - Eclipse Public License (EPL) v. 1.0 - CeCILL v. 2.0, v. 2.1 - Mozilla Public Licence (MPL) v. 2 - GNU Lesser General Public Licence (LGPL) v. 2.1, v. 3 - Creative Commons Attribution-ShareAlike v. 3.0 Unported (CC BY-SA 3.0) for works other than software - European Union Public Licence (EUPL) v. 1.1, v. 1.2 - Québec Free and Open-Source Licence — Reciprocity (LiLiQ-R) or Strong Reciprocity (LiLiQ-R+). The European Commission may update this Appendix to later versions of the above licences without producing a new version of the EUPL, as long as they provide the rights granted in Article 2 of this Licence and protect the covered Source Code from exclusive appropriation. All other changes or additions to this Appendix require the production of a new EUPL version. ================================================ FILE: boards/adafruit_matrixportal_esp32s3_wled.json ================================================ { "build": { "arduino":{ "ldscript": "esp32s3_out.ld", "partitions": "default_8MB.csv" }, "core": "esp32", "extra_flags": [ "-DARDUINO_ADAFRUIT_MATRIXPORTAL_ESP32S3", "-DARDUINO_USB_CDC_ON_BOOT=1", "-DARDUINO_RUNNING_CORE=1", "-DARDUINO_EVENT_RUNNING_CORE=1", "-DBOARD_HAS_PSRAM" ], "f_cpu": "240000000L", "f_flash": "80000000L", "flash_mode": "qio", "hwids": [ [ "0x239A", "0x8125" ], [ "0x239A", "0x0125" ], [ "0x239A", "0x8126" ] ], "mcu": "esp32s3", "variant": "adafruit_matrixportal_esp32s3" }, "connectivity": [ "bluetooth", "wifi" ], "debug": { "openocd_target": "esp32s3.cfg" }, "frameworks": [ "arduino", "espidf" ], "name": "Adafruit MatrixPortal ESP32-S3 for WLED", "upload": { "flash_size": "8MB", "maximum_ram_size": 327680, "maximum_size": 8388608, "use_1200bps_touch": true, "wait_for_upload_port": true, "require_upload_port": true, "speed": 460800 }, "url": "https://www.adafruit.com/product/5778", "vendor": "Adafruit" } ================================================ FILE: boards/lilygo-t7-s3.json ================================================ { "build": { "arduino":{ "ldscript": "esp32s3_out.ld", "memory_type": "qio_opi", "partitions": "default_16MB.csv" }, "core": "esp32", "extra_flags": [ "-DARDUINO_TTGO_T7_S3", "-DBOARD_HAS_PSRAM", "-DARDUINO_USB_MODE=1" ], "f_cpu": "240000000L", "f_flash": "80000000L", "flash_mode": "qio", "hwids": [ [ "0X303A", "0x1001" ] ], "mcu": "esp32s3", "variant": "esp32s3" }, "connectivity": [ "wifi", "bluetooth" ], "debug": { "openocd_target": "esp32s3.cfg" }, "frameworks": [ "arduino", "espidf" ], "name": "LILYGO T3-S3", "upload": { "flash_size": "16MB", "maximum_ram_size": 327680, "maximum_size": 16777216, "require_upload_port": true, "speed": 921600 }, "url": "https://www.aliexpress.us/item/3256804591247074.html", "vendor": "LILYGO" } ================================================ FILE: boards/lolin_s3_mini.json ================================================ { "build": { "arduino": { "ldscript": "esp32s3_out.ld", "memory_type": "qio_qspi" }, "core": "esp32", "extra_flags": [ "-DBOARD_HAS_PSRAM", "-DARDUINO_LOLIN_S3_MINI", "-DARDUINO_USB_MODE=1" ], "f_cpu": "240000000L", "f_flash": "80000000L", "flash_mode": "qio", "hwids": [ [ "0x303A", "0x8167" ] ], "mcu": "esp32s3", "variant": "lolin_s3_mini" }, "connectivity": [ "bluetooth", "wifi" ], "debug": { "openocd_target": "esp32s3.cfg" }, "frameworks": [ "arduino", "espidf" ], "name": "WEMOS LOLIN S3 Mini", "upload": { "flash_size": "4MB", "maximum_ram_size": 327680, "maximum_size": 4194304, "require_upload_port": true, "speed": 460800 }, "url": "https://www.wemos.cc/en/latest/s3/index.html", "vendor": "WEMOS" } ================================================ FILE: images/Readme.md ================================================ ### Additional Logos Additional awesome logos for WLED can be found here [Aircoookie/Akemi](https://github.com/Aircoookie/Akemi). ================================================ FILE: include/README ================================================ This directory is intended for project header files. A header file is a file containing C declarations and macro definitions to be shared between several project source files. You request the use of a header file in your project source file (C, C++, etc) located in `src` folder by including it, with the C preprocessing directive `#include'. ```src/main.c #include "header.h" int main (void) { ... } ``` Including a header file produces the same results as copying the header file into each source file that needs it. Such copying would be time-consuming and error-prone. With a header file, the related declarations appear in only one place. If they need to be changed, they can be changed in one place, and programs that include the header file will automatically use the new version when next recompiled. The header file eliminates the labor of finding and changing all the copies as well as the risk that a failure to find one copy will result in inconsistencies within a program. In C, the usual convention is to give header files names that end with `.h'. It is most portable to use only letters, digits, dashes, and underscores in header file names, and at most one dot. Read more about using header files in official GCC documentation: * Include Syntax * Include Operation * Once-Only Headers * Computed Includes https://gcc.gnu.org/onlinedocs/cpp/Header-Files.html ================================================ FILE: lib/ESP8266PWM/src/core_esp8266_waveform_phase.cpp ================================================ /* esp8266_waveform imported from platform source code Modified for WLED to work around a fault in the NMI handling, which can result in the system locking up and hard WDT crashes. Imported from https://github.com/esp8266/Arduino/blob/7e0d20e2b9034994f573a236364e0aef17fd66de/cores/esp8266/core_esp8266_waveform_phase.cpp */ /* esp8266_waveform - General purpose waveform generation and control, supporting outputs on all pins in parallel. Copyright (c) 2018 Earle F. Philhower, III. All rights reserved. Copyright (c) 2020 Dirk O. Kaar. The core idea is to have a programmable waveform generator with a unique high and low period (defined in microseconds or CPU clock cycles). TIMER1 is set to 1-shot mode and is always loaded with the time until the next edge of any live waveforms. Up to one waveform generator per pin supported. Each waveform generator is synchronized to the ESP clock cycle counter, not the timer. This allows for removing interrupt jitter and delay as the counter always increments once per 80MHz clock. Changes to a waveform are contiguous and only take effect on the next waveform transition, allowing for smooth transitions. This replaces older tone(), analogWrite(), and the Servo classes. Everywhere in the code where "ccy" or "ccys" is used, it means ESP.getCycleCount() clock cycle time, or an interval measured in clock cycles, but not TIMER1 cycles (which may be 2 CPU clock cycles @ 160MHz). This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2.1 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this library; if not, write to the Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA */ #include "core_esp8266_waveform.h" #include #include "debug.h" #include "ets_sys.h" #include // ----- @willmmiles begin patch ----- // Linker magic extern "C" void usePWMFixedNMI(void) {}; // NMI crash workaround // Sometimes the NMI fails to return, stalling the CPU. When this happens, // the next NMI gets a return address /inside the NMI handler function/. // We work around this by caching the last NMI return address, and restoring // the epc3 and eps3 registers to the previous values if the observed epc3 // happens to be pointing to the _NMILevelVector function. extern "C" void _NMILevelVector(); extern "C" void _UserExceptionVector_1(); // the next function after _NMILevelVector static inline IRAM_ATTR void nmiCrashWorkaround() { static uintptr_t epc3_backup, eps3_backup; uintptr_t epc3, eps3; __asm__ __volatile__("rsr %0,epc3; rsr %1,eps3":"=a"(epc3),"=a" (eps3)); if ((epc3 < (uintptr_t) &_NMILevelVector) || (epc3 >= (uintptr_t) &_UserExceptionVector_1)) { // Address is good; save backup epc3_backup = epc3; eps3_backup = eps3; } else { // Address is inside the NMI handler -- restore from backup __asm__ __volatile__("wsr %0,epc3; wsr %1,eps3"::"a"(epc3_backup),"a"(eps3_backup)); } } // ----- @willmmiles end patch ----- // No-op calls to override the PWM implementation extern "C" void _setPWMFreq_weak(uint32_t freq) { (void) freq; } extern "C" IRAM_ATTR bool _stopPWM_weak(int pin) { (void) pin; return false; } extern "C" bool _setPWM_weak(int pin, uint32_t val, uint32_t range) { (void) pin; (void) val; (void) range; return false; } // Timer is 80MHz fixed. 160MHz CPU frequency need scaling. constexpr bool ISCPUFREQ160MHZ = clockCyclesPerMicrosecond() == 160; // Maximum delay between IRQs, Timer1, <= 2^23 / 80MHz constexpr int32_t MAXIRQTICKSCCYS = microsecondsToClockCycles(10000); // Maximum servicing time for any single IRQ constexpr uint32_t ISRTIMEOUTCCYS = microsecondsToClockCycles(18); // The latency between in-ISR rearming of the timer and the earliest firing constexpr int32_t IRQLATENCYCCYS = microsecondsToClockCycles(2); // The SDK and hardware take some time to actually get to our NMI code constexpr int32_t DELTAIRQCCYS = ISCPUFREQ160MHZ ? microsecondsToClockCycles(2) >> 1 : microsecondsToClockCycles(2); // for INFINITE, the NMI proceeds on the waveform without expiry deadline. // for EXPIRES, the NMI expires the waveform automatically on the expiry ccy. // for UPDATEEXPIRY, the NMI recomputes the exact expiry ccy and transitions to EXPIRES. // for UPDATEPHASE, the NMI recomputes the target timings // for INIT, the NMI initializes nextPeriodCcy, and if expiryCcy != 0 includes UPDATEEXPIRY. enum class WaveformMode : uint8_t {INFINITE = 0, EXPIRES = 1, UPDATEEXPIRY = 2, UPDATEPHASE = 3, INIT = 4}; // Waveform generator can create tones, PWM, and servos typedef struct { uint32_t nextPeriodCcy; // ESP clock cycle when a period begins. uint32_t endDutyCcy; // ESP clock cycle when going from duty to off int32_t dutyCcys; // Set next off cycle at low->high to maintain phase int32_t adjDutyCcys; // Temporary correction for next period int32_t periodCcys; // Set next phase cycle at low->high to maintain phase uint32_t expiryCcy; // For time-limited waveform, the CPU clock cycle when this waveform must stop. If WaveformMode::UPDATE, temporarily holds relative ccy count WaveformMode mode; bool autoPwm; // perform PWM duty to idle cycle ratio correction under high load at the expense of precise timings } Waveform; namespace { static struct { Waveform pins[17]; // State of all possible pins uint32_t states = 0; // Is the pin high or low, updated in NMI so no access outside the NMI code uint32_t enabled = 0; // Is it actively running, updated in NMI so no access outside the NMI code // Enable lock-free by only allowing updates to waveform.states and waveform.enabled from IRQ service routine int32_t toSetBits = 0; // Message to the NMI handler to start/modify exactly one waveform int32_t toDisableBits = 0; // Message to the NMI handler to disable exactly one pin from waveform generation // toSetBits temporaries // cheaper than packing them in every Waveform, since we permit only one use at a time uint32_t phaseCcy; // positive phase offset ccy count int8_t alignPhase; // < 0 no phase alignment, otherwise starts waveform in relative phase offset to given pin uint32_t(*timer1CB)() = nullptr; bool timer1Running = false; uint32_t nextEventCcy; } waveform; } // Interrupt on/off control static IRAM_ATTR void timer1Interrupt(); // Non-speed critical bits #pragma GCC optimize ("Os") static void initTimer() { timer1_disable(); ETS_FRC_TIMER1_INTR_ATTACH(NULL, NULL); ETS_FRC_TIMER1_NMI_INTR_ATTACH(timer1Interrupt); timer1_enable(TIM_DIV1, TIM_EDGE, TIM_SINGLE); waveform.timer1Running = true; timer1_write(IRQLATENCYCCYS); // Cause an interrupt post-haste } static void IRAM_ATTR deinitTimer() { ETS_FRC_TIMER1_NMI_INTR_ATTACH(NULL); timer1_disable(); timer1_isr_init(); waveform.timer1Running = false; } extern "C" { // Set a callback. Pass in NULL to stop it void setTimer1Callback_weak(uint32_t (*fn)()) { waveform.timer1CB = fn; std::atomic_thread_fence(std::memory_order_acq_rel); if (!waveform.timer1Running && fn) { initTimer(); } else if (waveform.timer1Running && !fn && !waveform.enabled) { deinitTimer(); } } // Start up a waveform on a pin, or change the current one. Will change to the new // waveform smoothly on next low->high transition. For immediate change, stopWaveform() // first, then it will immediately begin. int startWaveformClockCycles_weak(uint8_t pin, uint32_t highCcys, uint32_t lowCcys, uint32_t runTimeCcys, int8_t alignPhase, uint32_t phaseOffsetCcys, bool autoPwm) { uint32_t periodCcys = highCcys + lowCcys; if (periodCcys < MAXIRQTICKSCCYS) { if (!highCcys) { periodCcys = (MAXIRQTICKSCCYS / periodCcys) * periodCcys; } else if (!lowCcys) { highCcys = periodCcys = (MAXIRQTICKSCCYS / periodCcys) * periodCcys; } } // sanity checks, including mixed signed/unsigned arithmetic safety if ((pin > 16) || isFlashInterfacePin(pin) || (alignPhase > 16) || static_cast(periodCcys) <= 0 || static_cast(highCcys) < 0 || static_cast(lowCcys) < 0) { return false; } Waveform& wave = waveform.pins[pin]; wave.dutyCcys = highCcys; wave.adjDutyCcys = 0; wave.periodCcys = periodCcys; wave.autoPwm = autoPwm; waveform.alignPhase = (alignPhase < 0) ? -1 : alignPhase; waveform.phaseCcy = phaseOffsetCcys; std::atomic_thread_fence(std::memory_order_acquire); const uint32_t pinBit = 1UL << pin; if (!(waveform.enabled & pinBit)) { // wave.nextPeriodCcy and wave.endDutyCcy are initialized by the ISR wave.expiryCcy = runTimeCcys; // in WaveformMode::INIT, temporarily hold relative cycle count wave.mode = WaveformMode::INIT; if (!wave.dutyCcys) { // If initially at zero duty cycle, force GPIO off if (pin == 16) { GP16O = 0; } else { GPOC = pinBit; } } std::atomic_thread_fence(std::memory_order_release); waveform.toSetBits = 1UL << pin; std::atomic_thread_fence(std::memory_order_release); if (!waveform.timer1Running) { initTimer(); } else if (T1V > IRQLATENCYCCYS) { // Must not interfere if Timer is due shortly timer1_write(IRQLATENCYCCYS); } } else { wave.mode = WaveformMode::INFINITE; // turn off possible expiry to make update atomic from NMI std::atomic_thread_fence(std::memory_order_release); if (runTimeCcys) { wave.expiryCcy = runTimeCcys; // in WaveformMode::UPDATEEXPIRY, temporarily hold relative cycle count wave.mode = WaveformMode::UPDATEEXPIRY; std::atomic_thread_fence(std::memory_order_release); waveform.toSetBits = 1UL << pin; } else if (alignPhase >= 0) { // @willmmiles new feature wave.mode = WaveformMode::UPDATEPHASE; // recalculate start std::atomic_thread_fence(std::memory_order_release); waveform.toSetBits = 1UL << pin; } } std::atomic_thread_fence(std::memory_order_acq_rel); while (waveform.toSetBits) { esp_yield(); // Wait for waveform to update std::atomic_thread_fence(std::memory_order_acquire); } return true; } // Stops a waveform on a pin IRAM_ATTR int stopWaveform_weak(uint8_t pin) { // Can't possibly need to stop anything if there is no timer active if (!waveform.timer1Running) { return false; } // If user sends in a pin >16 but <32, this will always point to a 0 bit // If they send >=32, then the shift will result in 0 and it will also return false std::atomic_thread_fence(std::memory_order_acquire); const uint32_t pinBit = 1UL << pin; if (waveform.enabled & pinBit) { waveform.toDisableBits = 1UL << pin; std::atomic_thread_fence(std::memory_order_release); // Must not interfere if Timer is due shortly if (T1V > IRQLATENCYCCYS) { timer1_write(IRQLATENCYCCYS); } while (waveform.toDisableBits) { /* no-op */ // Can't delay() since stopWaveform may be called from an IRQ std::atomic_thread_fence(std::memory_order_acquire); } } if (!waveform.enabled && !waveform.timer1CB) { deinitTimer(); } return true; } }; // Speed critical bits #pragma GCC optimize ("O2") // For dynamic CPU clock frequency switch in loop the scaling logic would have to be adapted. // Using constexpr makes sure that the CPU clock frequency is compile-time fixed. static inline IRAM_ATTR int32_t scaleCcys(const int32_t ccys, const bool isCPU2X) { if (ISCPUFREQ160MHZ) { return isCPU2X ? ccys : (ccys >> 1); } else { return isCPU2X ? (ccys << 1) : ccys; } } static IRAM_ATTR void timer1Interrupt() { const uint32_t isrStartCcy = ESP.getCycleCount(); //int32_t clockDrift = isrStartCcy - waveform.nextEventCcy; // ----- @willmmiles begin patch ----- nmiCrashWorkaround(); // ----- @willmmiles end patch ----- const bool isCPU2X = CPU2X & 1; if ((waveform.toSetBits && !(waveform.enabled & waveform.toSetBits)) || waveform.toDisableBits) { // Handle enable/disable requests from main app. waveform.enabled = (waveform.enabled & ~waveform.toDisableBits) | waveform.toSetBits; // Set the requested waveforms on/off // Find the first GPIO being generated by checking GCC's find-first-set (returns 1 + the bit of the first 1 in an int32_t) waveform.toDisableBits = 0; } if (waveform.toSetBits) { const int toSetPin = __builtin_ffs(waveform.toSetBits) - 1; Waveform& wave = waveform.pins[toSetPin]; switch (wave.mode) { case WaveformMode::INIT: waveform.states &= ~waveform.toSetBits; // Clear the state of any just started if (waveform.alignPhase >= 0 && waveform.enabled & (1UL << waveform.alignPhase)) { wave.nextPeriodCcy = waveform.pins[waveform.alignPhase].nextPeriodCcy + scaleCcys(waveform.phaseCcy, isCPU2X); } else { wave.nextPeriodCcy = waveform.nextEventCcy; } if (!wave.expiryCcy) { wave.mode = WaveformMode::INFINITE; break; } // fall through case WaveformMode::UPDATEEXPIRY: // in WaveformMode::UPDATEEXPIRY, expiryCcy temporarily holds relative CPU cycle count wave.expiryCcy = wave.nextPeriodCcy + scaleCcys(wave.expiryCcy, isCPU2X); wave.mode = WaveformMode::EXPIRES; break; // @willmmiles new feature case WaveformMode::UPDATEPHASE: // in WaveformMode::UPDATEPHASE, we recalculate the targets if ((waveform.alignPhase >= 0) && (waveform.enabled & (1UL << waveform.alignPhase))) { // Compute phase shift to realign with target auto const newPeriodCcy = waveform.pins[waveform.alignPhase].nextPeriodCcy + scaleCcys(waveform.phaseCcy, isCPU2X); auto const period = scaleCcys(wave.periodCcys, isCPU2X); auto shift = ((static_cast (newPeriodCcy - wave.nextPeriodCcy) + period/2) % period) - (period/2); wave.nextPeriodCcy += static_cast(shift); if (static_cast(wave.endDutyCcy - wave.nextPeriodCcy) > 0) { wave.endDutyCcy = wave.nextPeriodCcy; } } default: break; } waveform.toSetBits = 0; } // Exit the loop if the next event, if any, is sufficiently distant. const uint32_t isrTimeoutCcy = isrStartCcy + ISRTIMEOUTCCYS; uint32_t busyPins = waveform.enabled; waveform.nextEventCcy = isrStartCcy + MAXIRQTICKSCCYS; uint32_t now = ESP.getCycleCount(); uint32_t isrNextEventCcy = now; while (busyPins) { if (static_cast(isrNextEventCcy - now) > IRQLATENCYCCYS) { waveform.nextEventCcy = isrNextEventCcy; break; } isrNextEventCcy = waveform.nextEventCcy; uint32_t loopPins = busyPins; while (loopPins) { const int pin = __builtin_ffsl(loopPins) - 1; const uint32_t pinBit = 1UL << pin; loopPins ^= pinBit; Waveform& wave = waveform.pins[pin]; /* @willmmiles - wtf? We don't want to accumulate drift if (clockDrift) { wave.endDutyCcy += clockDrift; wave.nextPeriodCcy += clockDrift; wave.expiryCcy += clockDrift; } */ uint32_t waveNextEventCcy = (waveform.states & pinBit) ? wave.endDutyCcy : wave.nextPeriodCcy; if (WaveformMode::EXPIRES == wave.mode && static_cast(waveNextEventCcy - wave.expiryCcy) >= 0 && static_cast(now - wave.expiryCcy) >= 0) { // Disable any waveforms that are done waveform.enabled ^= pinBit; busyPins ^= pinBit; } else { const int32_t overshootCcys = now - waveNextEventCcy; if (overshootCcys >= 0) { const int32_t periodCcys = scaleCcys(wave.periodCcys, isCPU2X); if (waveform.states & pinBit) { // active configuration and forward are 100% duty if (wave.periodCcys == wave.dutyCcys) { wave.nextPeriodCcy += periodCcys; wave.endDutyCcy = wave.nextPeriodCcy; } else { if (wave.autoPwm) { wave.adjDutyCcys += overshootCcys; } waveform.states ^= pinBit; if (16 == pin) { GP16O = 0; } else { GPOC = pinBit; } } waveNextEventCcy = wave.nextPeriodCcy; } else { wave.nextPeriodCcy += periodCcys; if (!wave.dutyCcys) { wave.endDutyCcy = wave.nextPeriodCcy; } else { int32_t dutyCcys = scaleCcys(wave.dutyCcys, isCPU2X); if (dutyCcys <= wave.adjDutyCcys) { dutyCcys >>= 1; wave.adjDutyCcys -= dutyCcys; } else if (wave.adjDutyCcys) { dutyCcys -= wave.adjDutyCcys; wave.adjDutyCcys = 0; } wave.endDutyCcy = now + dutyCcys; if (static_cast(wave.endDutyCcy - wave.nextPeriodCcy) > 0) { wave.endDutyCcy = wave.nextPeriodCcy; } waveform.states |= pinBit; if (16 == pin) { GP16O = 1; } else { GPOS = pinBit; } } waveNextEventCcy = wave.endDutyCcy; } if (WaveformMode::EXPIRES == wave.mode && static_cast(waveNextEventCcy - wave.expiryCcy) > 0) { waveNextEventCcy = wave.expiryCcy; } } if (static_cast(waveNextEventCcy - isrTimeoutCcy) >= 0) { busyPins ^= pinBit; if (static_cast(waveform.nextEventCcy - waveNextEventCcy) > 0) { waveform.nextEventCcy = waveNextEventCcy; } } else if (static_cast(isrNextEventCcy - waveNextEventCcy) > 0) { isrNextEventCcy = waveNextEventCcy; } } now = ESP.getCycleCount(); } //clockDrift = 0; } int32_t callbackCcys = 0; if (waveform.timer1CB) { callbackCcys = scaleCcys(waveform.timer1CB(), isCPU2X); } now = ESP.getCycleCount(); int32_t nextEventCcys = waveform.nextEventCcy - now; // Account for unknown duration of timer1CB(). if (waveform.timer1CB && nextEventCcys > callbackCcys) { waveform.nextEventCcy = now + callbackCcys; nextEventCcys = callbackCcys; } // Timer is 80MHz fixed. 160MHz CPU frequency need scaling. int32_t deltaIrqCcys = DELTAIRQCCYS; int32_t irqLatencyCcys = IRQLATENCYCCYS; if (isCPU2X) { nextEventCcys >>= 1; deltaIrqCcys >>= 1; irqLatencyCcys >>= 1; } // Firing timer too soon, the NMI occurs before ISR has returned. if (nextEventCcys < irqLatencyCcys + deltaIrqCcys) { waveform.nextEventCcy = now + IRQLATENCYCCYS + DELTAIRQCCYS; nextEventCcys = irqLatencyCcys; } else { nextEventCcys -= deltaIrqCcys; } // Register access is fast and edge IRQ was configured before. T1L = nextEventCcys; } ================================================ FILE: lib/NeoESP32RmtHI/include/NeoEsp32RmtHIMethod.h ================================================ /*------------------------------------------------------------------------- NeoPixel driver for ESP32 RMTs using High-priority Interrupt (NB. This cannot be mixed with the non-HI driver.) Written by Will M. Miles. I invest time and resources providing this open source code, please support me by donating (see https://github.com/Makuna/NeoPixelBus) ------------------------------------------------------------------------- This file is part of the Makuna/NeoPixelBus library. NeoPixelBus is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. NeoPixelBus is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with NeoPixel. If not, see . -------------------------------------------------------------------------*/ #pragma once #if defined(ARDUINO_ARCH_ESP32) // Use the NeoEspRmtSpeed types from the driver-based implementation #include namespace NeoEsp32RmtHiMethodDriver { // Install the driver for a specific channel, specifying timing properties esp_err_t Install(rmt_channel_t channel, uint32_t rmtBit0, uint32_t rmtBit1, uint32_t resetDuration); // Remove the driver on a specific channel esp_err_t Uninstall(rmt_channel_t channel); // Write a buffer of data to a specific channel. // Buffer reference is held until write completes. esp_err_t Write(rmt_channel_t channel, const uint8_t *src, size_t src_size); // Wait until transaction is complete. esp_err_t WaitForTxDone(rmt_channel_t channel, TickType_t wait_time); }; template class NeoEsp32RmtHIMethodBase { public: typedef NeoNoSettings SettingsObject; NeoEsp32RmtHIMethodBase(uint8_t pin, uint16_t pixelCount, size_t elementSize, size_t settingsSize) : _sizeData(pixelCount * elementSize + settingsSize), _pin(pin) { construct(); } NeoEsp32RmtHIMethodBase(uint8_t pin, uint16_t pixelCount, size_t elementSize, size_t settingsSize, NeoBusChannel channel) : _sizeData(pixelCount* elementSize + settingsSize), _pin(pin), _channel(channel) { construct(); } ~NeoEsp32RmtHIMethodBase() { // wait until the last send finishes before destructing everything // arbitrary time out of 10 seconds ESP_ERROR_CHECK_WITHOUT_ABORT(NeoEsp32RmtHiMethodDriver::WaitForTxDone(_channel.RmtChannelNumber, 10000 / portTICK_PERIOD_MS)); ESP_ERROR_CHECK(NeoEsp32RmtHiMethodDriver::Uninstall(_channel.RmtChannelNumber)); gpio_matrix_out(_pin, SIG_GPIO_OUT_IDX, false, false); pinMode(_pin, INPUT); free(_dataEditing); free(_dataSending); } bool IsReadyToUpdate() const { return (ESP_OK == ESP_ERROR_CHECK_WITHOUT_ABORT_SILENT_TIMEOUT(NeoEsp32RmtHiMethodDriver::WaitForTxDone(_channel.RmtChannelNumber, 0))); } void Initialize() { rmt_config_t config = {}; config.rmt_mode = RMT_MODE_TX; config.channel = _channel.RmtChannelNumber; config.gpio_num = static_cast(_pin); config.mem_block_num = 1; config.tx_config.loop_en = false; config.tx_config.idle_output_en = true; config.tx_config.idle_level = T_SPEED::IdleLevel; config.tx_config.carrier_en = false; config.tx_config.carrier_level = RMT_CARRIER_LEVEL_LOW; config.clk_div = T_SPEED::RmtClockDivider; ESP_ERROR_CHECK(rmt_config(&config)); // Uses ESP library ESP_ERROR_CHECK(NeoEsp32RmtHiMethodDriver::Install(_channel.RmtChannelNumber, T_SPEED::RmtBit0, T_SPEED::RmtBit1, T_SPEED::RmtDurationReset)); } void Update(bool maintainBufferConsistency) { // wait for not actively sending data // this will time out at 10 seconds, an arbitrarily long period of time // and do nothing if this happens if (ESP_OK == ESP_ERROR_CHECK_WITHOUT_ABORT(NeoEsp32RmtHiMethodDriver::WaitForTxDone(_channel.RmtChannelNumber, 10000 / portTICK_PERIOD_MS))) { // now start the RMT transmit with the editing buffer before we swap ESP_ERROR_CHECK_WITHOUT_ABORT(NeoEsp32RmtHiMethodDriver::Write(_channel.RmtChannelNumber, _dataEditing, _sizeData)); if (maintainBufferConsistency) { // copy editing to sending, // this maintains the contract that "colors present before will // be the same after", otherwise GetPixelColor will be inconsistent memcpy(_dataSending, _dataEditing, _sizeData); } // swap so the user can modify without affecting the async operation std::swap(_dataSending, _dataEditing); } } bool AlwaysUpdate() { // this method requires update to be called only if changes to buffer return false; } bool SwapBuffers() { std::swap(_dataSending, _dataEditing); return true; } uint8_t* getData() const { return _dataEditing; }; size_t getDataSize() const { return _sizeData; } void applySettings([[maybe_unused]] const SettingsObject& settings) { } private: const size_t _sizeData; // Size of '_data*' buffers const uint8_t _pin; // output pin number const T_CHANNEL _channel; // holds instance for multi channel support // Holds data stream which include LED color values and other settings as needed uint8_t* _dataEditing; // exposed for get and set uint8_t* _dataSending; // used for async send using RMT void construct() { _dataEditing = static_cast(malloc(_sizeData)); // data cleared later in Begin() _dataSending = static_cast(malloc(_sizeData)); // no need to initialize it, it gets overwritten on every send } }; // normal typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHINWs2811Method; typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHINWs2812xMethod; typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHINWs2816Method; typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHINWs2805Method; typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHINSk6812Method; typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHINTm1814Method; typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHINTm1829Method; typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHINTm1914Method; typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHINApa106Method; typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHINTx1812Method; typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHINGs1903Method; typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHIN800KbpsMethod; typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHIN400KbpsMethod; typedef NeoEsp32RmtHINWs2805Method NeoEsp32RmtHINWs2814Method; typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI0Ws2811Method; typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI0Ws2812xMethod; typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI0Ws2816Method; typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI0Ws2805Method; typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI0Sk6812Method; typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI0Tm1814Method; typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI0Tm1829Method; typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI0Tm1914Method; typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI0Apa106Method; typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI0Tx1812Method; typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI0Gs1903Method; typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI0800KbpsMethod; typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI0400KbpsMethod; typedef NeoEsp32RmtHI0Ws2805Method NeoEsp32RmtHI0Ws2814Method; typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI1Ws2811Method; typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI1Ws2812xMethod; typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI1Ws2816Method; typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI1Ws2805Method; typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI1Sk6812Method; typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI1Tm1814Method; typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI1Tm1829Method; typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI1Tm1914Method; typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI1Apa106Method; typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI1Tx1812Method; typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI1Gs1903Method; typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI1800KbpsMethod; typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI1400KbpsMethod; typedef NeoEsp32RmtHI1Ws2805Method NeoEsp32RmtHI1Ws2814Method; #if !defined(CONFIG_IDF_TARGET_ESP32C3) typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI2Ws2811Method; typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI2Ws2812xMethod; typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI2Ws2816Method; typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI2Ws2805Method; typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI2Sk6812Method; typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI2Tm1814Method; typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI2Tm1829Method; typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI2Tm1914Method; typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI2Apa106Method; typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI2Tx1812Method; typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI2Gs1903Method; typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI2800KbpsMethod; typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI2400KbpsMethod; typedef NeoEsp32RmtHI2Ws2805Method NeoEsp32RmtHI2Ws2814Method; typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI3Ws2811Method; typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI3Ws2812xMethod; typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI3Ws2816Method; typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI3Ws2805Method; typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI3Sk6812Method; typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI3Tm1814Method; typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI3Tm1829Method; typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI3Tm1914Method; typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI3Apa106Method; typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI3Tx1812Method; typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI3Gs1903Method; typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI3800KbpsMethod; typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI3400KbpsMethod; typedef NeoEsp32RmtHI3Ws2805Method NeoEsp32RmtHI3Ws2814Method; #if !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32S3) typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI4Ws2811Method; typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI4Ws2812xMethod; typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI4Ws2816Method; typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI4Ws2805Method; typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI4Sk6812Method; typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI4Tm1814Method; typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI4Tm1829Method; typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI4Tm1914Method; typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI4Apa106Method; typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI4Tx1812Method; typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI4Gs1903Method; typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI4800KbpsMethod; typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI4400KbpsMethod; typedef NeoEsp32RmtHI4Ws2805Method NeoEsp32RmtHI4Ws2814Method; typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI5Ws2811Method; typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI5Ws2812xMethod; typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI5Ws2816Method; typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI5Ws2805Method; typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI5Sk6812Method; typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI5Tm1814Method; typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI5Tm1829Method; typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI5Tm1914Method; typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI5Apa106Method; typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI5Tx1812Method; typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI5Gs1903Method; typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI5800KbpsMethod; typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI5400KbpsMethod; typedef NeoEsp32RmtHI5Ws2805Method NeoEsp32RmtHI5Ws2814Method; typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI6Ws2811Method; typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI6Ws2812xMethod; typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI6Ws2816Method; typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI6Ws2805Method; typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI6Sk6812Method; typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI6Tm1814Method; typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI6Tm1829Method; typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI6Tm1914Method; typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI6Apa106Method; typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI6Tx1812Method; typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI6Gs1903Method; typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI6800KbpsMethod; typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI6400KbpsMethod; typedef NeoEsp32RmtHI6Ws2805Method NeoEsp32RmtHI6Ws2814Method; typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI7Ws2811Method; typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI7Ws2812xMethod; typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI7Ws2816Method; typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI7Ws2805Method; typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI7Sk6812Method; typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI7Tm1814Method; typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI7Tm1829Method; typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI7Tm1914Method; typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI7Apa106Method; typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI7Tx1812Method; typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI7Gs1903Method; typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI7800KbpsMethod; typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI7400KbpsMethod; typedef NeoEsp32RmtHI7Ws2805Method NeoEsp32RmtHI7Ws2814Method; #endif // !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32S3) #endif // !defined(CONFIG_IDF_TARGET_ESP32C3) // inverted typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHINWs2811InvertedMethod; typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHINWs2812xInvertedMethod; typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHINWs2816InvertedMethod; typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHINWs2805InvertedMethod; typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHINSk6812InvertedMethod; typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHINTm1814InvertedMethod; typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHINTm1829InvertedMethod; typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHINTm1914InvertedMethod; typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHINApa106InvertedMethod; typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHINTx1812InvertedMethod; typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHINGs1903InvertedMethod; typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHIN800KbpsInvertedMethod; typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHIN400KbpsInvertedMethod; typedef NeoEsp32RmtHINWs2805InvertedMethod NeoEsp32RmtHINWs2814InvertedMethod; typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI0Ws2811InvertedMethod; typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI0Ws2812xInvertedMethod; typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI0Ws2816InvertedMethod; typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI0Ws2805InvertedMethod; typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI0Sk6812InvertedMethod; typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI0Tm1814InvertedMethod; typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI0Tm1829InvertedMethod; typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI0Tm1914InvertedMethod; typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI0Apa106InvertedMethod; typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI0Tx1812InvertedMethod; typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI0Gs1903InvertedMethod; typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI0800KbpsInvertedMethod; typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI0400KbpsInvertedMethod; typedef NeoEsp32RmtHI0Ws2805InvertedMethod NeoEsp32RmtHI0Ws2814InvertedMethod; typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI1Ws2811InvertedMethod; typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI1Ws2812xInvertedMethod; typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI1Ws2816InvertedMethod; typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI1Ws2805InvertedMethod; typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI1Sk6812InvertedMethod; typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI1Tm1814InvertedMethod; typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI1Tm1829InvertedMethod; typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI1Tm1914InvertedMethod; typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI1Apa106InvertedMethod; typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI1Tx1812InvertedMethod; typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI1Gs1903InvertedMethod; typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI1800KbpsInvertedMethod; typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI1400KbpsInvertedMethod; typedef NeoEsp32RmtHI1Ws2805InvertedMethod NeoEsp32RmtHI1Ws2814InvertedMethod; #if !defined(CONFIG_IDF_TARGET_ESP32C3) typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI2Ws2811InvertedMethod; typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI2Ws2812xInvertedMethod; typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI2Ws2816InvertedMethod; typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI2Ws2805InvertedMethod; typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI2Sk6812InvertedMethod; typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI2Tm1814InvertedMethod; typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI2Tm1829InvertedMethod; typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI2Tm1914InvertedMethod; typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI2Apa106InvertedMethod; typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI2Tx1812InvertedMethod; typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI2Gs1903InvertedMethod; typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI2800KbpsInvertedMethod; typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI2400KbpsInvertedMethod; typedef NeoEsp32RmtHI2Ws2805InvertedMethod NeoEsp32RmtHI2Ws2814InvertedMethod; typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI3Ws2811InvertedMethod; typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI3Ws2812xInvertedMethod; typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI3Ws2805InvertedMethod; typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI3Ws2816InvertedMethod; typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI3Sk6812InvertedMethod; typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI3Tm1814InvertedMethod; typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI3Tm1829InvertedMethod; typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI3Tm1914InvertedMethod; typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI3Apa106InvertedMethod; typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI3Tx1812InvertedMethod; typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI3Gs1903InvertedMethod; typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI3800KbpsInvertedMethod; typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI3400KbpsInvertedMethod; typedef NeoEsp32RmtHI3Ws2805InvertedMethod NeoEsp32RmtHI3Ws2814InvertedMethod; #if !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32S3) typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI4Ws2811InvertedMethod; typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI4Ws2812xInvertedMethod; typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI4Ws2816InvertedMethod; typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI4Ws2805InvertedMethod; typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI4Sk6812InvertedMethod; typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI4Tm1814InvertedMethod; typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI4Tm1829InvertedMethod; typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI4Tm1914InvertedMethod; typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI4Apa106InvertedMethod; typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI4Tx1812InvertedMethod; typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI4Gs1903InvertedMethod; typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI4800KbpsInvertedMethod; typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI4400KbpsInvertedMethod; typedef NeoEsp32RmtHI4Ws2805InvertedMethod NeoEsp32RmtHI4Ws2814InvertedMethod; typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI5Ws2811InvertedMethod; typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI5Ws2812xInvertedMethod; typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI5Ws2816InvertedMethod; typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI5Ws2805InvertedMethod; typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI5Sk6812InvertedMethod; typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI5Tm1814InvertedMethod; typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI5Tm1829InvertedMethod; typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI5Tm1914InvertedMethod; typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI5Apa106InvertedMethod; typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI5Tx1812InvertedMethod; typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI5Gs1903InvertedMethod; typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI5800KbpsInvertedMethod; typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI5400KbpsInvertedMethod; typedef NeoEsp32RmtHI5Ws2805InvertedMethod NeoEsp32RmtHI5Ws2814InvertedMethod; typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI6Ws2811InvertedMethod; typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI6Ws2812xInvertedMethod; typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI6Ws2816InvertedMethod; typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI6Ws2805InvertedMethod; typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI6Sk6812InvertedMethod; typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI6Tm1814InvertedMethod; typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI6Tm1829InvertedMethod; typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI6Tm1914InvertedMethod; typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI6Apa106InvertedMethod; typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI6Tx1812InvertedMethod; typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI6Gs1903InvertedMethod; typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI6800KbpsInvertedMethod; typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI6400KbpsInvertedMethod; typedef NeoEsp32RmtHI6Ws2805InvertedMethod NeoEsp32RmtHI6Ws2814InvertedMethod; typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI7Ws2811InvertedMethod; typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI7Ws2812xInvertedMethod; typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI7Ws2816InvertedMethod; typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI7Ws2805InvertedMethod; typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI7Sk6812InvertedMethod; typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI7Tm1814InvertedMethod; typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI7Tm1829InvertedMethod; typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI7Tm1914InvertedMethod; typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI7Apa106InvertedMethod; typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI7Tx1812InvertedMethod; typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI7Gs1903InvertedMethod; typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI7800KbpsInvertedMethod; typedef NeoEsp32RmtHIMethodBase NeoEsp32RmtHI7400KbpsInvertedMethod; typedef NeoEsp32RmtHI7Ws2805InvertedMethod NeoEsp32RmtHI7Ws2814InvertedMethod; #endif // !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32S3) #endif // !defined(CONFIG_IDF_TARGET_ESP32C3) #endif ================================================ FILE: lib/NeoESP32RmtHI/library.json ================================================ { "name": "NeoESP32RmtHI", "build": { "libArchive": false }, "platforms": ["espressif32"], "dependencies": [ { "owner": "makuna", "name": "NeoPixelBus", "version": "^2.8.3" } ] } ================================================ FILE: lib/NeoESP32RmtHI/src/NeoEsp32RmtHI.S ================================================ /* RMT ISR shim * Bridges from a high-level interrupt to the C++ code. * * This code is largely derived from Espressif's 'hli_vector.S' Bluetooth ISR. * */ #if defined(__XTENSA__) && defined(ESP32) && !defined(CONFIG_BTDM_CTRL_HLI) #include #include "sdkconfig.h" #include "soc/soc.h" /* If the Bluetooth driver has hooked the high-priority interrupt, we piggyback on it and don't need this. */ #ifndef CONFIG_BTDM_CTRL_HLI /* Select interrupt based on system check level - Base ESP32: could be 4 or 5, depends on platform config - S2: 5 - S3: 5 */ #if CONFIG_ESP_SYSTEM_CHECK_INT_LEVEL_5 /* Use level 4 */ #define RFI_X 4 #define xt_highintx xt_highint4 #else /* !CONFIG_ESP_SYSTEM_CHECK_INT_LEVEL_5 */ /* Use level 5 */ #define RFI_X 5 #define xt_highintx xt_highint5 #endif /* CONFIG_ESP_SYSTEM_CHECK_INT_LEVEL_5 */ // Register map, based on interrupt level #define EPC_X (EPC + RFI_X) #define EXCSAVE_X (EXCSAVE + RFI_X) // The sp mnemonic is used all over in ESP's assembly, though I'm not sure where it's expected to be defined? #define sp a1 /* Interrupt stack size, for C code. */ #define RMT_INTR_STACK_SIZE 512 /* Save area for the CPU state: * - 64 words for the general purpose registers * - 7 words for some of the special registers: * - WINDOWBASE, WINDOWSTART — only WINDOWSTART is truly needed * - SAR, LBEG, LEND, LCOUNT — since the C code might use these * - EPC1 — since the C code might cause window overflow exceptions * This is not laid out as standard exception frame structure * for simplicity of the save/restore code. */ #define REG_FILE_SIZE (64 * 4) #define SPECREG_OFFSET REG_FILE_SIZE #define SPECREG_SIZE (7 * 4) #define REG_SAVE_AREA_SIZE (SPECREG_OFFSET + SPECREG_SIZE) .data _rmt_intr_stack: .space RMT_INTR_STACK_SIZE _rmt_save_ctx: .space REG_SAVE_AREA_SIZE .section .iram1,"ax" .global xt_highintx .type xt_highintx,@function .align 4 xt_highintx: movi a0, _rmt_save_ctx /* save 4 lower registers */ s32i a1, a0, 4 s32i a2, a0, 8 s32i a3, a0, 12 rsr a2, EXCSAVE_X /* holds the value of a0 */ s32i a2, a0, 0 /* Save special registers */ addi a0, a0, SPECREG_OFFSET rsr a2, WINDOWBASE s32i a2, a0, 0 rsr a2, WINDOWSTART s32i a2, a0, 4 rsr a2, SAR s32i a2, a0, 8 #if XCHAL_HAVE_LOOPS rsr a2, LBEG s32i a2, a0, 12 rsr a2, LEND s32i a2, a0, 16 rsr a2, LCOUNT s32i a2, a0, 20 #endif rsr a2, EPC1 s32i a2, a0, 24 /* disable exception mode, window overflow */ movi a0, PS_INTLEVEL(RFI_X+1) | PS_EXCM wsr a0, PS rsync /* Save the remaining physical registers. * 4 registers are already saved, which leaves 60 registers to save. * (FIXME: consider the case when the CPU is configured with physical 32 registers) * These 60 registers are saved in 5 iterations, 12 registers at a time. */ movi a1, 5 movi a3, _rmt_save_ctx + 4 * 4 /* This is repeated 5 times, each time the window is shifted by 12 registers. * We come here with a1 = downcounter, a3 = save pointer, a2 and a0 unused. */ 1: s32i a4, a3, 0 s32i a5, a3, 4 s32i a6, a3, 8 s32i a7, a3, 12 s32i a8, a3, 16 s32i a9, a3, 20 s32i a10, a3, 24 s32i a11, a3, 28 s32i a12, a3, 32 s32i a13, a3, 36 s32i a14, a3, 40 s32i a15, a3, 44 /* We are about to rotate the window, so that a12-a15 will become the new a0-a3. * Copy a0-a3 to a12-15 to still have access to these values. * At the same time we can decrement the counter and adjust the save area pointer */ /* a0 is constant (_rmt_save_ctx), no need to copy */ addi a13, a1, -1 /* copy and decrement the downcounter */ /* a2 is scratch so no need to copy */ addi a15, a3, 48 /* copy and adjust the save area pointer */ beqz a13, 2f /* have saved all registers ? */ rotw 3 /* rotate the window and go back */ j 1b /* the loop is complete */ 2: rotw 4 /* this brings us back to the original window */ /* a0 still points to _rmt_save_ctx */ /* Can clear WINDOWSTART now, all registers are saved */ rsr a2, WINDOWBASE /* WINDOWSTART = (1 << WINDOWBASE) */ movi a3, 1 ssl a2 sll a3, a3 wsr a3, WINDOWSTART _highint_stack_switch: movi a0, 0 movi sp, _rmt_intr_stack + RMT_INTR_STACK_SIZE - 16 s32e a0, sp, -12 /* For GDB: set null SP */ s32e a0, sp, -16 /* For GDB: set null PC */ movi a0, _highint_stack_switch /* For GDB: cosmetics, for the frame where stack switch happened */ /* Set up PS for C, disable all interrupts except NMI and debug, and clear EXCM. */ movi a6, PS_INTLEVEL(RFI_X) | PS_UM | PS_WOE wsr a6, PS rsync /* Call C handler */ mov a6, sp call4 NeoEsp32RmtMethodIsr l32e sp, sp, -12 /* switch back to the original stack */ /* Done with C handler; re-enable exception mode, disabling window overflow */ movi a2, PS_INTLEVEL(RFI_X+1) | PS_EXCM /* TOCHECK */ wsr a2, PS rsync /* Restore the special registers. * WINDOWSTART will be restored near the end. */ movi a0, _rmt_save_ctx + SPECREG_OFFSET l32i a2, a0, 8 wsr a2, SAR #if XCHAL_HAVE_LOOPS l32i a2, a0, 12 wsr a2, LBEG l32i a2, a0, 16 wsr a2, LEND l32i a2, a0, 20 wsr a2, LCOUNT #endif l32i a2, a0, 24 wsr a2, EPC1 /* Restoring the physical registers. * This is the reverse to the saving process above. */ /* Rotate back to the final window, then start loading 12 registers at a time, * in 5 iterations. * Again, a1 is the downcounter and a3 is the save area pointer. * After each rotation, a1 and a3 are copied from a13 and a15. * To simplify the loop, we put the initial values into a13 and a15. */ rotw -4 movi a15, _rmt_save_ctx + 64 * 4 /* point to the end of the save area */ movi a13, 5 1: /* Copy a1 and a3 from their previous location, * at the same time decrementing and adjusting the save area pointer. */ addi a1, a13, -1 addi a3, a15, -48 /* Load 12 registers */ l32i a4, a3, 0 l32i a5, a3, 4 l32i a6, a3, 8 l32i a7, a3, 12 l32i a8, a3, 16 l32i a9, a3, 20 l32i a10, a3, 24 l32i a11, a3, 28 /* ensure PS and EPC written */ l32i a12, a3, 32 l32i a13, a3, 36 l32i a14, a3, 40 l32i a15, a3, 44 /* Done with the loop? */ beqz a1, 2f /* If no, rotate the window and repeat */ rotw -3 j 1b 2: /* Done with the loop. Only 4 registers (a0-a3 in the original window) remain * to be restored. Also need to restore WINDOWSTART, since all the general * registers are now in place. */ movi a0, _rmt_save_ctx l32i a2, a0, SPECREG_OFFSET + 4 wsr a2, WINDOWSTART l32i a1, a0, 4 l32i a2, a0, 8 l32i a3, a0, 12 rsr a0, EXCSAVE_X /* holds the value of a0 before the interrupt handler */ /* Return from the interrupt, restoring PS from EPS_X */ rfi RFI_X /* The linker has no reason to link in this file; all symbols it exports are already defined (weakly!) in the default int handler. Define a symbol here so we can use it to have the linker inspect this anyway. */ .global ld_include_hli_vectors_rmt ld_include_hli_vectors_rmt: #endif // CONFIG_BTDM_CTRL_HLI #endif // XTensa ================================================ FILE: lib/NeoESP32RmtHI/src/NeoEsp32RmtHIMethod.cpp ================================================ /*------------------------------------------------------------------------- NeoPixel library helper functions for Esp32. A BIG thanks to Andreas Merkle for the investigation and implementation of a workaround to the GCC bug that drops method attributes from template methods Written by Michael C. Miller. I invest time and resources providing this open source code, please support me by donating (see https://github.com/Makuna/NeoPixelBus) ------------------------------------------------------------------------- This file is part of the Makuna/NeoPixelBus library. NeoPixelBus is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. NeoPixelBus is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with NeoPixel. If not, see . -------------------------------------------------------------------------*/ #include #if defined(ARDUINO_ARCH_ESP32) #include #include "esp_idf_version.h" #include "NeoEsp32RmtHIMethod.h" #include "soc/soc.h" #include "soc/rmt_reg.h" #ifdef __riscv #include "riscv/interrupt.h" #endif #if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(4, 0, 0) #include "hal/rmt_ll.h" #else /* Shims for older ESP-IDF v3; we can safely assume original ESP32 */ #include "soc/rmt_struct.h" // Selected RMT API functions borrowed from ESP-IDF v4.4.8 // components/hal/esp32/include/hal/rmt_ll.h // Copyright 2019 Espressif Systems (Shanghai) PTE LTD // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. __attribute__((always_inline)) static inline void rmt_ll_tx_reset_pointer(rmt_dev_t *dev, uint32_t channel) { dev->conf_ch[channel].conf1.mem_rd_rst = 1; dev->conf_ch[channel].conf1.mem_rd_rst = 0; } __attribute__((always_inline)) static inline void rmt_ll_tx_start(rmt_dev_t *dev, uint32_t channel) { dev->conf_ch[channel].conf1.tx_start = 1; } __attribute__((always_inline)) static inline void rmt_ll_tx_stop(rmt_dev_t *dev, uint32_t channel) { RMTMEM.chan[channel].data32[0].val = 0; dev->conf_ch[channel].conf1.tx_start = 0; dev->conf_ch[channel].conf1.mem_rd_rst = 1; dev->conf_ch[channel].conf1.mem_rd_rst = 0; } __attribute__((always_inline)) static inline void rmt_ll_tx_enable_pingpong(rmt_dev_t *dev, uint32_t channel, bool enable) { dev->apb_conf.mem_tx_wrap_en = enable; } __attribute__((always_inline)) static inline void rmt_ll_tx_enable_loop(rmt_dev_t *dev, uint32_t channel, bool enable) { dev->conf_ch[channel].conf1.tx_conti_mode = enable; } __attribute__((always_inline)) static inline uint32_t rmt_ll_tx_get_channel_status(rmt_dev_t *dev, uint32_t channel) { return dev->status_ch[channel]; } __attribute__((always_inline)) static inline void rmt_ll_tx_set_limit(rmt_dev_t *dev, uint32_t channel, uint32_t limit) { dev->tx_lim_ch[channel].limit = limit; } __attribute__((always_inline)) static inline void rmt_ll_enable_interrupt(rmt_dev_t *dev, uint32_t mask, bool enable) { if (enable) { dev->int_ena.val |= mask; } else { dev->int_ena.val &= ~mask; } } __attribute__((always_inline)) static inline void rmt_ll_enable_tx_end_interrupt(rmt_dev_t *dev, uint32_t channel, bool enable) { dev->int_ena.val &= ~(1 << (channel * 3)); dev->int_ena.val |= (enable << (channel * 3)); } __attribute__((always_inline)) static inline void rmt_ll_enable_tx_err_interrupt(rmt_dev_t *dev, uint32_t channel, bool enable) { dev->int_ena.val &= ~(1 << (channel * 3 + 2)); dev->int_ena.val |= (enable << (channel * 3 + 2)); } __attribute__((always_inline)) static inline void rmt_ll_enable_tx_thres_interrupt(rmt_dev_t *dev, uint32_t channel, bool enable) { dev->int_ena.val &= ~(1 << (channel + 24)); dev->int_ena.val |= (enable << (channel + 24)); } __attribute__((always_inline)) static inline void rmt_ll_clear_tx_end_interrupt(rmt_dev_t *dev, uint32_t channel) { dev->int_clr.val = (1 << (channel * 3)); } __attribute__((always_inline)) static inline void rmt_ll_clear_tx_err_interrupt(rmt_dev_t *dev, uint32_t channel) { dev->int_clr.val = (1 << (channel * 3 + 2)); } __attribute__((always_inline)) static inline void rmt_ll_clear_tx_thres_interrupt(rmt_dev_t *dev, uint32_t channel) { dev->int_clr.val = (1 << (channel + 24)); } __attribute__((always_inline)) static inline uint32_t rmt_ll_get_tx_thres_interrupt_status(rmt_dev_t *dev) { uint32_t status = dev->int_st.val; return (status & 0xFF000000) >> 24; } #endif // ********************************* // Select method for binding interrupt // // - If the Bluetooth driver has registered a high-level interrupt, piggyback on that API // - If we're on a modern core, allocate the interrupt with the API (old cores are bugged) // - Otherwise use the low-level hardware API to manually bind the interrupt #if defined(CONFIG_BTDM_CTRL_HLI) // Espressif's bluetooth driver offers a helpful sharing layer; bring in the interrupt management calls #include "hal/interrupt_controller_hal.h" extern "C" esp_err_t hli_intr_register(intr_handler_t handler, void* arg, uint32_t intr_reg, uint32_t intr_mask); #else /* !CONFIG_BTDM_CTRL_HLI*/ // Declare the our high-priority ISR handler extern "C" void ld_include_hli_vectors_rmt(); // an object with an address, but no space #if defined(CONFIG_IDF_TARGET_ESP32S2) || defined(CONFIG_IDF_TARGET_ESP32S3) || defined(CONFIG_IDF_TARGET_ESP32C3) #include "soc/periph_defs.h" #endif // Select level flag #if defined(__riscv) // RISCV chips don't block interrupts while scheduling; all we need to do is be higher than the WiFi ISR #define INT_LEVEL_FLAG ESP_INTR_FLAG_LEVEL3 #elif defined(CONFIG_ESP_SYSTEM_CHECK_INT_LEVEL_5) #define INT_LEVEL_FLAG ESP_INTR_FLAG_LEVEL4 #else #define INT_LEVEL_FLAG ESP_INTR_FLAG_LEVEL5 #endif // ESP-IDF v3 cannot enable high priority interrupts through the API at all; // and ESP-IDF v4 on XTensa cannot enable Level 5 due to incorrect interrupt descriptor tables #if !defined(__XTENSA__) || (ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 0, 0)) || ((ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(4, 0, 0) && CONFIG_ESP_SYSTEM_CHECK_INT_LEVEL_5)) #define NEOESP32_RMT_CAN_USE_INTR_ALLOC // XTensa cores require the assembly bridge #ifdef __XTENSA__ #define HI_IRQ_HANDLER nullptr #define HI_IRQ_HANDLER_ARG ld_include_hli_vectors_rmt #else #define HI_IRQ_HANDLER NeoEsp32RmtMethodIsr #define HI_IRQ_HANDLER_ARG nullptr #endif #else /* !CONFIG_BTDM_CTRL_HLI && !NEOESP32_RMT_CAN_USE_INTR_ALLOC */ // This is the index of the LV5 interrupt vector - see interrupt descriptor table in idf components/hal/esp32/interrupt_descriptor_table.c #define ESP32_LV5_IRQ_INDEX 26 #endif /* NEOESP32_RMT_CAN_USE_INTR_ALLOC */ #endif /* CONFIG_BTDM_CTRL_HLI */ // RMT driver implementation struct NeoEsp32RmtHIChannelState { uint32_t rmtBit0, rmtBit1; uint32_t resetDuration; const byte* txDataStart; // data array const byte* txDataEnd; // one past end const byte* txDataCurrent; // current location size_t rmtOffset; }; // Global variables #if defined(NEOESP32_RMT_CAN_USE_INTR_ALLOC) static intr_handle_t isrHandle = nullptr; #endif static NeoEsp32RmtHIChannelState** driverState = nullptr; constexpr size_t rmtBatchSize = RMT_MEM_ITEM_NUM / 2; // Fill the RMT buffer memory // This is implemented using many arguments instead of passing the structure object to ensure we do only one lookup // All the arguments are passed in registers, so they don't need to be looked up again static void IRAM_ATTR RmtFillBuffer(uint8_t channel, const byte** src_ptr, const byte* end, uint32_t bit0, uint32_t bit1, size_t* offset_ptr, size_t reserve) { // We assume that (rmtToWrite % 8) == 0 size_t rmtToWrite = rmtBatchSize - reserve; rmt_item32_t* dest =(rmt_item32_t*) &RMTMEM.chan[channel].data32[*offset_ptr + reserve]; // write directly in to RMT memory const byte* psrc = *src_ptr; *offset_ptr ^= rmtBatchSize; if (psrc != end) { while (rmtToWrite > 0) { uint8_t data = *psrc; for (uint8_t bit = 0; bit < 8; bit++) { dest->val = (data & 0x80) ? bit1 : bit0; dest++; data <<= 1; } rmtToWrite -= 8; psrc++; if (psrc == end) { break; } } *src_ptr = psrc; } if (rmtToWrite > 0) { // Add end event rmt_item32_t bit0_val = {{.val = bit0 }}; *dest = rmt_item32_t {{{ .duration0 = 0, .level0 = bit0_val.level1, .duration1 = 0, .level1 = bit0_val.level1 }}}; } } static void IRAM_ATTR RmtStartWrite(uint8_t channel, NeoEsp32RmtHIChannelState& state) { // Reset context state state.rmtOffset = 0; // Fill the first part of the buffer with a reset event // FUTURE: we could do timing analysis with the last interrupt on this channel // Use 8 words to stay aligned with the buffer fill logic rmt_item32_t bit0_val = {{.val = state.rmtBit0 }}; rmt_item32_t fill = {{{ .duration0 = 100, .level0 = bit0_val.level1, .duration1 = 100, .level1 = bit0_val.level1 }}}; rmt_item32_t* dest = (rmt_item32_t*) &RMTMEM.chan[channel].data32[0]; for (auto i = 0; i < 7; ++i) dest[i] = fill; fill.duration1 = state.resetDuration > 1400 ? (state.resetDuration - 1400) : 100; dest[7] = fill; // Fill the remaining buffer with real data RmtFillBuffer(channel, &state.txDataCurrent, state.txDataEnd, state.rmtBit0, state.rmtBit1, &state.rmtOffset, 8); RmtFillBuffer(channel, &state.txDataCurrent, state.txDataEnd, state.rmtBit0, state.rmtBit1, &state.rmtOffset, 0); // Start operation rmt_ll_clear_tx_thres_interrupt(&RMT, channel); rmt_ll_tx_reset_pointer(&RMT, channel); rmt_ll_tx_start(&RMT, channel); } extern "C" void IRAM_ATTR NeoEsp32RmtMethodIsr(void *arg) { // Tx threshold interrupt uint32_t status = rmt_ll_get_tx_thres_interrupt_status(&RMT); while (status) { uint8_t channel = __builtin_ffs(status) - 1; if (driverState[channel]) { // Normal case NeoEsp32RmtHIChannelState& state = *driverState[channel]; RmtFillBuffer(channel, &state.txDataCurrent, state.txDataEnd, state.rmtBit0, state.rmtBit1, &state.rmtOffset, 0); } else { // Danger - another driver got invoked? rmt_ll_tx_stop(&RMT, channel); } rmt_ll_clear_tx_thres_interrupt(&RMT, channel); status = rmt_ll_get_tx_thres_interrupt_status(&RMT); } }; // Wrapper around the register analysis defines // For all currently supported chips, this is constant for all channels; but this is not true of *all* ESP32 static inline bool _RmtStatusIsTransmitting(rmt_channel_t channel, uint32_t status) { uint32_t v; switch(channel) { #ifdef RMT_STATE_CH0 case 0: v = (status >> RMT_STATE_CH0_S) & RMT_STATE_CH0_V; break; #endif #ifdef RMT_STATE_CH1 case 1: v = (status >> RMT_STATE_CH1_S) & RMT_STATE_CH1_V; break; #endif #ifdef RMT_STATE_CH2 case 2: v = (status >> RMT_STATE_CH2_S) & RMT_STATE_CH2_V; break; #endif #ifdef RMT_STATE_CH3 case 3: v = (status >> RMT_STATE_CH3_S) & RMT_STATE_CH3_V; break; #endif #ifdef RMT_STATE_CH4 case 4: v = (status >> RMT_STATE_CH4_S) & RMT_STATE_CH4_V; break; #endif #ifdef RMT_STATE_CH5 case 5: v = (status >> RMT_STATE_CH5_S) & RMT_STATE_CH5_V; break; #endif #ifdef RMT_STATE_CH6 case 6: v = (status >> RMT_STATE_CH6_S) & RMT_STATE_CH6_V; break; #endif #ifdef RMT_STATE_CH7 case 7: v = (status >> RMT_STATE_CH7_S) & RMT_STATE_CH7_V; break; #endif default: v = 0; } return v != 0; } esp_err_t NeoEsp32RmtHiMethodDriver::Install(rmt_channel_t channel, uint32_t rmtBit0, uint32_t rmtBit1, uint32_t reset) { // Validate channel number if (channel >= RMT_CHANNEL_MAX) { return ESP_ERR_INVALID_ARG; } esp_err_t err = ESP_OK; if (!driverState) { // First time init driverState = reinterpret_cast(heap_caps_calloc(RMT_CHANNEL_MAX, sizeof(NeoEsp32RmtHIChannelState*), MALLOC_CAP_INTERNAL)); if (!driverState) return ESP_ERR_NO_MEM; // Ensure all interrupts are cleared before binding RMT.int_ena.val = 0; RMT.int_clr.val = 0xFFFFFFFF; // Bind interrupt handler #if defined(CONFIG_BTDM_CTRL_HLI) // Bluetooth driver has taken the empty high-priority interrupt. Fortunately, it allows us to // hook up another handler. err = hli_intr_register(NeoEsp32RmtMethodIsr, nullptr, (uintptr_t) &RMT.int_st, 0xFF000000); // 25 is the magic number of the bluetooth ISR on ESP32 - see soc/soc.h. intr_matrix_set(cpu_hal_get_core_id(), ETS_RMT_INTR_SOURCE, 25); intr_cntrl_ll_enable_interrupts(1<<25); #elif defined(NEOESP32_RMT_CAN_USE_INTR_ALLOC) // Use the platform code to allocate the interrupt // If we need the additional assembly bridge, we pass it as the "arg" to the IDF so it gets linked in err = esp_intr_alloc(ETS_RMT_INTR_SOURCE, INT_LEVEL_FLAG | ESP_INTR_FLAG_IRAM, HI_IRQ_HANDLER, (void*) HI_IRQ_HANDLER_ARG, &isrHandle); //err = ESP_ERR_NOT_FINISHED; #else // Broken IDF API does not allow us to reserve the interrupt; do it manually static volatile const void* __attribute__((used)) pleaseLinkAssembly = (void*) ld_include_hli_vectors_rmt; intr_matrix_set(xPortGetCoreID(), ETS_RMT_INTR_SOURCE, ESP32_LV5_IRQ_INDEX); ESP_INTR_ENABLE(ESP32_LV5_IRQ_INDEX); #endif if (err != ESP_OK) { heap_caps_free(driverState); driverState = nullptr; return err; } } if (driverState[channel] != nullptr) { return ESP_ERR_INVALID_STATE; // already in use } NeoEsp32RmtHIChannelState* state = reinterpret_cast(heap_caps_calloc(1, sizeof(NeoEsp32RmtHIChannelState), MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT)); if (state == nullptr) { return ESP_ERR_NO_MEM; } // Store timing information state->rmtBit0 = rmtBit0; state->rmtBit1 = rmtBit1; state->resetDuration = reset; // Initialize hardware rmt_ll_tx_stop(&RMT, channel); rmt_ll_tx_reset_pointer(&RMT, channel); rmt_ll_enable_tx_err_interrupt(&RMT, channel, false); rmt_ll_enable_tx_end_interrupt(&RMT, channel, false); rmt_ll_enable_tx_thres_interrupt(&RMT, channel, false); rmt_ll_clear_tx_err_interrupt(&RMT, channel); rmt_ll_clear_tx_end_interrupt(&RMT, channel); rmt_ll_clear_tx_thres_interrupt(&RMT, channel); rmt_ll_tx_enable_loop(&RMT, channel, false); rmt_ll_tx_enable_pingpong(&RMT, channel, true); rmt_ll_tx_set_limit(&RMT, channel, rmtBatchSize); driverState[channel] = state; rmt_ll_enable_tx_thres_interrupt(&RMT, channel, true); return err; } esp_err_t NeoEsp32RmtHiMethodDriver::Uninstall(rmt_channel_t channel) { if ((channel >= RMT_CHANNEL_MAX) || !driverState || !driverState[channel]) return ESP_ERR_INVALID_ARG; NeoEsp32RmtHIChannelState* state = driverState[channel]; WaitForTxDone(channel, 10000 / portTICK_PERIOD_MS); // Done or not, we're out of here rmt_ll_tx_stop(&RMT, channel); rmt_ll_enable_tx_thres_interrupt(&RMT, channel, false); driverState[channel] = nullptr; heap_caps_free(state); #if !defined(CONFIG_BTDM_CTRL_HLI) /* Cannot unbind from bluetooth ISR */ // Turn off the driver ISR and release global state if none are left for (uint8_t channelIndex = 0; channelIndex < RMT_CHANNEL_MAX; ++channelIndex) { if (driverState[channelIndex]) return ESP_OK; // done } #if defined(NEOESP32_RMT_CAN_USE_INTR_ALLOC) esp_intr_free(isrHandle); #else ESP_INTR_DISABLE(ESP32_LV5_IRQ_INDEX); #endif heap_caps_free(driverState); driverState = nullptr; #endif /* !defined(CONFIG_BTDM_CTRL_HLI) */ return ESP_OK; } esp_err_t NeoEsp32RmtHiMethodDriver::Write(rmt_channel_t channel, const uint8_t *src, size_t src_size) { if ((channel >= RMT_CHANNEL_MAX) || !driverState || !driverState[channel]) return ESP_ERR_INVALID_ARG; NeoEsp32RmtHIChannelState& state = *driverState[channel]; esp_err_t result = WaitForTxDone(channel, 10000 / portTICK_PERIOD_MS); if (result == ESP_OK) { state.txDataStart = src; state.txDataCurrent = src; state.txDataEnd = src + src_size; RmtStartWrite(channel, state); } return result; } esp_err_t NeoEsp32RmtHiMethodDriver::WaitForTxDone(rmt_channel_t channel, TickType_t wait_time) { if ((channel >= RMT_CHANNEL_MAX) || !driverState || !driverState[channel]) return ESP_ERR_INVALID_ARG; NeoEsp32RmtHIChannelState& state = *driverState[channel]; // yield-wait until wait_time esp_err_t rv = ESP_OK; uint32_t status; while(1) { status = rmt_ll_tx_get_channel_status(&RMT, channel); if (!_RmtStatusIsTransmitting(channel, status)) break; if (wait_time == 0) { rv = ESP_ERR_TIMEOUT; break; }; TickType_t sleep = std::min(wait_time, (TickType_t) 5); vTaskDelay(sleep); wait_time -= sleep; }; return rv; } #endif ================================================ FILE: lib/README ================================================ 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 ================================================ FILE: package.json ================================================ { "name": "wled", "version": "16.0.0-alpha", "description": "Tools for WLED project", "main": "tools/cdata.js", "directories": { "lib": "lib", "test": "test" }, "scripts": { "build": "node tools/cdata.js", "test": "node --test", "dev": "nodemon -e js,html,htm,css,png,jpg,gif,ico,js -w tools/ -w wled00/data/ -x node tools/cdata.js" }, "repository": { "type": "git", "url": "git+https://github.com/wled/WLED.git" }, "author": "", "license": "ISC", "bugs": { "url": "https://github.com/wled/WLED/issues" }, "homepage": "https://github.com/wled/WLED#readme", "dependencies": { "clean-css": "^5.3.3", "html-minifier-terser": "^7.2.0", "web-resource-inliner": "^7.0.0", "nodemon": "^3.1.14" }, "engines": { "node": ">=20.0.0" } } ================================================ FILE: pio-scripts/build_ui.py ================================================ Import("env") import shutil node_ex = shutil.which("node") # Check if Node.js is installed and present in PATH if it failed, abort the build if node_ex is None: print('\x1b[0;31;43m' + 'Node.js is not installed or missing from PATH html css js will not be processed check https://kno.wled.ge/advanced/compiling-wled/' + '\x1b[0m') exitCode = env.Execute("null") exit(exitCode) else: # Install the necessary node packages for the pre-build asset bundling script print('\x1b[6;33;42m' + 'Installing node packages' + '\x1b[0m') env.Execute("npm ci") # Call the bundling script exitCode = env.Execute("npm run build") # If it failed, abort the build if (exitCode): print('\x1b[0;31;43m' + 'npm run build fails check https://kno.wled.ge/advanced/compiling-wled/' + '\x1b[0m') exit(exitCode) ================================================ FILE: pio-scripts/dynarray.py ================================================ # Add a section to the linker script to store our dynamic arrays # This is implemented as a pio post-script to ensure that we can # place our linker script at the correct point in the command arguments. Import("env") from pathlib import Path platform = env.get("PIOPLATFORM") script_file = Path(f"tools/dynarray_{platform}.ld") if script_file.is_file(): linker_script = f"-T{script_file}" if platform == "espressif32": # For ESP32, the script must be added at the right point in the list linkflags = env.get("LINKFLAGS", []) idx = linkflags.index("memory.ld") linkflags.insert(idx+1, linker_script) env.Replace(LINKFLAGS=linkflags) else: # For other platforms, put it in last env.Append(LINKFLAGS=[linker_script]) ================================================ FILE: pio-scripts/load_usermods.py ================================================ Import('env') from collections import deque from pathlib import Path # For OS-agnostic path manipulation import re from urllib.parse import urlparse from click import secho from SCons.Script import Exit from platformio.builder.tools.piolib import LibBuilderBase usermod_dir = Path(env["PROJECT_DIR"]).resolve() / "usermods" # Utility functions def find_usermod(mod: str) -> Path: """Locate this library in the usermods folder. We do this to avoid needing to rename a bunch of folders; this could be removed later """ # Check name match mp = usermod_dir / mod if mp.exists(): return mp mp = usermod_dir / f"{mod}_v2" if mp.exists(): return mp mp = usermod_dir / f"usermod_v2_{mod}" if mp.exists(): return mp raise RuntimeError(f"Couldn't locate module {mod} in usermods directory!") # Names of external/registry deps listed in custom_usermods. # Populated during parsing below; read by is_wled_module() at configure time. _custom_usermod_names: set[str] = set() # Matches any RFC-valid URL scheme (http, https, git, git+https, symlink, file, hg+ssh, etc.) _URL_SCHEME_RE = re.compile(r'^[a-zA-Z][a-zA-Z0-9+.-]*://') # SSH git URL: user@host:path (e.g. git@github.com:user/repo.git#tag) _SSH_URL_RE = re.compile(r'^[^@\s]+@[^@:\s]+:[^:\s]') # Explicit custom name: "LibName = " (PlatformIO [=] form) _NAME_EQ_RE = re.compile(r'^([A-Za-z0-9_.-]+)\s*=\s*(\S.*)') def _is_external_entry(line: str) -> bool: """Return True if line is a lib_deps-style external/registry entry.""" if _NAME_EQ_RE.match(line): # "LibName = " return True if _URL_SCHEME_RE.match(line): # https://, git://, symlink://, etc. return True if _SSH_URL_RE.match(line): # git@github.com:user/repo.git return True if '@' in line: # "owner/Name @ ^1.0.0" return True if re.match(r'^[^/\s]+/[^/\s]+$', line): # "owner/Name" return True return False def _predict_dep_name(entry: str) -> str | None: """Predict the library name PlatformIO will assign to this dep (best-effort). Accuracy relies on the library's manifest "name" matching the repo/package name in the spec. This holds for well-authored libraries; the libArchive check (which requires library.json) provides an early-failure safety net. """ entry = entry.strip() # "LibName = " — name is given explicitly; always use it m = _NAME_EQ_RE.match(entry) if m: return m.group(1).strip() # URL scheme: extract name from path if _URL_SCHEME_RE.match(entry): parsed = urlparse(entry) if parsed.netloc in ('github.com', 'gitlab.com', 'bitbucket.com'): parts = [p for p in parsed.path.split('/') if p] if len(parts) >= 2: name = parts[1] else: name = Path(parsed.path.rstrip('/')).name.strip() if name.endswith('.git'): name = name[:-4] return name or None # SSH git URL: git@github.com:user/repo.git#tag → repo if _SSH_URL_RE.match(entry): path_part = entry.split(':', 1)[1].split('#')[0].rstrip('/') name = Path(path_part).name return (name[:-4] if name.endswith('.git') else name) or None # Versioned registry: "owner/Name @ version" → Name if '@' in entry: name_part = entry.split('@')[0].strip() return name_part.split('/')[-1].strip() if '/' in name_part else name_part # Plain registry: "owner/Name" → Name if re.match(r'^[^/\s]+/[^/\s]+$', entry): return entry.split('/')[-1].strip() return None def is_wled_module(dep: LibBuilderBase) -> bool: """Returns true if the specified library is a wled module.""" return ( usermod_dir in Path(dep.src_dir).parents or str(dep.name).startswith("wled-") or dep.name in _custom_usermod_names ) ## Script starts here — parse custom_usermods raw_usermods = env.GetProjectOption("custom_usermods", "") usermods_libdeps: list[str] = [] for line in raw_usermods.splitlines(): line = line.strip() if not line or line.startswith('#') or line.startswith(';'): continue if _is_external_entry(line): # External URL or registry entry: pass through to lib_deps unchanged. predicted = _predict_dep_name(line) if predicted: _custom_usermod_names.add(predicted) else: secho( f"WARNING: Cannot determine library name for custom_usermods entry " f"{line!r}. If it is not recognised as a WLED module at build time, " f"ensure its library.json 'name' matches the repo name.", fg="yellow", err=True) usermods_libdeps.append(line) else: # Bare name(s): split on whitespace for backwards compatibility. for token in line.split(): if token == '*': for mod_path in sorted(usermod_dir.iterdir()): if mod_path.is_dir() and (mod_path / 'library.json').exists(): _custom_usermod_names.add(mod_path.name) usermods_libdeps.append(f"symlink://{mod_path.resolve()}") else: resolved = find_usermod(token) _custom_usermod_names.add(resolved.name) usermods_libdeps.append(f"symlink://{resolved.resolve()}") if usermods_libdeps: env.GetProjectConfig().set("env:" + env['PIOENV'], 'lib_deps', env.GetProjectOption('lib_deps') + usermods_libdeps) # Utility function for assembling usermod include paths def cached_add_includes(dep, dep_cache: set, includes: deque): """ Add dep's include paths to includes if it's not in the cache """ if dep not in dep_cache: dep_cache.add(dep) for include in dep.get_include_dirs(): if include not in includes: includes.appendleft(include) if usermod_dir not in Path(dep.src_dir).parents: # Recurse, but only for NON-usermods for subdep in dep.depbuilders: cached_add_includes(subdep, dep_cache, includes) # Monkey-patch ConfigureProjectLibBuilder to mark up the dependencies # Save the old value old_ConfigureProjectLibBuilder = env.ConfigureProjectLibBuilder # Our new wrapper def wrapped_ConfigureProjectLibBuilder(xenv): # Call the wrapped function result = old_ConfigureProjectLibBuilder.clone(xenv)() # Fix up include paths # In PlatformIO >=6.1.17, this could be done prior to ConfigureProjectLibBuilder wled_dir = xenv["PROJECT_SRC_DIR"] # Build a list of dependency include dirs # TODO: Find out if this is the order that PlatformIO/SCons puts them in?? processed_deps = set() extra_include_dirs = deque() # Deque used for fast prepend for dep in result.depbuilders: cached_add_includes(dep, processed_deps, extra_include_dirs) wled_deps = [dep for dep in result.depbuilders if is_wled_module(dep)] broken_usermods = [] for dep in wled_deps: # Add the wled folder to the include path dep.env.PrependUnique(CPPPATH=str(wled_dir)) # Add WLED's own dependencies for dir in extra_include_dirs: dep.env.PrependUnique(CPPPATH=str(dir)) # Ensure debug info is emitted for this module's source files. # validate_modules.py uses `nm --defined-only -l` on the final ELF to check # that each module has at least one symbol placed in the binary. The -l flag # reads DWARF debug sections to map placed symbols back to their original source # files; without -g those sections are absent and the check cannot attribute any # symbol to a specific module. We scope this to usermods only — the main WLED # build and other libraries are unaffected. dep.env.AppendUnique(CCFLAGS=["-g"]) # Enforce that libArchive is not set; we must link them directly to the executable if dep.lib_archive: broken_usermods.append(dep) if broken_usermods: broken_usermods = [usermod.name for usermod in broken_usermods] secho( f"ERROR: libArchive=false is missing on usermod(s) {' '.join(broken_usermods)} -- " f"modules will not compile in correctly. Add '\"build\": {{\"libArchive\": false}}' " f"to their library.json.", fg="red", err=True) Exit(1) # Save the depbuilders list for later validation xenv.Replace(WLED_MODULES=wled_deps) return result # Apply the wrapper env.AddMethod(wrapped_ConfigureProjectLibBuilder, "ConfigureProjectLibBuilder") ================================================ FILE: pio-scripts/obj-dump.py ================================================ # Little convenience script to get an object dump # You may add "-S" to the objdump commandline (i.e. replace "-D -C " with "-d -S -C ") # to get source code intermixed with disassembly (SLOW !) Import('env') def obj_dump_after_elf(source, target, env): platform = env.PioPlatform() board = env.BoardConfig() mcu = board.get("build.mcu", "esp32") print("Create firmware.asm") if mcu == "esp8266": env.Execute("xtensa-lx106-elf-objdump "+ "-D -C " + str(target[0]) + " > "+ "$BUILD_DIR/${PROGNAME}.asm") if mcu == "esp32": env.Execute("xtensa-esp32-elf-objdump "+ "-D -C " + str(target[0]) + " > "+ "$BUILD_DIR/${PROGNAME}.asm") if mcu == "esp32s2": env.Execute("xtensa-esp32s2-elf-objdump "+ "-D -C " + str(target[0]) + " > "+ "$BUILD_DIR/${PROGNAME}.asm") if mcu == "esp32s3": env.Execute("xtensa-esp32s3-elf-objdump "+ "-D -C " + str(target[0]) + " > "+ "$BUILD_DIR/${PROGNAME}.asm") if mcu == "esp32c3": env.Execute("riscv32-esp-elf-objdump "+ "-D -C " + str(target[0]) + " > "+ "$BUILD_DIR/${PROGNAME}.asm") env.AddPostAction("$BUILD_DIR/${PROGNAME}.elf", [obj_dump_after_elf]) ================================================ FILE: pio-scripts/output_bins.py ================================================ Import('env') import os import shutil import gzip import json OUTPUT_DIR = "build_output{}".format(os.path.sep) #OUTPUT_DIR = os.path.join("build_output") def _get_cpp_define_value(env, define): define_list = [item[-1] for item in env["CPPDEFINES"] if item[0] == define] if define_list: return define_list[0] return None def _create_dirs(dirs=["map", "release", "firmware"]): for d in dirs: os.makedirs(os.path.join(OUTPUT_DIR, d), exist_ok=True) def create_release(source): release_name_def = _get_cpp_define_value(env, "WLED_RELEASE_NAME") if release_name_def: release_name = release_name_def.replace("\\\"", "") with open("package.json", "r") as package: version = json.load(package)["version"] release_file = os.path.join(OUTPUT_DIR, "release", f"WLED_{version}_{release_name}.bin") release_gz_file = release_file + ".gz" print(f"Copying {source} to {release_file}") shutil.copy(source, release_file) bin_gzip(release_file, release_gz_file) else: variant = env["PIOENV"] bin_file = "{}firmware{}{}.bin".format(OUTPUT_DIR, os.path.sep, variant) print(f"Copying {source} to {bin_file}") shutil.copy(source, bin_file) def bin_rename_copy(source, target, env): _create_dirs() variant = env["PIOENV"] builddir = os.path.join(env["PROJECT_BUILD_DIR"], variant) source_map = os.path.join(builddir, env["PROGNAME"] + ".map") # create string with location and file names based on variant map_file = "{}map{}{}.map".format(OUTPUT_DIR, os.path.sep, variant) create_release(str(target[0])) # copy firmware.map to map/.map if os.path.isfile("firmware.map"): print("Found linker mapfile firmware.map") shutil.copy("firmware.map", map_file) if os.path.isfile(source_map): print(f"Found linker mapfile {source_map}") shutil.copy(source_map, map_file) def bin_gzip(source, target): # only create gzip for esp8266 if not env["PIOPLATFORM"] == "espressif8266": return print(f"Creating gzip file {target} from {source}") with open(source,"rb") as fp: with gzip.open(target, "wb", compresslevel = 9) as f: shutil.copyfileobj(fp, f) env.AddPostAction("$BUILD_DIR/${PROGNAME}.bin", bin_rename_copy) ================================================ FILE: pio-scripts/set_metadata.py ================================================ Import('env') import subprocess import json import re def get_github_repo(): """Extract GitHub repository name from git remote URL. Uses the remote that the current branch tracks, falling back to 'origin'. This handles cases where repositories have multiple remotes or where the main remote is not named 'origin'. Returns: str: Repository name in 'owner/repo' format for GitHub repos, 'unknown' for non-GitHub repos, missing git CLI, or any errors. """ try: remote_name = 'origin' # Default fallback # Try to get the remote for the current branch try: # Get current branch name branch_result = subprocess.run(['git', 'rev-parse', '--abbrev-ref', 'HEAD'], capture_output=True, text=True, check=True) current_branch = branch_result.stdout.strip() # Get the remote for the current branch remote_result = subprocess.run(['git', 'config', f'branch.{current_branch}.remote'], capture_output=True, text=True, check=True) tracked_remote = remote_result.stdout.strip() # Use the tracked remote if we found one if tracked_remote: remote_name = tracked_remote except subprocess.CalledProcessError: # If branch config lookup fails, continue with 'origin' as fallback pass # Get the remote URL for the determined remote result = subprocess.run(['git', 'remote', 'get-url', remote_name], capture_output=True, text=True, check=True) remote_url = result.stdout.strip() # Check if it's a GitHub URL if 'github.com' not in remote_url.lower(): return None # Parse GitHub URL patterns: # https://github.com/owner/repo.git # git@github.com:owner/repo.git # https://github.com/owner/repo # Remove .git suffix if present if remote_url.endswith('.git'): remote_url = remote_url[:-4] # Handle HTTPS URLs https_match = re.search(r'github\.com/([^/]+/[^/]+)', remote_url, re.IGNORECASE) if https_match: return https_match.group(1) # Handle SSH URLs ssh_match = re.search(r'github\.com:([^/]+/[^/]+)', remote_url, re.IGNORECASE) if ssh_match: return ssh_match.group(1) return None except FileNotFoundError: # Git CLI is not installed or not in PATH return None except subprocess.CalledProcessError: # Git command failed (e.g., not a git repo, no remote, etc.) return None except Exception: # Any other unexpected error return None # WLED version is managed by package.json; this is picked up in several places # - It's integrated in to the UI code # - Here, for wled_metadata.cpp # - The output_bins script # We always take it from package.json to ensure consistency with open("package.json", "r") as package: WLED_VERSION = json.load(package)["version"] def has_def(cppdefs, name): """ Returns true if a given name is set in a CPPDEFINES collection """ for f in cppdefs: if isinstance(f, tuple): f = f[0] if f == name: return True return False def add_wled_metadata_flags(env, node): cdefs = env["CPPDEFINES"].copy() if not has_def(cdefs, "WLED_REPO"): repo = get_github_repo() if repo: cdefs.append(("WLED_REPO", f"\\\"{repo}\\\"")) cdefs.append(("WLED_VERSION", WLED_VERSION)) # This transforms the node in to a Builder; it cannot be modified again return env.Object( node, CPPDEFINES=cdefs ) env.AddBuildMiddleware( add_wled_metadata_flags, "*/wled_metadata.cpp" ) ================================================ FILE: pio-scripts/strip-floats.py ================================================ Import('env') # # Dump build environment (for debug) #print env.Dump() # flags = " ".join(env['LINKFLAGS']) flags = flags.replace("-u _printf_float", "") flags = flags.replace("-u _scanf_float", "") newflags = flags.split() env.Replace( LINKFLAGS=newflags ) ================================================ FILE: pio-scripts/user_config_copy.py ================================================ Import('env') import os import shutil # copy WLED00/my_config_sample.h to WLED00/my_config.h if os.path.isfile("wled00/my_config.h"): print ("*** use existing my_config.h ***") else: shutil.copy("wled00/my_config_sample.h", "wled00/my_config.h") ================================================ FILE: pio-scripts/validate_modules.py ================================================ import os import re import subprocess from pathlib import Path # For OS-agnostic path manipulation from click import secho from SCons.Script import Action, Exit Import("env") def read_lines(p: Path): """ Read in the contents of a file for analysis """ with p.open("r", encoding="utf-8", errors="ignore") as f: return f.readlines() def _get_nm_path(env) -> str: """ Derive the nm tool path from the build environment """ if "NM" in env: return env.subst("$NM") # Derive from the C compiler: xtensa-esp32-elf-gcc → xtensa-esp32-elf-nm cc = env.subst("$CC") nm = re.sub(r'(gcc|g\+\+)$', 'nm', os.path.basename(cc)) return os.path.join(os.path.dirname(cc), nm) def check_elf_modules(elf_path: Path, env, module_lib_builders) -> set[str]: """ Check which modules have at least one defined symbol placed in the ELF. The map file is not a reliable source for this: with LTO, original object file paths are replaced by temporary ltrans.o partitions in all output sections, making per-module attribution impossible from the map alone. Instead we invoke nm --defined-only -l on the ELF, which uses DWARF debug info to attribute each placed symbol to its original source file. Requires usermod libraries to be compiled with -g so that DWARF sections are present in the ELF. load_usermods.py injects -g for all WLED modules via dep.env.AppendUnique(CCFLAGS=["-g"]). Returns the set of build_dir basenames for confirmed modules. """ nm_path = _get_nm_path(env) try: result = subprocess.run( [nm_path, "--defined-only", "-l", str(elf_path)], capture_output=True, text=True, errors="ignore", timeout=120, ) nm_output = result.stdout except (subprocess.TimeoutExpired, FileNotFoundError, OSError) as e: secho(f"WARNING: nm failed ({e}); skipping per-module validation", fg="yellow", err=True) return {Path(b.build_dir).name for b in module_lib_builders} # conservative pass # Match placed symbols against builders as we parse nm output, exiting early # once all builders are accounted for. # nm --defined-only still includes debugging symbols (type 'N') such as the # per-CU markers GCC emits in .debug_info (e.g. "usermod_example_cpp_6734d48d"). # These live at address 0x00000000 in their debug section — not in any load # segment — so filtering them out leaves only genuinely placed symbols. # nm -l appends a tab-separated "file:lineno" location to each symbol line. remaining = {Path(str(b.src_dir)): Path(b.build_dir).name for b in module_lib_builders} found = set() for line in nm_output.splitlines(): if not remaining: break # all builders matched addr, _, _ = line.partition(' ') if not addr.lstrip('0'): continue # zero address — skip debug-section marker if '\t' not in line: continue loc = line.rsplit('\t', 1)[1] # Strip trailing :lineno (e.g. "/path/to/foo.cpp:42" → "/path/to/foo.cpp") src_path = Path(loc.rsplit(':', 1)[0]) # Path.is_relative_to() handles OS-specific separators correctly without # any regex, avoiding Windows path escaping issues. for src_dir in list(remaining): if src_path.is_relative_to(src_dir): found.add(remaining.pop(src_dir)) break return found DYNARRAY_SECTION = ".dtors" if env.get("PIOPLATFORM") == "espressif8266" else ".dynarray" USERMODS_SECTION = f"{DYNARRAY_SECTION}.usermods.1" def count_usermod_objects(map_file: list[str]) -> int: """ Returns the number of usermod objects in the usermod list """ # Count the number of entries in the usermods table section return len([x for x in map_file if USERMODS_SECTION in x]) def validate_map_file(source, target, env): """ Validate that all modules appear in the output build """ build_dir = Path(env.subst("$BUILD_DIR")) map_file_path = build_dir / env.subst("${PROGNAME}.map") if not map_file_path.exists(): secho(f"ERROR: Map file not found: {map_file_path}", fg="red", err=True) Exit(1) # Identify the WLED module builders, set by load_usermods.py module_lib_builders = env['WLED_MODULES'] # Extract the values we care about modules = {Path(builder.build_dir).name: builder.name for builder in module_lib_builders} secho(f"INFO: {len(modules)} libraries linked as WLED optional/user modules") # Now parse the map file map_file_contents = read_lines(map_file_path) usermod_object_count = count_usermod_objects(map_file_contents) secho(f"INFO: {usermod_object_count} usermod object entries") elf_path = build_dir / env.subst("${PROGNAME}.elf") confirmed_modules = check_elf_modules(elf_path, env, module_lib_builders) missing_modules = [modname for mdir, modname in modules.items() if mdir not in confirmed_modules] if missing_modules: secho( f"ERROR: No symbols from {missing_modules} found in linked output!", fg="red", err=True) Exit(1) return None env.Append(LINKFLAGS=[env.subst("-Wl,--Map=${BUILD_DIR}/${PROGNAME}.map")]) env.AddPostAction("$BUILD_DIR/${PROGNAME}.elf", Action(validate_map_file, cmdstr='Checking linked optional modules (usermods) in map file')) ================================================ FILE: platformio.ini ================================================ ; PlatformIO Project Configuration File ; Please visit documentation: https://docs.platformio.org/page/projectconf.html [platformio] # ------------------------------------------------------------------------------ # ENVIRONMENTS # # Please uncomment one of the lines below to select your board(s) # (use `platformio_override.ini` when building for your own board; see `platformio_override.ini.sample` for an example) # ------------------------------------------------------------------------------ # CI/release binaries default_envs = nodemcuv2 esp8266_2m esp01_1m_full nodemcuv2_160 esp8266_2m_160 esp01_1m_full_160 nodemcuv2_compat esp8266_2m_compat esp01_1m_full_compat esp32dev esp32dev_debug esp32_eth esp32_wrover lolin_s2_mini esp32c3dev esp32c3dev_qio esp32S3_wroom2 esp32s3dev_16MB_opi esp32s3dev_8MB_opi esp32s3dev_8MB_qspi esp32s3_4M_qspi usermods src_dir = ./wled00 data_dir = ./wled00/data build_cache_dir = ~/.buildcache extra_configs = platformio_override.ini [common] # ------------------------------------------------------------------------------ # PLATFORM: # !! DO NOT confuse platformio's ESP8266 development platform with Arduino core for ESP8266 # # arduino core 2.6.3 = platformIO 2.3.2 # arduino core 2.7.0 = platformIO 2.5.0 # ------------------------------------------------------------------------------ arduino_core_2_6_3 = espressif8266@2.3.3 arduino_core_2_7_4 = espressif8266@2.6.2 arduino_core_3_0_0 = espressif8266@3.0.0 arduino_core_3_0_2 = espressif8266@3.2.0 arduino_core_3_1_0 = espressif8266@4.1.0 arduino_core_3_1_2 = espressif8266@4.2.1 # Development platforms arduino_core_develop = https://github.com/platformio/platform-espressif8266#develop arduino_core_git = https://github.com/platformio/platform-espressif8266#feature/stage # Platform to use for ESP8266 platform_wled_default = ${common.arduino_core_3_1_2} # We use 2.7.4.7 for all, includes PWM flicker fix and Wstring optimization #platform_packages = tasmota/framework-arduinoespressif8266 @ 3.20704.7 platform_packages = platformio/toolchain-xtensa @ ~2.100300.220621 #2.40802.200502 platformio/tool-esptool #@ ~1.413.0 platformio/tool-esptoolpy #@ ~1.30000.0 ## previous platform for 8266, in case of problems with the new one ## you'll need makuna/NeoPixelBus@ 2.6.9 for arduino_core_3_0_2, which does not support Ucs890x ;; platform_wled_default = ${common.arduino_core_3_0_2} ;; platform_packages = tasmota/framework-arduinoespressif8266 @ 3.20704.7 ;; platformio/toolchain-xtensa @ ~2.40802.200502 ;; platformio/tool-esptool @ ~1.413.0 ;; platformio/tool-esptoolpy @ ~1.30000.0 # ------------------------------------------------------------------------------ # FLAGS: DEBUG # esp8266 : see https://docs.platformio.org/en/latest/platforms/espressif8266.html#debug-level # esp32 : see https://docs.platformio.org/en/latest/platforms/espressif32.html#debug-level # ------------------------------------------------------------------------------ debug_flags = -D DEBUG=1 -D WLED_DEBUG -DDEBUG_ESP_WIFI -DDEBUG_ESP_HTTP_CLIENT -DDEBUG_ESP_HTTP_UPDATE -DDEBUG_ESP_HTTP_SERVER -DDEBUG_ESP_UPDATER -DDEBUG_ESP_OTA -DDEBUG_TLS_MEM ;; for esp8266 # if needed (for memleaks etc) also add; -DDEBUG_ESP_OOM -include "umm_malloc/umm_malloc_cfg.h" # -DDEBUG_ESP_CORE is not working right now # ------------------------------------------------------------------------------ # FLAGS: ldscript (available ldscripts at https://github.com/esp8266/Arduino/tree/master/tools/sdk/ld) # ldscript_2m1m (2048 KB) = 1019 KB sketch, 4 KB eeprom, 1004 KB spiffs, 16 KB reserved # ldscript_4m1m (4096 KB) = 1019 KB sketch, 4 KB eeprom, 1002 KB spiffs, 16 KB reserved, 2048 KB empty/ota? # # Available lwIP variants (macros): # -DPIO_FRAMEWORK_ARDUINO_LWIP_HIGHER_BANDWIDTH = v1.4 Higher Bandwidth (default) # -DPIO_FRAMEWORK_ARDUINO_LWIP2_LOW_MEMORY = v2 Lower Memory # -DPIO_FRAMEWORK_ARDUINO_LWIP2_HIGHER_BANDWIDTH = v2 Higher Bandwidth # -DPIO_FRAMEWORK_ARDUINO_LWIP2_HIGHER_BANDWIDTH_LOW_FLASH # # BearSSL performance: # When building with -DSECURE_CLIENT=SECURE_CLIENT_BEARSSL, please add `board_build.f_cpu = 160000000` to the environment configuration # # BearSSL ciphers: # When building on core >= 2.5, you can add the build flag -DBEARSSL_SSL_BASIC in order to build BearSSL with a limited set of ciphers: # TLS_RSA_WITH_AES_128_CBC_SHA256 / AES128-SHA256 # TLS_RSA_WITH_AES_256_CBC_SHA256 / AES256-SHA256 # TLS_RSA_WITH_AES_128_CBC_SHA / AES128-SHA # TLS_RSA_WITH_AES_256_CBC_SHA / AES256-SHA # This reduces the OTA size with ~45KB, so it's especially useful on low memory boards (512k/1m). # ------------------------------------------------------------------------------ build_flags = -DMQTT_MAX_PACKET_SIZE=1024 -DSECURE_CLIENT=SECURE_CLIENT_BEARSSL -DBEARSSL_SSL_BASIC -D CORE_DEBUG_LEVEL=0 -D NDEBUG -Wno-attributes ;; silence warnings about unknown attribute 'maybe_unused' in NeoPixelBus #build_flags for the IRremoteESP8266 library (enabled decoders have to appear here) -D _IR_ENABLE_DEFAULT_=false -D DECODE_HASH=true -D DECODE_NEC=true -D DECODE_SONY=true -D DECODE_SAMSUNG=true -D DECODE_LG=true -DWLED_USE_MY_CONFIG -D WLED_PS_DONT_REPLACE_FX ; PS replacement FX are purely a flash memory saving feature, do not replace classic FX until we run out of flash build_unflags = ldscript_1m128k = eagle.flash.1m128.ld ldscript_2m512k = eagle.flash.2m512.ld ldscript_2m1m = eagle.flash.2m1m.ld ldscript_4m1m = eagle.flash.4m1m.ld [scripts_defaults] extra_scripts = pre:pio-scripts/set_metadata.py post:pio-scripts/output_bins.py post:pio-scripts/strip-floats.py post:pio-scripts/dynarray.py pre:pio-scripts/user_config_copy.py pre:pio-scripts/load_usermods.py pre:pio-scripts/build_ui.py post:pio-scripts/validate_modules.py ;; double-check the build output usermods ; post:pio-scripts/obj-dump.py ;; convenience script to create a disassembly dump of the firmware (hardcore debugging) # ------------------------------------------------------------------------------ # COMMON SETTINGS: # ------------------------------------------------------------------------------ [env] framework = arduino board_build.flash_mode = dout monitor_speed = 115200 # slow upload speed but most compatible (use platformio_override.ini to use faster speed) upload_speed = 115200 # ------------------------------------------------------------------------------ # LIBRARIES: required dependencies # Please note that we don't always use the latest version of a library. # # The following libraries have been included (and some of them changed) in the source: # ArduinoJson@5.13.5, E131@1.0.0(changed), Time@1.5, Timezone@1.2.1 # ------------------------------------------------------------------------------ lib_compat_mode = strict lib_deps = fastled/FastLED @ 3.6.0 IRremoteESP8266 @ 2.8.2 https://github.com/Makuna/NeoPixelBus.git#a0919d1c10696614625978dd6fb750a1317a14ce https://github.com/Aircoookie/ESPAsyncWebServer.git#v2.4.2 marvinroger/AsyncMqttClient @ 0.9.0 # for I2C interface ;Wire # ESP-NOW library ;gmag11/QuickESPNow @ ~0.7.0 https://github.com/blazoncek/QuickESPNow.git#optional-debug #For use of the TTGO T-Display ESP32 Module with integrated TFT display uncomment the following line #TFT_eSPI #For compatible OLED display uncomment following #olikraus/U8g2 #@ ~2.33.15 #For Dallas sensor uncomment following #paulstoffregen/OneWire @ ~2.3.8 #For BME280 sensor uncomment following #BME280 @ ~3.0.0 ;adafruit/Adafruit BMP280 Library @ 2.1.0 ;adafruit/Adafruit CCS811 Library @ 1.0.4 ;adafruit/Adafruit Si7021 Library @ 1.4.0 #For MAX1704x Lipo Monitor / Fuel Gauge uncomment following ; https://github.com/adafruit/Adafruit_BusIO @ 1.14.5 ; https://github.com/adafruit/Adafruit_MAX1704X @ 1.0.2 #For MPU6050 IMU uncomment follwoing ;electroniccats/MPU6050 @1.0.1 # SHT85 ;robtillaart/SHT85@~0.3.3 extra_scripts = ${scripts_defaults.extra_scripts} [esp8266] build_unflags = ${common.build_unflags} build_flags = -DESP8266 -DFP_IN_IROM ;-Wno-deprecated-declarations ;-Wno-register ;; leaves some warnings when compiling C files: command-line option '-Wno-register' is valid for C++/ObjC++ but not for C ;-Dregister= # remove warnings in C++17 due to use of deprecated register keyword by the FastLED library ;; warning: this can be dangerous -Wno-misleading-indentation ; NONOSDK22x_190703 = 2.2.2-dev(38a443e) -DPIO_FRAMEWORK_ARDUINO_ESPRESSIF_SDK22x_190703 ; lwIP 2 - Higher Bandwidth no Features ; -DPIO_FRAMEWORK_ARDUINO_LWIP2_HIGHER_BANDWIDTH_LOW_FLASH ; lwIP 1.4 - Higher Bandwidth (Aircoookie has) -DPIO_FRAMEWORK_ARDUINO_LWIP_HIGHER_BANDWIDTH ; VTABLES in Flash -DVTABLES_IN_FLASH ; restrict to minimal mime-types -DMIMETYPE_MINIMAL ; other special-purpose framework flags (see https://docs.platformio.org/en/latest/platforms/espressif8266.html) ; decrease code cache size and increase IRAM to fit all pixel functions -D PIO_FRAMEWORK_ARDUINO_MMU_CACHE16_IRAM48 ;; in case of linker errors like "section `.text1' will not fit in region `iram1_0_seg'" ; -D PIO_FRAMEWORK_ARDUINO_MMU_CACHE16_IRAM48_SECHEAP_SHARED ;; (experimental) adds some extra heap, but may cause slowdown -D NON32XFER_HANDLER ;; ask forgiveness for PROGMEM misuse lib_deps = #https://github.com/lorol/LITTLEFS.git ESPAsyncTCP @ 1.2.2 ESPAsyncUDP ESP8266PWM ${env.lib_deps} ;; compatibilty flags - same as 0.14.0 which seems to work better on some 8266 boards. Not using PIO_FRAMEWORK_ARDUINO_MMU_CACHE16_IRAM48 build_flags_compat = -DESP8266 -DFP_IN_IROM ;;-Wno-deprecated-declarations -Wno-misleading-indentation ;;-Wno-attributes ;; silence warnings about unknown attribute 'maybe_unused' in NeoPixelBus -DPIO_FRAMEWORK_ARDUINO_ESPRESSIF_SDK22x_190703 -DPIO_FRAMEWORK_ARDUINO_LWIP_HIGHER_BANDWIDTH -DVTABLES_IN_FLASH -DMIMETYPE_MINIMAL -DWLED_SAVE_IRAM ;; needed to prevent linker error ;; this platform version was used for WLED 0.14.0 platform_compat = espressif8266@4.2.0 platform_packages_compat = platformio/toolchain-xtensa @ ~2.100300.220621 #2.40802.200502 platformio/tool-esptool #@ ~1.413.0 platformio/tool-esptoolpy #@ ~1.30000.0 ;; experimental - for using older NeoPixelBus 2.7.9 lib_deps_compat = ESPAsyncTCP @ 1.2.2 ESPAsyncUDP ESP8266PWM fastled/FastLED @ 3.6.0 IRremoteESP8266 @ 2.8.2 makuna/NeoPixelBus @ 2.7.9 https://github.com/blazoncek/QuickESPNow.git#optional-debug https://github.com/Aircoookie/ESPAsyncWebServer.git#v2.4.0 [esp32_all_variants] lib_deps = esp32async/AsyncTCP @ 3.4.7 bitbank2/AnimatedGIF@^1.4.7 https://github.com/Aircoookie/GifDecoder.git#bc3af189b6b1e06946569f6b4287f0b79a860f8e build_flags = -D CONFIG_ASYNC_TCP_USE_WDT=0 -D CONFIG_ASYNC_TCP_STACK_SIZE=8192 -D WLED_ENABLE_GIF [esp32] platform = ${esp32_idf_V4.platform} platform_packages = build_unflags = ${common.build_unflags} build_flags = ${esp32_idf_V4.build_flags} lib_deps = ${esp32_idf_V4.lib_deps} tiny_partitions = tools/WLED_ESP32_2MB_noOTA.csv default_partitions = tools/WLED_ESP32_4MB_1MB_FS.csv extended_partitions = tools/WLED_ESP32_4MB_700k_FS.csv big_partitions = tools/WLED_ESP32_4MB_256KB_FS.csv ;; 1.8MB firmware, 256KB filesystem, coredump support large_partitions = tools/WLED_ESP32_8MB.csv extreme_partitions = tools/WLED_ESP32_16MB_9MB_FS.csv board_build.partitions = ${esp32.default_partitions} ;; default partioning for 4MB Flash - can be overridden in build envs # additional build flags for audioreactive - must be applied globally AR_build_flags = ;; -fsingle-precision-constant ;; forces ArduinoFFT to use float math (2x faster) AR_lib_deps = ;; for pre-usermod-library platformio_override compatibility [esp32_idf_V4] ;; build environment for ESP32 using ESP-IDF 4.4.x / arduino-esp32 v2.0.5 ;; *** important: build flags from esp32_idf_V4 are inherited by _all_ esp32-based MCUs: esp32, esp32s2, esp32s3, esp32c3 ;; ;; please note that you can NOT update existing ESP32 installs with a "V4" build. Also updating by OTA will not work properly. ;; You need to completely erase your device (esptool erase_flash) first, then install the "V4" build from VSCode+platformio. platform = https://github.com/tasmota/platform-espressif32/releases/download/2024.06.00/platform-espressif32.zip ;; Tasmota Arduino Core 2.0.18 with IPv6 support, based on IDF 4.4.8 platform_packages = build_unflags = ${common.build_unflags} build_flags = -g -Wshadow=compatible-local ;; emit warning in case a local variable "shadows" another local one -DARDUINO_ARCH_ESP32 -DESP32 ${esp32_all_variants.build_flags} -D WLED_ENABLE_DMX_INPUT lib_deps = ${esp32_all_variants.lib_deps} https://github.com/someweisguy/esp_dmx.git#47db25d8c515e76fabcf5fc5ab0b786f98eeade0 ${env.lib_deps} [esp32s2] ;; generic definitions for all ESP32-S2 boards platform = ${esp32_idf_V4.platform} platform_packages = ${esp32_idf_V4.platform_packages} build_unflags = ${common.build_unflags} build_flags = -g -DARDUINO_ARCH_ESP32 -DARDUINO_ARCH_ESP32S2 -DCONFIG_IDF_TARGET_ESP32S2=1 -DARDUINO_USB_MSC_ON_BOOT=0 -DARDUINO_USB_DFU_ON_BOOT=0 -DCO -DARDUINO_USB_MODE=0 ;; this flag is mandatory for ESP32-S2 ! ;; please make sure that the following flags are properly set (to 0 or 1) by your board.json, or included in your custom platformio_override.ini entry: ;; ARDUINO_USB_CDC_ON_BOOT ${esp32_idf_V4.build_flags} lib_deps = ${esp32_idf_V4.lib_deps} board_build.partitions = ${esp32.default_partitions} ;; default partioning for 4MB Flash - can be overridden in build envs [esp32c3] ;; generic definitions for all ESP32-C3 boards platform = ${esp32_idf_V4.platform} platform_packages = ${esp32_idf_V4.platform_packages} build_unflags = ${common.build_unflags} build_flags = -g -DARDUINO_ARCH_ESP32 -DARDUINO_ARCH_ESP32C3 -DCONFIG_IDF_TARGET_ESP32C3=1 -DCO -DARDUINO_USB_MODE=1 ;; this flag is mandatory for ESP32-C3 ;; please make sure that the following flags are properly set (to 0 or 1) by your board.json, or included in your custom platformio_override.ini entry: ;; ARDUINO_USB_CDC_ON_BOOT ${esp32_idf_V4.build_flags} lib_deps = ${esp32_idf_V4.lib_deps} board_build.partitions = ${esp32.default_partitions} ;; default partioning for 4MB Flash - can be overridden in build envs board_build.flash_mode = qio [esp32s3] ;; generic definitions for all ESP32-S3 boards platform = ${esp32_idf_V4.platform} platform_packages = ${esp32_idf_V4.platform_packages} build_unflags = ${common.build_unflags} build_flags = -g -DESP32 -DARDUINO_ARCH_ESP32 -DARDUINO_ARCH_ESP32S3 -DCONFIG_IDF_TARGET_ESP32S3=1 -DARDUINO_USB_MSC_ON_BOOT=0 -DARDUINO_DFU_ON_BOOT=0 -DCO ;; please make sure that the following flags are properly set (to 0 or 1) by your board.json, or included in your custom platformio_override.ini entry: ;; ARDUINO_USB_MODE, ARDUINO_USB_CDC_ON_BOOT ${esp32_idf_V4.build_flags} lib_deps = ${esp32_idf_V4.lib_deps} board_build.partitions = ${esp32.large_partitions} ;; default partioning for 8MB flash - can be overridden in build envs # ------------------------------------------------------------------------------ # WLED BUILDS # ------------------------------------------------------------------------------ [env:nodemcuv2] board = nodemcuv2 platform = ${common.platform_wled_default} platform_packages = ${common.platform_packages} board_build.ldscript = ${common.ldscript_4m1m} build_unflags = ${common.build_unflags} build_flags = ${common.build_flags} ${esp8266.build_flags} -D WLED_RELEASE_NAME=\"ESP8266\" #-DWLED_DISABLE_2D -D WLED_DISABLE_PARTICLESYSTEM2D lib_deps = ${esp8266.lib_deps} monitor_filters = esp8266_exception_decoder [env:nodemcuv2_compat] extends = env:nodemcuv2 ;; using platform version and build options from WLED 0.14.0 platform = ${esp8266.platform_compat} platform_packages = ${esp8266.platform_packages_compat} build_flags = ${common.build_flags} ${esp8266.build_flags_compat} -D WLED_RELEASE_NAME=\"ESP8266_compat\" #-DWLED_DISABLE_2D -D WLED_DISABLE_PARTICLESYSTEM2D ;; lib_deps = ${esp8266.lib_deps_compat} ;; experimental - use older NeoPixelBus 2.7.9 [env:nodemcuv2_160] extends = env:nodemcuv2 board_build.f_cpu = 160000000L build_flags = ${common.build_flags} ${esp8266.build_flags} -D WLED_RELEASE_NAME=\"ESP8266_160\" #-DWLED_DISABLE_2D -D WLED_DISABLE_PARTICLESYSTEM2D custom_usermods = audioreactive [env:esp8266_2m] board = esp_wroom_02 platform = ${common.platform_wled_default} platform_packages = ${common.platform_packages} board_build.ldscript = ${common.ldscript_2m512k} build_unflags = ${common.build_unflags} build_flags = ${common.build_flags} ${esp8266.build_flags} -D WLED_RELEASE_NAME=\"ESP02\" -D WLED_DISABLE_PARTICLESYSTEM2D -D WLED_DISABLE_PARTICLESYSTEM1D lib_deps = ${esp8266.lib_deps} [env:esp8266_2m_compat] extends = env:esp8266_2m ;; using platform version and build options from WLED 0.14.0 platform = ${esp8266.platform_compat} platform_packages = ${esp8266.platform_packages_compat} build_flags = ${common.build_flags} ${esp8266.build_flags_compat} -D WLED_RELEASE_NAME=\"ESP02_compat\" #-DWLED_DISABLE_2D -D WLED_DISABLE_PARTICLESYSTEM1D -D WLED_DISABLE_PARTICLESYSTEM2D [env:esp8266_2m_160] extends = env:esp8266_2m board_build.f_cpu = 160000000L build_flags = ${common.build_flags} ${esp8266.build_flags} -D WLED_RELEASE_NAME=\"ESP02_160\" -D WLED_DISABLE_PARTICLESYSTEM1D -D WLED_DISABLE_PARTICLESYSTEM2D custom_usermods = audioreactive [env:esp01_1m_full] board = esp01_1m platform = ${common.platform_wled_default} platform_packages = ${common.platform_packages} board_build.ldscript = ${common.ldscript_1m128k} build_unflags = ${common.build_unflags} build_flags = ${common.build_flags} ${esp8266.build_flags} -D WLED_RELEASE_NAME=\"ESP01\" -D WLED_DISABLE_OTA ; -D WLED_USE_REAL_MATH ;; may fix wrong sunset/sunrise times, at the cost of 7064 bytes FLASH and 975 bytes RAM -D WLED_DISABLE_PARTICLESYSTEM1D -D WLED_DISABLE_PARTICLESYSTEM2D lib_deps = ${esp8266.lib_deps} [env:esp01_1m_full_compat] extends = env:esp01_1m_full ;; using platform version and build options from WLED 0.14.0 platform = ${esp8266.platform_compat} platform_packages = ${esp8266.platform_packages_compat} build_flags = ${common.build_flags} ${esp8266.build_flags_compat} -D WLED_RELEASE_NAME=\"ESP01_compat\" -D WLED_DISABLE_OTA #-DWLED_DISABLE_2D -D WLED_DISABLE_PARTICLESYSTEM1D -D WLED_DISABLE_PARTICLESYSTEM2D [env:esp01_1m_full_160] extends = env:esp01_1m_full board_build.f_cpu = 160000000L build_flags = ${common.build_flags} ${esp8266.build_flags} -D WLED_RELEASE_NAME=\"ESP01_160\" -D WLED_DISABLE_OTA ; -D WLED_USE_REAL_MATH ;; may fix wrong sunset/sunrise times, at the cost of 7064 bytes FLASH and 975 bytes RAM -D WLED_DISABLE_PARTICLESYSTEM1D -D WLED_DISABLE_PARTICLESYSTEM2D custom_usermods = audioreactive [env:esp32dev] board = esp32dev platform = ${esp32_idf_V4.platform} platform_packages = ${esp32_idf_V4.platform_packages} build_unflags = ${common.build_unflags} custom_usermods = audioreactive build_flags = ${common.build_flags} ${esp32_idf_V4.build_flags} -D WLED_RELEASE_NAME=\"ESP32\" #-D WLED_DISABLE_BROWNOUT_DET -DARDUINO_USB_CDC_ON_BOOT=0 ;; this flag is mandatory for "classic ESP32" when building with arduino-esp32 >=2.0.3 lib_deps = ${esp32_idf_V4.lib_deps} monitor_filters = esp32_exception_decoder board_build.partitions = ${esp32.default_partitions} board_build.flash_mode = dio [env:esp32dev_debug] extends = env:esp32dev upload_speed = 921600 build_flags = ${common.build_flags} ${esp32_idf_V4.build_flags} -D WLED_DEBUG -D WLED_RELEASE_NAME=\"ESP32_DEBUG\" [env:esp32dev_8M] board = esp32dev platform = ${esp32_idf_V4.platform} platform_packages = ${esp32_idf_V4.platform_packages} custom_usermods = audioreactive build_unflags = ${common.build_unflags} build_flags = ${common.build_flags} ${esp32_idf_V4.build_flags} -D WLED_RELEASE_NAME=\"ESP32_8M\" #-D WLED_DISABLE_BROWNOUT_DET -DARDUINO_USB_CDC_ON_BOOT=0 ;; this flag is mandatory for "classic ESP32" when building with arduino-esp32 >=2.0.3 lib_deps = ${esp32_idf_V4.lib_deps} monitor_filters = esp32_exception_decoder board_build.partitions = ${esp32.large_partitions} board_upload.flash_size = 8MB board_upload.maximum_size = 8388608 ; board_build.f_flash = 80000000L board_build.flash_mode = dio [env:esp32dev_16M] board = esp32dev platform = ${esp32_idf_V4.platform} platform_packages = ${esp32_idf_V4.platform_packages} custom_usermods = audioreactive build_unflags = ${common.build_unflags} build_flags = ${common.build_flags} ${esp32_idf_V4.build_flags} -D WLED_RELEASE_NAME=\"ESP32_16M\" #-D WLED_DISABLE_BROWNOUT_DET -DARDUINO_USB_CDC_ON_BOOT=0 ;; this flag is mandatory for "classic ESP32" when building with arduino-esp32 >=2.0.3 lib_deps = ${esp32_idf_V4.lib_deps} monitor_filters = esp32_exception_decoder board_build.partitions = ${esp32.extreme_partitions} board_upload.flash_size = 16MB board_upload.maximum_size = 16777216 board_build.f_flash = 80000000L board_build.flash_mode = dio [env:esp32_eth] board = esp32-poe platform = ${esp32_idf_V4.platform} platform_packages = ${esp32_idf_V4.platform_packages} upload_speed = 921600 custom_usermods = audioreactive build_unflags = ${common.build_unflags} build_flags = ${common.build_flags} ${esp32.build_flags} -D WLED_RELEASE_NAME=\"ESP32_Ethernet\" -D RLYPIN=-1 -D WLED_USE_ETHERNET -D BTNPIN=-1 -D SR_DMTYPE=-1 -D AUDIOPIN=-1 -D I2S_SDPIN=-1 -D I2S_WSPIN=-1 -D I2S_CKPIN=-1 -D MCLK_PIN=-1 ;; force AR to not allocate any PINs at startup -D DATA_PINS=4 ;; default led pin = 16 conflicts with pins used for ethernet ; -D WLED_DISABLE_ESPNOW ;; ESP-NOW requires wifi, may crash with ethernet only => uncomment if your board uses ETH_CLOCK_GPIO0_OUT, ETH_CLOCK_GPIO16_OUT, ETH_CLOCK_GPIO17_OUT -DARDUINO_USB_CDC_ON_BOOT=0 ;; this flag is mandatory for "classic ESP32" when building with arduino-esp32 >=2.0.3 lib_deps = ${esp32.lib_deps} board_build.partitions = ${esp32.default_partitions} board_build.flash_mode = dio [env:esp32_wrover] extends = esp32_idf_V4 board = ttgo-t7-v14-mini32 board_build.f_flash = 80000000L board_build.flash_mode = qio board_build.partitions = ${esp32.extended_partitions} custom_usermods = audioreactive build_unflags = ${common.build_unflags} build_flags = ${common.build_flags} ${esp32_idf_V4.build_flags} -D WLED_RELEASE_NAME=\"ESP32_WROVER\" -DARDUINO_USB_CDC_ON_BOOT=0 ;; this flag is mandatory for "classic ESP32" when building with arduino-esp32 >=2.0.3 -DBOARD_HAS_PSRAM -mfix-esp32-psram-cache-issue ;; Older ESP32 (rev.<3) need a PSRAM fix (increases static RAM used) https://docs.espressif.com/projects/esp-idf/en/stable/esp32/api-guides/external-ram.html -D DATA_PINS=25 lib_deps = ${esp32_idf_V4.lib_deps} [env:esp32c3dev] extends = esp32c3 platform = ${esp32c3.platform} platform_packages = ${esp32c3.platform_packages} framework = arduino board = esp32-c3-devkitm-1 board_build.partitions = ${esp32.default_partitions} build_flags = ${common.build_flags} ${esp32c3.build_flags} -D WLED_RELEASE_NAME=\"ESP32-C3\" -D WLED_WATCHDOG_TIMEOUT=0 -DLOLIN_WIFI_FIX ; seems to work much better with this -DARDUINO_USB_CDC_ON_BOOT=1 ;; for virtual CDC USB ;-DARDUINO_USB_CDC_ON_BOOT=0 ;; for serial-to-USB chip upload_speed = 460800 build_unflags = ${common.build_unflags} lib_deps = ${esp32c3.lib_deps} board_build.flash_mode = dio ; safe default, required for OTA updates to 0.16 from older version which used dio (must match the bootloader!) [env:esp32c3dev_qio] extends = env:esp32c3dev build_flags = ${common.build_flags} ${esp32c3.build_flags} -D WLED_RELEASE_NAME=\"ESP32-C3-QIO\" board_build.flash_mode = qio ; qio is faster and works on almost all boards (some boards may use dio to get 2 extra pins) [env:esp32s3dev_16MB_opi] ;; ESP32-S3 development board, with 16MB FLASH and >= 8MB PSRAM (memory_type: qio_opi) board = esp32-s3-devkitc-1 ;; generic dev board; the next line adds PSRAM support board_build.arduino.memory_type = qio_opi ;; use with PSRAM: 8MB or 16MB platform = ${esp32s3.platform} platform_packages = ${esp32s3.platform_packages} upload_speed = 921600 custom_usermods = audioreactive build_unflags = ${common.build_unflags} build_flags = ${common.build_flags} ${esp32s3.build_flags} -D WLED_RELEASE_NAME=\"ESP32-S3_16MB_opi\" -D WLED_WATCHDOG_TIMEOUT=0 ;-D ARDUINO_USB_CDC_ON_BOOT=0 ;; -D ARDUINO_USB_MODE=1 ;; for boards with serial-to-USB chip -D ARDUINO_USB_CDC_ON_BOOT=1 ;; -D ARDUINO_USB_MODE=1 ;; for boards with USB-OTG connector only (USBCDC or "TinyUSB") -DBOARD_HAS_PSRAM lib_deps = ${esp32s3.lib_deps} board_build.partitions = ${esp32.extreme_partitions} board_upload.flash_size = 16MB board_upload.maximum_size = 16777216 board_build.f_flash = 80000000L board_build.flash_mode = qio monitor_filters = esp32_exception_decoder [env:esp32s3dev_8MB_opi] ;; ESP32-S3 development board, with 8MB FLASH and >= 8MB PSRAM (memory_type: qio_opi) board = esp32-s3-devkitc-1 ;; generic dev board; the next line adds PSRAM support board_build.arduino.memory_type = qio_opi ;; use with PSRAM: 8MB or 16MB platform = ${esp32s3.platform} platform_packages = ${esp32s3.platform_packages} upload_speed = 921600 custom_usermods = audioreactive build_unflags = ${common.build_unflags} build_flags = ${common.build_flags} ${esp32s3.build_flags} -D WLED_RELEASE_NAME=\"ESP32-S3_8MB_opi\" -D WLED_WATCHDOG_TIMEOUT=0 ;-D ARDUINO_USB_CDC_ON_BOOT=0 ;; -D ARDUINO_USB_MODE=1 ;; for boards with serial-to-USB chip -D ARDUINO_USB_CDC_ON_BOOT=1 ;; -D ARDUINO_USB_MODE=1 ;; for boards with USB-OTG connector only (USBCDC or "TinyUSB") -DBOARD_HAS_PSRAM lib_deps = ${esp32s3.lib_deps} board_build.partitions = ${esp32.large_partitions} board_build.f_flash = 80000000L board_build.flash_mode = qio monitor_filters = esp32_exception_decoder [env:esp32s3dev_8MB_qspi] ;; generic ESP32-S3 board with 8MB FLASH and PSRAM (memory_type: qio_qspi). Try this one if esp32s3dev_8MB_opi does not work on your board extends = env:esp32s3dev_8MB_opi board_build.arduino.memory_type = qio_qspi board_build.flash_mode = qio build_flags = ${common.build_flags} ${esp32s3.build_flags} -D WLED_RELEASE_NAME=\"ESP32-S3_8MB_qspi\" -D WLED_WATCHDOG_TIMEOUT=0 ;-D ARDUINO_USB_CDC_ON_BOOT=0 ;; -D ARDUINO_USB_MODE=1 ;; for boards with serial-to-USB chip -D ARDUINO_USB_CDC_ON_BOOT=1 ;; -D ARDUINO_USB_MODE=1 ;; for boards with USB-OTG connector only (USBCDC or "TinyUSB") -DBOARD_HAS_PSRAM ;; -DLOLIN_WIFI_FIX ;; uncomment if you have WiFi connectivity problems monitor_filters = esp32_exception_decoder [env:esp32S3_wroom2] ;; For ESP32-S3 WROOM-2, a.k.a. ESP32-S3 DevKitC-1 v1.1 ;; with >= 16MB FLASH and >= 8MB PSRAM (memory_type: opi_opi) platform = ${esp32s3.platform} platform_packages = ${esp32s3.platform_packages} board = esp32s3camlcd ;; this is the only standard board with "opi_opi" board_build.arduino.memory_type = opi_opi upload_speed = 921600 custom_usermods = audioreactive build_unflags = ${common.build_unflags} build_flags = ${common.build_flags} ${esp32s3.build_flags} -D WLED_RELEASE_NAME=\"ESP32-S3_WROOM-2\" -D WLED_WATCHDOG_TIMEOUT=0 -D ARDUINO_USB_CDC_ON_BOOT=0 ;; -D ARDUINO_USB_MODE=1 ;; for boards with serial-to-USB chip ;; -D ARDUINO_USB_CDC_ON_BOOT=1 ;; -D ARDUINO_USB_MODE=1 ;; for boards with USB-OTG connector only (USBCDC or "TinyUSB") -DBOARD_HAS_PSRAM -D LEDPIN=38 -D DATA_PINS=38 ;; buildin WS2812b LED -D BTNPIN=0 -D RLYPIN=16 -D IRPIN=17 -D AUDIOPIN=-1 ;;-D WLED_DEBUG -D SR_DMTYPE=1 -D I2S_SDPIN=13 -D I2S_CKPIN=14 -D I2S_WSPIN=15 -D MCLK_PIN=4 ;; I2S mic lib_deps = ${esp32s3.lib_deps} board_build.partitions = ${esp32.extreme_partitions} board_upload.flash_size = 16MB board_upload.maximum_size = 16777216 monitor_filters = esp32_exception_decoder [env:esp32S3_wroom2_32MB] ;; For ESP32-S3 WROOM-2 with 32MB Flash, and >= 8MB PSRAM (memory_type: opi_opi) extends = env:esp32S3_wroom2 build_flags = ${common.build_flags} ${esp32s3.build_flags} -D WLED_RELEASE_NAME=\"ESP32-S3_WROOM-2_32MB\" -D WLED_WATCHDOG_TIMEOUT=0 -D ARDUINO_USB_CDC_ON_BOOT=0 ;; -D ARDUINO_USB_MODE=1 ;; for boards with serial-to-USB chip ;; -D ARDUINO_USB_CDC_ON_BOOT=1 ;; -D ARDUINO_USB_MODE=1 ;; for boards with USB-OTG connector only (USBCDC or "TinyUSB") -DBOARD_HAS_PSRAM -D LEDPIN=38 -D DATA_PINS=38 ;; buildin WS2812b LED -D BTNPIN=0 -D RLYPIN=16 -D IRPIN=17 -D AUDIOPIN=-1 ;;-D WLED_DEBUG -D SR_DMTYPE=1 -D I2S_SDPIN=13 -D I2S_CKPIN=14 -D I2S_WSPIN=15 -D MCLK_PIN=4 ;; I2S mic board_build.partitions = tools/WLED_ESP32_32MB.csv board_upload.flash_size = 32MB board_upload.maximum_size = 33554432 monitor_filters = esp32_exception_decoder [env:esp32s3_4M_qspi] ;; ESP32-S3, with 4MB FLASH and <= 4MB PSRAM (memory_type: qio_qspi) board = lolin_s3_mini ;; -S3 mini, 4MB flash 2MB PSRAM platform = ${esp32s3.platform} platform_packages = ${esp32s3.platform_packages} upload_speed = 921600 custom_usermods = audioreactive build_unflags = ${common.build_unflags} build_flags = ${common.build_flags} ${esp32s3.build_flags} -D WLED_RELEASE_NAME=\"ESP32-S3_4M_qspi\" -DARDUINO_USB_CDC_ON_BOOT=1 ;; -DARDUINO_USB_MODE=1 ;; for boards with USB-OTG connector only (USBCDC or "TinyUSB") -DBOARD_HAS_PSRAM -DLOLIN_WIFI_FIX ; seems to work much better with this -D WLED_WATCHDOG_TIMEOUT=0 lib_deps = ${esp32s3.lib_deps} board_build.partitions = ${esp32.default_partitions} board_build.f_flash = 80000000L board_build.flash_mode = qio monitor_filters = esp32_exception_decoder [env:lolin_s2_mini] platform = ${esp32s2.platform} platform_packages = ${esp32s2.platform_packages} board = lolin_s2_mini board_build.partitions = ${esp32.default_partitions} board_build.flash_mode = qio board_build.f_flash = 80000000L custom_usermods = audioreactive build_unflags = ${common.build_unflags} build_flags = ${common.build_flags} ${esp32s2.build_flags} -D WLED_RELEASE_NAME=\"ESP32-S2\" -DARDUINO_USB_CDC_ON_BOOT=1 -DARDUINO_USB_MSC_ON_BOOT=0 -DARDUINO_USB_DFU_ON_BOOT=0 -DBOARD_HAS_PSRAM -DLOLIN_WIFI_FIX ; seems to work much better with this -D WLED_WATCHDOG_TIMEOUT=0 -D DATA_PINS=16 -D HW_PIN_SCL=35 -D HW_PIN_SDA=33 -D HW_PIN_CLOCKSPI=7 -D HW_PIN_DATASPI=11 -D HW_PIN_MISOSPI=9 ; -D STATUSLED=15 lib_deps = ${esp32s2.lib_deps} [env:usermods] board = esp32dev platform = ${esp32_idf_V4.platform} platform_packages = ${esp32_idf_V4.platform_packages} build_unflags = ${common.build_unflags} build_flags = ${common.build_flags} ${esp32_idf_V4.build_flags} -D WLED_RELEASE_NAME=\"ESP32_USERMODS\" -DTOUCH_CS=9 lib_deps = ${esp32_idf_V4.lib_deps} monitor_filters = esp32_exception_decoder board_build.flash_mode = dio custom_usermods = * ; Expands to all usermods in usermods folder board_build.partitions = ${esp32.extreme_partitions} ; We're gonna need a bigger boat ================================================ FILE: platformio_override.sample.ini ================================================ # Example PlatformIO Project Configuration Override # ------------------------------------------------------------------------------ # Copy to platformio_override.ini to activate overrides # ------------------------------------------------------------------------------ # Please visit documentation: https://docs.platformio.org/page/projectconf.html [platformio] default_envs = WLED_generic8266_1M, esp32dev_V4_dio80 # put the name(s) of your own build environment here. You can define as many as you need #---------- # SAMPLE #---------- [env:WLED_generic8266_1M] extends = env:esp01_1m_full # when you want to extend the existing environment (define only updated options) ; board = esp01_1m # uncomment when ou need different board ; platform = ${common.platform_wled_default} # uncomment and change when you want particular platform ; platform_packages = ${common.platform_packages} ; board_build.ldscript = ${common.ldscript_1m128k} ; upload_speed = 921600 # fast upload speed (remove ';' if your board supports fast upload speed) # Sample libraries used for various usermods. Uncomment when using particular usermod. lib_deps = ${esp8266.lib_deps} ; olikraus/U8g2 # @~2.33.15 ; paulstoffregen/OneWire@~2.3.8 ; adafruit/Adafruit Unified Sensor@^1.1.4 ; adafruit/DHT sensor library@^1.4.1 ; adafruit/Adafruit BME280 Library@^2.2.2 ; Wire ; robtillaart/SHT85@~0.3.3 ; ;gmag11/QuickESPNow @ ~0.7.0 # will also load QuickDebug ; https://github.com/blazoncek/QuickESPNow.git#optional-debug ;; exludes debug library build_unflags = ${common.build_unflags} build_flags = ${common.build_flags} ${esp8266.build_flags} ; ; *** To use the below defines/overrides, copy and paste each onto its own line just below build_flags in the section above. ; ; Set a release name that may be used to distinguish required binary for flashing ; -D WLED_RELEASE_NAME=\"ESP32_MULTI_USREMODS\" ; ; disable specific features ; -D WLED_DISABLE_OTA ; -D WLED_DISABLE_ALEXA ; -D WLED_DISABLE_HUESYNC ; -D WLED_DISABLE_LOXONE ; -D WLED_DISABLE_INFRARED ; -D WLED_DISABLE_WEBSOCKETS ; -D WLED_DISABLE_MQTT ; -D WLED_DISABLE_ADALIGHT ; -D WLED_DISABLE_2D ; -D WLED_DISABLE_PXMAGIC ; -D WLED_DISABLE_ESPNOW ; -D WLED_DISABLE_BROWNOUT_DET ; ; enable optional built-in features ; -D WLED_ENABLE_PIXART ; -D WLED_ENABLE_USERMOD_PAGE # if created ; -D WLED_ENABLE_DMX ; ; PIN defines - uncomment and change, if needed: ; -D DATA_PINS=2 ; or use this for multiple outputs ; -D DATA_PINS=1,3 ; -D BTNPIN=0 ; -D IRPIN=4 ; -D RLYPIN=12 ; -D RLYMDE=1 ; -D RLYODRAIN=0 ; -D LED_BUILTIN=2 # GPIO of built-in LED ; ; Limit max buses ; -D WLED_MAX_BUSSES=2 ; -D WLED_MAX_ANALOG_CHANNELS=3 # only 3 PWM HW pins available ; -D WLED_MAX_DIGITAL_CHANNELS=2 # only 2 HW accelerated pins available ; ; Configure default WiFi ; -D CLIENT_SSID='"MyNetwork"' ; -D CLIENT_PASS='"Netw0rkPassw0rd"' ; ; Configure and use Ethernet ; -D WLED_USE_ETHERNET ; -D WLED_ETH_DEFAULT=5 ; do not use pins 5, (16,) 17, 18, 19, 21, 22, 23, 25, 26, 27 for anything but ethernet ; -D PHY_ADDR=0 -D ETH_PHY_POWER=5 -D ETH_PHY_MDC=23 -D ETH_PHY_MDIO=18 ; -D ETH_CLK_MODE=ETH_CLOCK_GPIO17_OUT ; ; NTP time configuration ; -D WLED_NTP_ENABLED=true ; -D WLED_TIMEZONE=2 ; -D WLED_LAT=48.86 ; -D WLED_LON=2.33 ; ; Use Watchdog timer with 10s guard ; -D WLED_WATCHDOG_TIMEOUT=10 ; ; Create debug build (with remote debug) ; -D WLED_DEBUG ; -D WLED_DEBUG_HOST='"192.168.0.100"' ; -D WLED_DEBUG_PORT=7868 ; ; Use Autosave usermod and set it to do save after 90s ; -D USERMOD_AUTO_SAVE ; -D AUTOSAVE_AFTER_SEC=90 ; ; Use AHT10/AHT15/AHT20 usermod ; -D USERMOD_AHT10 ; ; Use INA226 usermod ; -D USERMOD_INA226 ; ; Use 4 Line Display usermod with SPI display ; -D USERMOD_FOUR_LINE_DISPLAY ; -DFLD_SPI_DEFAULT ; -D FLD_TYPE=SSD1306_SPI64 ; -D FLD_PIN_CLOCKSPI=14 ; -D FLD_PIN_DATASPI=13 ; -D FLD_PIN_DC=26 ; -D FLD_PIN_CS=15 ; -D FLD_PIN_RESET=27 ; ; Use Rotary encoder usermod (in conjunction with 4LD) ; -D USERMOD_ROTARY_ENCODER_UI ; -D ENCODER_DT_PIN=5 ; -D ENCODER_CLK_PIN=18 ; -D ENCODER_SW_PIN=19 ; ; Use Dallas DS18B20 temperature sensor usermod and configure it to use GPIO13 ; -D USERMOD_DALLASTEMPERATURE ; -D TEMPERATURE_PIN=13 ; ; Use Multi Relay usermod and configure it to use 6 relays and appropriate GPIO ; -D USERMOD_MULTI_RELAY ; -D MULTI_RELAY_MAX_RELAYS=6 ; -D MULTI_RELAY_PINS=12,23,22,21,24,25 ; ; Use PIR sensor usermod and configure it to use GPIO4 and timer of 60s ; -D USERMOD_PIRSWITCH ; -D PIR_SENSOR_PIN=4 # use -1 to disable usermod ; -D PIR_SENSOR_OFF_SEC=60 ; -D PIR_SENSOR_MAX_SENSORS=2 # max allowable sensors (uses OR logic for triggering) ; ; Use Audioreactive usermod and configure I2S microphone ; -D AUDIOPIN=-1 ; -D DMTYPE=1 # 0-analog/disabled, 1-I2S generic, 2-ES7243, 3-SPH0645, 4-I2S+mclk, 5-I2S PDM ; -D I2S_SDPIN=36 ; -D I2S_WSPIN=23 ; -D I2S_CKPIN=19 ; ; Use PWM fan usermod ; -D USERMOD_PWM_FAN ; -D TACHO_PIN=33 ; -D PWM_PIN=32 ; ; Use POV Display usermod ; -D USERMOD_POV_DISPLAY ; Use built-in or custom LED as a status indicator (assumes LED is connected to GPIO16) ; -D STATUSLED=16 ; ; set the name of the module - make sure there is a quote-backslash-quote before the name and a backslash-quote-quote after the name ; -D SERVERNAME="\"WLED\"" ; ; set the number of LEDs ; -D PIXEL_COUNTS=30 ; or this for multiple outputs ; -D PIXEL_COUNTS=30,30 ; ; set the default LED type ; -D LED_TYPES=22 # see const.h (TYPE_xxxx) ; or this for multiple outputs ; -D LED_TYPES=TYPE_SK6812_RGBW,TYPE_WS2812_RGB ; ; set default color order of your led strip ; -D DEFAULT_LED_COLOR_ORDER=COL_ORDER_GRB ; ; set milliampere limit when using ESP power pin (or inadequate PSU) to power LEDs ; -D ABL_MILLIAMPS_DEFAULT=850 ; -D LED_MILLIAMPS_DEFAULT=55 ; ; enable IR by setting remote type ; -D IRTYPE=0 # 0 Remote disabled | 1 24-key RGB | 2 24-key with CT | 3 40-key blue | 4 40-key RGB | 5 21-key RGB | 6 6-key black | 7 9-key red | 8 JSON remote ; ; use PSRAM on classic ESP32 rev.1 (rev.3 or above has no issues) ; -DBOARD_HAS_PSRAM -mfix-esp32-psram-cache-issue # needed only for classic ESP32 rev.1 ; ; configure I2C and SPI interface (for various hardware) ; -D I2CSDAPIN=33 # initialise interface ; -D I2CSCLPIN=35 # initialise interface ; -D HW_PIN_SCL=35 ; -D HW_PIN_SDA=33 ; -D HW_PIN_CLOCKSPI=7 ; -D HW_PIN_DATASPI=11 ; -D HW_PIN_MISOSPI=9 # ------------------------------------------------------------------------------ # Optional: build flags for speed, instead of optimising for size. # Example of usage: see [env:esp32S3_PSRAM_HUB75] # ------------------------------------------------------------------------------ [Speed_Flags] build_unflags = -Os ;; to disable standard optimization for small size build_flags = -O2 ;; optimize for speed -free -fipa-pta ;; very useful, too ;;-fsingle-precision-constant ;; makes all floating point literals "float" (default is "double") ;;-funsafe-math-optimizations ;; less dangerous than -ffast-math; still allows the compiler to exploit FMA and reciprocals (up to 10% faster on -S3) # Important: we need to explicitly switch off some "-O2" optimizations -fno-jump-tables -fno-tree-switch-conversion ;; needed - firmware may crash otherwise -freorder-blocks -Wwrite-strings -fstrict-volatile-bitfields ;; needed - recommended by espressif # ------------------------------------------------------------------------------ # PRE-CONFIGURED DEVELOPMENT BOARDS AND CONTROLLERS # ------------------------------------------------------------------------------ [env:esp07] board = esp07 platform = ${common.platform_wled_default} platform_packages = ${common.platform_packages} board_build.ldscript = ${common.ldscript_4m1m} build_unflags = ${common.build_unflags} build_flags = ${common.build_flags} ${esp8266.build_flags} lib_deps = ${esp8266.lib_deps} [env:d1_mini] board = d1_mini platform = ${common.platform_wled_default} platform_packages = ${common.platform_packages} upload_speed = 921600 board_build.ldscript = ${common.ldscript_4m1m} build_unflags = ${common.build_unflags} build_flags = ${common.build_flags} ${esp8266.build_flags} lib_deps = ${esp8266.lib_deps} monitor_filters = esp8266_exception_decoder [env:heltec_wifi_kit_8] board = d1_mini platform = ${common.platform_wled_default} platform_packages = ${common.platform_packages} board_build.ldscript = ${common.ldscript_4m1m} build_unflags = ${common.build_unflags} build_flags = ${common.build_flags} ${esp8266.build_flags} lib_deps = ${esp8266.lib_deps} [env:h803wf] board = d1_mini platform = ${common.platform_wled_default} platform_packages = ${common.platform_packages} board_build.ldscript = ${common.ldscript_4m1m} build_unflags = ${common.build_unflags} build_flags = ${common.build_flags} ${esp8266.build_flags} -D DATA_PINS=1 -D WLED_DISABLE_INFRARED lib_deps = ${esp8266.lib_deps} [env:esp32dev_qio80] extends = env:esp32dev # we want to extend the existing esp32dev environment (and define only updated options) board = esp32dev build_flags = ${common.build_flags} ${esp32.build_flags} #-D WLED_DISABLE_BROWNOUT_DET lib_deps = ${esp32.lib_deps} monitor_filters = esp32_exception_decoder board_build.f_flash = 80000000L board_build.flash_mode = qio [env:esp32dev_V4_dio80] ;; experimental ESP32 env using ESP-IDF V4.4.x ;; Warning: this build environment is not stable!! ;; please erase your device before installing. extends = esp32_idf_V4 # based on newer "esp-idf V4" platform environment board = esp32dev build_flags = ${common.build_flags} ${esp32_idf_V4.build_flags} #-D WLED_DISABLE_BROWNOUT_DET lib_deps = ${esp32_idf_V4.lib_deps} monitor_filters = esp32_exception_decoder board_build.partitions = ${esp32.default_partitions} ;; if you get errors about "out of program space", change this to ${esp32.extended_partitions} or even ${esp32.big_partitions} board_build.f_flash = 80000000L board_build.flash_mode = dio [env:esp32s2_saola] extends = esp32s2 board = esp32-s2-saola-1 platform = ${esp32s2.platform} platform_packages = ${esp32s2.platform_packages} framework = arduino board_build.flash_mode = qio upload_speed = 460800 build_flags = ${common.build_flags} ${esp32s2.build_flags} ;-DLOLIN_WIFI_FIX ;; try this in case Wifi does not work -DARDUINO_USB_CDC_ON_BOOT=1 lib_deps = ${esp32s2.lib_deps} [env:esp32s3dev_8MB_PSRAM_qspi] ;; ESP32-TinyS3 development board, with 8MB FLASH and PSRAM (memory_type: qio_qspi) extends = env:esp32s3dev_8MB_PSRAM_opi ;board = um_tinys3 ; -> needs workaround from https://github.com/wled-dev/WLED/pull/2905#issuecomment-1328049860 board = esp32-s3-devkitc-1 ;; generic dev board; the next line adds PSRAM support board_build.arduino.memory_type = qio_qspi ;; use with PSRAM: 2MB or 4MB [env:esp8285_4CH_MagicHome] board = esp8285 platform = ${common.platform_wled_default} platform_packages = ${common.platform_packages} board_build.ldscript = ${common.ldscript_1m128k} build_unflags = ${common.build_unflags} build_flags = ${common.build_flags} ${esp8266.build_flags} -D WLED_DISABLE_OTA lib_deps = ${esp8266.lib_deps} [env:esp8285_H801] board = esp8285 platform = ${common.platform_wled_default} platform_packages = ${common.platform_packages} board_build.ldscript = ${common.ldscript_1m128k} build_unflags = ${common.build_unflags} build_flags = ${common.build_flags} ${esp8266.build_flags} -D WLED_DISABLE_OTA lib_deps = ${esp8266.lib_deps} [env:d1_mini_5CH_Shojo_PCB] board = d1_mini platform = ${common.platform_wled_default} platform_packages = ${common.platform_packages} board_build.ldscript = ${common.ldscript_4m1m} build_unflags = ${common.build_unflags} build_flags = ${common.build_flags} ${esp8266.build_flags} -D WLED_USE_SHOJO_PCB ;; NB: WLED_USE_SHOJO_PCB is not used anywhere in the source code. Not sure why its needed. lib_deps = ${esp8266.lib_deps} [env:d1_mini_debug] board = d1_mini build_type = debug platform = ${common.platform_wled_default} platform_packages = ${common.platform_packages} board_build.ldscript = ${common.ldscript_4m1m} build_unflags = ${common.build_unflags} build_flags = ${common.build_flags} ${esp8266.build_flags} ${common.debug_flags} lib_deps = ${esp8266.lib_deps} [env:d1_mini_ota] board = d1_mini upload_protocol = espota # exchange for your WLED IP upload_port = "10.10.1.27" platform = ${common.platform_wled_default} platform_packages = ${common.platform_packages} board_build.ldscript = ${common.ldscript_4m1m} build_unflags = ${common.build_unflags} build_flags = ${common.build_flags} ${esp8266.build_flags} lib_deps = ${esp8266.lib_deps} [env:anavi_miracle_controller] board = d1_mini platform = ${common.platform_wled_default} platform_packages = ${common.platform_packages} board_build.ldscript = ${common.ldscript_4m1m} build_unflags = ${common.build_unflags} build_flags = ${common.build_flags} ${esp8266.build_flags} -D DATA_PINS=12 -D IRPIN=-1 -D RLYPIN=2 lib_deps = ${esp8266.lib_deps} [env:esp32c3dev_2MB] ;; for ESP32-C3 boards with 2MB flash (instead of 4MB). ;; this board need a specific partition file. OTA not possible. extends = esp32c3 platform = ${esp32c3.platform} platform_packages = ${esp32c3.platform_packages} board = esp32-c3-devkitm-1 build_flags = ${common.build_flags} ${esp32c3.build_flags} -D WLED_WATCHDOG_TIMEOUT=0 -D WLED_DISABLE_OTA ; -DARDUINO_USB_CDC_ON_BOOT=1 ;; for virtual CDC USB -DARDUINO_USB_CDC_ON_BOOT=0 ;; for serial-to-USB chip build_unflags = ${common.build_unflags} upload_speed = 115200 lib_deps = ${esp32c3.lib_deps} board_build.partitions = tools/WLED_ESP32_2MB_noOTA.csv board_build.flash_mode = dio board_upload.flash_size = 2MB board_upload.maximum_size = 2097152 [env:wemos_shield_esp32] extends = esp32 ;; use default esp32 platform board = esp32dev upload_speed = 460800 build_flags = ${common.build_flags} ${esp32.build_flags} -D WLED_RELEASE_NAME=\"ESP32_wemos_shield\" -D DATA_PINS=16 -D RLYPIN=19 -D BTNPIN=17 -D IRPIN=18 -UWLED_USE_MY_CONFIG -D USERMOD_DALLASTEMPERATURE -D USERMOD_FOUR_LINE_DISPLAY -D TEMPERATURE_PIN=23 lib_deps = ${esp32.lib_deps} OneWire@~2.3.5 ;; needed for USERMOD_DALLASTEMPERATURE olikraus/U8g2 @ ^2.28.8 ;; needed for USERMOD_FOUR_LINE_DISPLAY board_build.partitions = ${esp32.default_partitions} [env:esp32_pico-D4] extends = esp32 ;; use default esp32 platform board = pico32 ;; pico32-D4 is different from the standard esp32dev ;; hardware details from https://github.com/srg74/WLED-ESP32-pico build_flags = ${common.build_flags} ${esp32.build_flags} -D WLED_RELEASE_NAME=\"pico32-D4\" -D SERVERNAME='"WLED-pico32"' -D WLED_DISABLE_ADALIGHT ;; no serial-to-USB chip on this board - better to disable serial protocols -D DATA_PINS=2,18 ;; LED pins -D RLYPIN=19 -D BTNPIN=0 -D IRPIN=-1 ;; no default pin for IR -D UM_AUDIOREACTIVE_ENABLE ;; enable AR by default ;; Audioreactive settings for on-board microphone (ICS-43432) -D SR_DMTYPE=1 -D I2S_SDPIN=25 -D I2S_WSPIN=15 -D I2S_CKPIN=14 -D SR_SQUELCH=5 -D SR_GAIN=30 lib_deps = ${esp32.lib_deps} board_build.partitions = ${esp32.default_partitions} board_build.f_flash = 80000000L [env:m5atom] extends = env:esp32dev # we want to extend the existing esp32dev environment (and define only updated options) build_flags = ${common.build_flags} ${esp32.build_flags} -D DATA_PINS=27 -D BTNPIN=39 [env:sp501e] board = esp_wroom_02 platform = ${common.platform_wled_default} board_build.ldscript = ${common.ldscript_2m512k} build_flags = ${common.build_flags} ${esp8266.build_flags} -D DATA_PINS=3 -D BTNPIN=1 lib_deps = ${esp8266.lib_deps} [env:sp511e] board = esp_wroom_02 platform = ${common.platform_wled_default} board_build.ldscript = ${common.ldscript_2m512k} build_flags = ${common.build_flags} ${esp8266.build_flags} -D DATA_PINS=3 -D BTNPIN=2 -D IRPIN=5 -D WLED_MAX_BUTTONS=3 lib_deps = ${esp8266.lib_deps} [env:Athom_RGBCW] ;7w and 5w(GU10) bulbs board = esp8285 platform = ${common.platform_wled_default} platform_packages = ${common.platform_packages} board_build.ldscript = ${common.ldscript_2m512k} build_unflags = ${common.build_unflags} build_flags = ${common.build_flags} ${esp8266.build_flags} -D BTNPIN=-1 -D RLYPIN=-1 -D DATA_PINS=4,12,14,13,5 -D LED_TYPES=TYPE_ANALOG_5CH -D WLED_DISABLE_INFRARED -D WLED_MAX_CCT_BLEND=0 lib_deps = ${esp8266.lib_deps} [env:Athom_15w_RGBCW] ;15w bulb board = esp8285 platform = ${common.platform_wled_default} platform_packages = ${common.platform_packages} board_build.ldscript = ${common.ldscript_2m512k} build_unflags = ${common.build_unflags} build_flags = ${common.build_flags} ${esp8266.build_flags} -D BTNPIN=-1 -D RLYPIN=-1 -D DATA_PINS=4,12,14,5,13 -D LED_TYPES=TYPE_ANALOG_5CH -D WLED_DISABLE_INFRARED -D WLED_MAX_CCT_BLEND=0 -D WLED_USE_IC_CCT lib_deps = ${esp8266.lib_deps} [env:Athom_3Pin_Controller] ;small controller with only data board = esp8285 platform = ${common.platform_wled_default} platform_packages = ${common.platform_packages} board_build.ldscript = ${common.ldscript_2m512k} build_unflags = ${common.build_unflags} build_flags = ${common.build_flags} ${esp8266.build_flags} -D BTNPIN=0 -D RLYPIN=-1 -D DATA_PINS=1 -D WLED_DISABLE_INFRARED lib_deps = ${esp8266.lib_deps} [env:Athom_4Pin_Controller] ; With clock and data interface board = esp8285 platform = ${common.platform_wled_default} platform_packages = ${common.platform_packages} board_build.ldscript = ${common.ldscript_2m512k} build_unflags = ${common.build_unflags} build_flags = ${common.build_flags} ${esp8266.build_flags} -D BTNPIN=0 -D RLYPIN=12 -D DATA_PINS=1 -D WLED_DISABLE_INFRARED lib_deps = ${esp8266.lib_deps} [env:Athom_5Pin_Controller] ;Analog light strip controller board = esp8285 platform = ${common.platform_wled_default} platform_packages = ${common.platform_packages} board_build.ldscript = ${common.ldscript_2m512k} build_unflags = ${common.build_unflags} build_flags = ${common.build_flags} ${esp8266.build_flags} -D BTNPIN=0 -D RLYPIN=-1 DATA_PINS=4,12,14,13 -D WLED_DISABLE_INFRARED lib_deps = ${esp8266.lib_deps} [env:MY9291] board = esp01_1m platform = ${common.platform_wled_default} platform_packages = ${common.platform_packages} board_build.ldscript = ${common.ldscript_1m128k} build_unflags = ${common.build_unflags} build_flags = ${common.build_flags} ${esp8266.build_flags} -D WLED_DISABLE_OTA -D USERMOD_MY9291 lib_deps = ${esp8266.lib_deps} # ------------------------------------------------------------------------------ # codm pixel controller board configurations # codm-controller-0_6 can also be used for the TYWE3S controller # ------------------------------------------------------------------------------ [env:codm-controller-0_6] board = esp_wroom_02 platform = ${common.platform_wled_default} platform_packages = ${common.platform_packages} board_build.ldscript = ${common.ldscript_2m512k} build_unflags = ${common.build_unflags} build_flags = ${common.build_flags} ${esp8266.build_flags} lib_deps = ${esp8266.lib_deps} [env:codm-controller-0_6-rev2] board = esp_wroom_02 platform = ${common.platform_wled_default} platform_packages = ${common.platform_packages} board_build.ldscript = ${common.ldscript_4m1m} build_unflags = ${common.build_unflags} build_flags = ${common.build_flags} ${esp8266.build_flags} lib_deps = ${esp8266.lib_deps} # ------------------------------------------------------------------------------ # EleksTube-IPS # ------------------------------------------------------------------------------ [env:elekstube_ips] extends = esp32 ;; use default esp32 platform board = esp32dev upload_speed = 921600 custom_usermods = ${env:esp32dev.custom_usermods} RTC EleksTube_IPS build_flags = ${common.build_flags} ${esp32.build_flags} -D WLED_DISABLE_BROWNOUT_DET -D WLED_DISABLE_INFRARED -D DATA_PINS=12 -D RLYPIN=27 -D BTNPIN=34 -D PIXEL_COUNTS=6 # Display config -D ST7789_DRIVER -D TFT_WIDTH=135 -D TFT_HEIGHT=240 -D CGRAM_OFFSET -D TFT_SDA_READ -D TFT_MOSI=23 -D TFT_SCLK=18 -D TFT_DC=25 -D TFT_RST=26 -D SPI_FREQUENCY=40000000 -D USER_SETUP_LOADED monitor_filters = esp32_exception_decoder # ------------------------------------------------------------------------------ # Usermod examples # ------------------------------------------------------------------------------ # 433MHz RF remote example for esp32dev [env:esp32dev_usermod_RF433] extends = env:esp32dev custom_usermods = ${env:esp32dev.custom_usermods} RF433 # External usermod from a git repository. # The library's `library.json` must include `"build": {"libArchive": false}`. # The name PlatformIO assigns is taken from the library's `library.json` "name" field. # If that name doesn't match the repo name in the URL, use the "LibName = URL" form # shown in the commented-out line below to supply the name explicitly. [env:esp32dev_external_usermod] extends = env:esp32dev custom_usermods = ${env:esp32dev.custom_usermods} https://github.com/wled/wled-usermod-example.git#main # ------------------------------------------------------------------------------ # Hub75 examples # ------------------------------------------------------------------------------ # Note: some panels may experience ghosting with default full brightness. use -D WLED_HUB75_MAX_BRIGHTNESS=239 or lower to fix it. [env:esp32dev_hub75] board = esp32dev upload_speed = 921600 platform = ${esp32_idf_V4.platform} platform_packages = build_unflags = ${common.build_unflags} build_flags = ${common.build_flags} -D WLED_RELEASE_NAME=\"ESP32_hub75\" -D WLED_ENABLE_HUB75MATRIX -D NO_GFX -D WLED_DEBUG_BUS ; -D WLED_DEBUG -D SR_DMTYPE=1 -D I2S_SDPIN=-1 -D I2S_CKPIN=-1 -D I2S_WSPIN=-1 -D MCLK_PIN=-1 ;; Disable to prevent pin clash lib_deps = ${esp32_idf_V4.lib_deps} https://github.com/mrfaptastic/ESP32-HUB75-MatrixPanel-DMA.git#3.0.11 monitor_filters = esp32_exception_decoder board_build.partitions = ${esp32.default_partitions} board_build.flash_mode = dio custom_usermods = audioreactive [env:esp32dev_hub75_forum_pinout] extends = env:esp32dev_hub75 build_flags = ${common.build_flags} -D WLED_RELEASE_NAME=\"ESP32_hub75_forum_pinout\" -D WLED_ENABLE_HUB75MATRIX -D NO_GFX -D ESP32_FORUM_PINOUT ;; enable for SmartMatrix default pins -D WLED_DEBUG_BUS -D SR_DMTYPE=1 -D I2S_SDPIN=-1 -D I2S_CKPIN=-1 -D I2S_WSPIN=-1 -D MCLK_PIN=-1 ;; Disable to prevent pin clash ; -D WLED_DEBUG [env:adafruit_matrixportal_esp32s3] ; ESP32-S3 processor, 8 MB flash, 2 MB of PSRAM, dedicated driver pins for HUB75 board = adafruit_matrixportal_esp32s3_wled ; modified board definition: removed flash section that causes FS erase on upload ;; adafruit recommends to use arduino-esp32 2.0.14 ;;platform = espressif32@ ~6.5.0 ;;platform_packages = platformio/framework-arduinoespressif32 @ 3.20014.231204 ;; arduino-esp32 2.0.14 platform = ${esp32s3.platform} platform_packages = upload_speed = 921600 build_unflags = ${common.build_unflags} build_flags = ${common.build_flags} ${esp32s3.build_flags} -D WLED_RELEASE_NAME=\"ESP32-S3_8M_qspi\" -DARDUINO_USB_CDC_ON_BOOT=1 ;; -DARDUINO_USB_MODE=1 ;; for boards with USB-OTG connector only (USBCDC or "TinyUSB") -DBOARD_HAS_PSRAM -DLOLIN_WIFI_FIX ; seems to work much better with this (sets lower TX power) -D WLED_WATCHDOG_TIMEOUT=0 -D WLED_ENABLE_HUB75MATRIX -D NO_GFX -D S3_LCD_DIV_NUM=20 ;; Attempt to fix wifi performance issue when panel active with S3 chips -D ARDUINO_ADAFRUIT_MATRIXPORTAL_ESP32S3 -D WLED_DEBUG_BUS -D SR_DMTYPE=1 -D I2S_SDPIN=-1 -D I2S_CKPIN=-1 -D I2S_WSPIN=-1 -D MCLK_PIN=-1 ;; Disable to prevent pin clash lib_deps = ${esp32s3.lib_deps} https://github.com/mrfaptastic/ESP32-HUB75-MatrixPanel-DMA.git#aa28e2a ;; S3_LCD_DIV_NUM fix board_build.partitions = ${esp32.large_partitions} ;; standard bootloader and 8MB Flash partitions ;; board_build.partitions = tools/partitions-8MB_spiffs-tinyuf2.csv ;; supports adafruit UF2 bootloader board_build.f_flash = 80000000L board_build.flash_mode = qio monitor_filters = esp32_exception_decoder custom_usermods = audioreactive [env:esp32S3_PSRAM_HUB75] ;; MOONHUB HUB75 adapter board (lilygo T7-S3 with 16MB flash and PSRAM) board = lilygo-t7-s3 platform = ${esp32s3.platform} platform_packages = upload_speed = 921600 build_unflags = ${common.build_unflags} ${Speed_Flags.build_unflags} ;; optional: removes "-Os" so we can override with "-O2" in build_flags build_flags = ${common.build_flags} ${esp32s3.build_flags} -D WLED_RELEASE_NAME=\"esp32S3_16MB_PSRAM_HUB75\" ${Speed_Flags.build_flags} ;; optional: -O2 -> optimize for speed instead of size -DARDUINO_USB_CDC_ON_BOOT=1 ;; -DARDUINO_USB_MODE=1 ;; for boards with USB-OTG connector only (USBCDC or "TinyUSB") -DBOARD_HAS_PSRAM -DLOLIN_WIFI_FIX ; seems to work much better with this (sets lower TX power) -D WLED_WATCHDOG_TIMEOUT=0 -D WLED_ENABLE_HUB75MATRIX -D NO_GFX -D S3_LCD_DIV_NUM=20 ;; Attempt to fix wifi performance issue when panel active with S3 chips -D MOONHUB_S3_PINOUT ;; HUB75 pinout -D WLED_DEBUG_BUS -D LEDPIN=14 -D BTNPIN=0 -D RLYPIN=15 -D IRPIN=-1 -D AUDIOPIN=-1 ;; defaults that avoid pin conflicts with HUB75 -D SR_DMTYPE=1 -D I2S_SDPIN=10 -D I2S_CKPIN=11 -D I2S_WSPIN=12 -D MCLK_PIN=-1 ;; I2S mic lib_deps = ${esp32s3.lib_deps} https://github.com/mrfaptastic/ESP32-HUB75-MatrixPanel-DMA.git#aa28e2a ;; S3_LCD_DIV_NUM fix ;;board_build.partitions = ${esp32.large_partitions} ;; for 8MB flash board_build.partitions = ${esp32.extreme_partitions} ;; for 16MB flash board_build.f_flash = 80000000L board_build.flash_mode = qio monitor_filters = esp32_exception_decoder custom_usermods = audioreactive ================================================ FILE: readme.md ================================================

# Welcome to WLED! ✨ A fast and feature-rich implementation of an ESP32 and ESP8266 webserver to control NeoPixel (WS2812B, WS2811, SK6812) LEDs or also SPI based chipsets like the WS2801 and APA102! Originally created by [Aircoookie](https://github.com/Aircoookie) ## ⚙️ Features - WS2812FX library with more than 100 special effects - FastLED noise effects and 50 palettes - Modern UI with color, effect and segment controls - Segments to set different effects and colors to user defined parts of the LED string - Settings page - configuration via the network - Access Point and station mode - automatic failsafe AP - [Up to 10 LED outputs](https://kno.wled.ge/features/multi-strip/#esp32) per instance - Support for RGBW strips - Up to 250 user presets to save and load colors/effects easily, supports cycling through them. - Presets can be used to automatically execute API calls - Nightlight function (gradually dims down) - Full OTA software updateability (HTTP + ArduinoOTA), password protectable - Configurable analog clock (Cronixie, 7-segment and EleksTube IPS clock support via usermods) - Configurable Auto Brightness limit for safe operation - Filesystem-based config for easier backup of presets and settings ## 💡 Supported light control interfaces - WLED app for [Android](https://play.google.com/store/apps/details?id=ca.cgagnier.wlednativeandroid) and [iOS](https://apps.apple.com/gb/app/wled-native/id6446207239) - JSON and HTTP request APIs - MQTT - E1.31, Art-Net, DDP and TPM2.net - [diyHue](https://github.com/diyhue/diyHue) (Wled is supported by diyHue, including Hue Sync Entertainment under udp. Thanks to [Gregory Mallios](https://github.com/gmallios)) - [Hyperion](https://github.com/hyperion-project/hyperion.ng) - UDP realtime - Alexa voice control (including dimming and color) - Sync to Philips hue lights - Adalight (PC ambilight via serial) and TPM2 - Sync color of multiple WLED devices (UDP notifier) - Infrared remotes (24-key RGB, receiver required) - Simple timers/schedules (time from NTP, timezones/DST supported) ## 📲 Quick start guide and documentation See the [documentation on our official site](https://kno.wled.ge)! [On this page](https://kno.wled.ge/basics/tutorials/) you can find excellent tutorials and tools to help you get your new project up and running! ## 🖼️ User interface ## 💾 Compatible hardware See [here](https://kno.wled.ge/basics/compatible-hardware)! ## ✌️ Other Licensed under the EUPL v1.2 license Credits [here](https://kno.wled.ge/about/contributors/)! CORS proxy by [Corsfix](https://corsfix.com/) Join the Discord server to discuss everything about WLED! Check out the WLED [Discourse forum](https://wled.discourse.group)! You can also send me mails to [dev.aircoookie@gmail.com](mailto:dev.aircoookie@gmail.com), but please, only do so if you want to talk to me privately. If WLED really brightens up your day, you can [![](https://img.shields.io/badge/send%20me%20a%20small%20gift-paypal-blue.svg?style=flat-square)](https://paypal.me/aircoookie) *Disclaimer:* If you are prone to photosensitive epilepsy, we recommended you do **not** use this software. If you still want to try, don't use strobe, lighting or noise modes or high effect speed settings. As per the EUPL license, I assume no liability for any damage to you or any other person or equipment. ================================================ FILE: requirements.in ================================================ platformio>=6.1.17 ================================================ FILE: requirements.txt ================================================ # # This file is autogenerated by pip-compile with Python 3.11 # by the following command: # # pip-compile requirements.in # ajsonrpc==1.2.0 # via platformio anyio==4.8.0 # via starlette bottle==0.13.2 # via platformio certifi==2025.1.31 # via requests charset-normalizer==3.4.1 # via requests click==8.1.8 # via # platformio # uvicorn colorama==0.4.6 # via platformio h11==0.16.0 # via # uvicorn # wsproto idna==3.10 # via # anyio # requests marshmallow==3.26.1 # via platformio packaging==24.2 # via marshmallow platformio==6.1.17 # via -r requirements.in pyelftools==0.32 # via platformio pyserial==3.5 # via platformio requests==2.32.4 # via platformio semantic-version==2.10.0 # via platformio sniffio==1.3.1 # via anyio starlette==0.45.3 # via platformio tabulate==0.9.0 # via platformio typing-extensions==4.12.2 # via anyio urllib3==2.5.0 # via requests uvicorn==0.34.0 # via platformio wsproto==1.2.0 # via platformio ================================================ FILE: test/README ================================================ This directory is intended for PIO Unit Testing and project tests. Unit Testing is a software testing method by which individual units of source code, sets of one or more MCU program modules together with associated control data, usage procedures, and operating procedures, are tested to determine whether they are fit for use. Unit testing finds problems early in the development cycle. More information about PIO Unit Testing: - https://docs.platformio.org/page/plus/unit-testing.html ================================================ FILE: tools/WLED_ESP32-wrover_4MB.csv ================================================ # Name, Type, SubType, Offset, Size, Flags nvs, data, nvs, 0x9000, 0x5000, otadata, data, ota, 0xe000, 0x2000, app0, app, ota_0, 0x10000, 0x1A0000, app1, app, ota_1, 0x1B0000,0x1A0000, spiffs, data, spiffs, 0x350000,0xB0000, ================================================ FILE: tools/WLED_ESP32_16MB.csv ================================================ # Name, Type, SubType, Offset, Size, Flags nvs, data, nvs, 0x9000, 0x5000, otadata, data, ota, 0xe000, 0x2000, app0, app, ota_0, 0x10000, 0x200000, app1, app, ota_1, 0x210000,0x200000, spiffs, data, spiffs, 0x410000,0xBE0000, ================================================ FILE: tools/WLED_ESP32_16MB_9MB_FS.csv ================================================ # Name, Type, SubType, Offset, Size, Flags nvs, data, nvs, 0x9000, 0x5000, otadata, data, ota, 0xe000, 0x2000, app0, app, ota_0, 0x10000, 0x300000, app1, app, ota_1, 0x310000,0x300000, spiffs, data, spiffs, 0x610000,0x9E0000, coredump, data, coredump,,64K # to create/use ffat, see https://github.com/marcmerlin/esp32_fatfsimage ================================================ FILE: tools/WLED_ESP32_2MB_noOTA.csv ================================================ # Name, Type, SubType, Offset, Size, Flags nvs, data, nvs, 0x9000, 20K, otadata, data, ota, 0xe000, 8K, app0, app, ota_0, 0x10000, 1536K, spiffs, data, spiffs, 0x190000, 384K, ================================================ FILE: tools/WLED_ESP32_32MB.csv ================================================ # Name, Type, SubType, Offset, Size, Flags nvs, data, nvs, 0x9000, 0x5000, otadata, data, ota, 0xe000, 0x2000, app0, app, ota_0, 0x10000, 0x300000, app1, app, ota_1, 0x310000,0x300000, spiffs, data, spiffs, 0x610000,0x19E0000, coredump, data, coredump,,64K ================================================ FILE: tools/WLED_ESP32_4MB_1MB_FS.csv ================================================ # Name, Type, SubType, Offset, Size, Flags nvs, data, nvs, 0x9000, 0x5000, otadata, data, ota, 0xe000, 0x2000, app0, app, ota_0, 0x10000, 0x180000, app1, app, ota_1, 0x190000,0x180000, spiffs, data, spiffs, 0x310000,0xF0000, ================================================ FILE: tools/WLED_ESP32_4MB_256KB_FS.csv ================================================ # Name, Type, SubType, Offset, Size, Flags nvs, data, nvs, 0x9000, 0x5000, otadata, data, ota, 0xe000, 0x2000, app0, app, ota_0, 0x10000, 0x1D0000, app1, app, ota_1, 0x1E0000,0x1D0000, spiffs, data, spiffs, 0x3B0000,0x40000, coredump, data, coredump,,64K ================================================ FILE: tools/WLED_ESP32_4MB_512KB_FS.csv ================================================ # Name, Type, SubType, Offset, Size, Flags nvs, data, nvs, 0x9000, 0x5000, otadata, data, ota, 0xe000, 0x2000, app0, app, ota_0, 0x10000, 0x1B0000, app1, app, ota_1, 0x1C0000,0x1B0000, spiffs, data, spiffs, 0x370000,0x80000, coredump, data, coredump,,64K ================================================ FILE: tools/WLED_ESP32_4MB_700k_FS.csv ================================================ # Name, Type, SubType, Offset, Size, Flags nvs, data, nvs, 0x9000, 0x5000, otadata, data, ota, 0xe000, 0x2000, app0, app, ota_0, 0x10000, 0x1A0000, app1, app, ota_1, 0x1B0000,0x1A0000, spiffs, data, spiffs, 0x350000,0xB0000, ================================================ FILE: tools/WLED_ESP32_8MB.csv ================================================ # Name, Type, SubType, Offset, Size, Flags nvs, data, nvs, 0x9000, 0x5000, otadata, data, ota, 0xe000, 0x2000, app0, app, ota_0, 0x10000, 0x200000, app1, app, ota_1, 0x210000,0x200000, spiffs, data, spiffs, 0x410000,0x3E0000, coredump, data, coredump,,64K ================================================ FILE: tools/all_xml.sh ================================================ #!/bin/bash # Pull all settings pages for comparison HOST=$1 TGT_PATH=$2 CURL_ARGS="--compressed" # Replicate one target many times function replicate() { for i in {0..10} do echo -n " http://${HOST}/settings.js?p=$i -o ${TGT_PATH}/$i.xml" done } read -a TARGETS <<< $(replicate) mkdir -p ${TGT_PATH} curl ${CURL_ARGS} ${TARGETS[@]} ================================================ FILE: tools/cdata-test.js ================================================ 'use strict'; const assert = require('node:assert'); const { describe, it, before, after } = require('node:test'); const fs = require('fs'); const path = require('path'); const child_process = require('child_process'); const util = require('util'); const execPromise = util.promisify(child_process.exec); process.env.NODE_ENV = 'test'; // Set the environment to testing const cdata = require('./cdata.js'); describe('Function', () => { const testFolderPath = path.join(__dirname, 'testFolder'); const oldFilePath = path.join(testFolderPath, 'oldFile.txt'); const newFilePath = path.join(testFolderPath, 'newFile.txt'); // Create a temporary file before the test before(() => { // Create test folder if (!fs.existsSync(testFolderPath)) { fs.mkdirSync(testFolderPath); } // Create an old file fs.writeFileSync(oldFilePath, 'This is an old file.'); // Modify the 'mtime' to simulate an old file const oldTime = new Date(); oldTime.setFullYear(oldTime.getFullYear() - 1); fs.utimesSync(oldFilePath, oldTime, oldTime); // Create a new file fs.writeFileSync(newFilePath, 'This is a new file.'); }); // delete the temporary files after the test after(() => { fs.rmSync(testFolderPath, { recursive: true }); }); describe('isFileNewerThan', async () => { it('should return true if the file is newer than the provided time', async () => { const pastTime = Date.now() - 10000; // 10 seconds ago assert.strictEqual(cdata.isFileNewerThan(newFilePath, pastTime), true); }); it('should return false if the file is older than the provided time', async () => { assert.strictEqual(cdata.isFileNewerThan(oldFilePath, Date.now()), false); }); it('should throw an exception if the file does not exist', async () => { assert.throws(() => { cdata.isFileNewerThan('nonexistent.txt', Date.now()); }); }); }); describe('isAnyFileInFolderNewerThan', async () => { it('should return true if a file in the folder is newer than the given time', async () => { const time = fs.statSync(path.join(testFolderPath, 'oldFile.txt')).mtime; assert.strictEqual(cdata.isAnyFileInFolderNewerThan(testFolderPath, time), true); }); it('should return false if no files in the folder are newer than the given time', async () => { assert.strictEqual(cdata.isAnyFileInFolderNewerThan(testFolderPath, new Date()), false); }); it('should throw an exception if the folder does not exist', async () => { assert.throws(() => { cdata.isAnyFileInFolderNewerThan('nonexistent', new Date()); }); }); }); }); describe('Script', () => { const folderPath = 'wled00'; const dataPath = path.join(folderPath, 'data'); before(() => { process.env.NODE_ENV = 'production'; // Backup files fs.cpSync("wled00/data", "wled00Backup", { recursive: true }); fs.cpSync("tools/cdata.js", "cdata.bak.js"); fs.cpSync("package.json", "package.bak.json"); }); after(() => { // Restore backup fs.rmSync("wled00/data", { recursive: true }); fs.renameSync("wled00Backup", "wled00/data"); fs.rmSync("tools/cdata.js"); fs.renameSync("cdata.bak.js", "tools/cdata.js"); fs.rmSync("package.json"); fs.renameSync("package.bak.json", "package.json"); }); // delete all html_*.h files async function deleteBuiltFiles() { const files = await fs.promises.readdir(folderPath); await Promise.all(files.map(file => { if (file.startsWith('html_') && path.extname(file) === '.h') { return fs.promises.unlink(path.join(folderPath, file)); } })); } // check if html_*.h files were created async function checkIfBuiltFilesExist() { const files = await fs.promises.readdir(folderPath); const htmlFiles = files.filter(file => file.startsWith('html_') && path.extname(file) === '.h'); assert(htmlFiles.length > 0, 'html_*.h files were not created'); } async function runAndCheckIfBuiltFilesExist() { await execPromise('node tools/cdata.js'); await checkIfBuiltFilesExist(); } async function checkIfFileWasNewlyCreated(file) { const modifiedTime = fs.statSync(file).mtimeMs; assert(Date.now() - modifiedTime < 500, file + ' was not modified'); } async function testFileModification(sourceFilePath, resultFile) { // run cdata.js to ensure html_*.h files are created await execPromise('node tools/cdata.js'); // modify file fs.appendFileSync(sourceFilePath, ' '); // delay for 1 second to ensure the modified time is different await new Promise(resolve => setTimeout(resolve, 1000)); // run script cdata.js again and wait for it to finish await execPromise('node tools/cdata.js'); await checkIfFileWasNewlyCreated(path.join(folderPath, resultFile)); } describe('should build if', () => { it('html_*.h files are missing', async () => { await deleteBuiltFiles(); await runAndCheckIfBuiltFilesExist(); }); it('only one html_*.h file is missing', async () => { // run script cdata.js and wait for it to finish await execPromise('node tools/cdata.js'); // delete a random html_*.h file let files = await fs.promises.readdir(folderPath); let htmlFiles = files.filter(file => file.startsWith('html_') && path.extname(file) === '.h'); const randomFile = htmlFiles[Math.floor(Math.random() * htmlFiles.length)]; await fs.promises.unlink(path.join(folderPath, randomFile)); await runAndCheckIfBuiltFilesExist(); }); it('script was executed with -f or --force', async () => { await execPromise('node tools/cdata.js'); await new Promise(resolve => setTimeout(resolve, 1000)); await execPromise('node tools/cdata.js --force'); await checkIfFileWasNewlyCreated(path.join(folderPath, 'html_ui.h')); await new Promise(resolve => setTimeout(resolve, 1000)); await execPromise('node tools/cdata.js -f'); await checkIfFileWasNewlyCreated(path.join(folderPath, 'html_ui.h')); }); it('a file changes', async () => { await testFileModification(path.join(dataPath, 'index.htm'), 'html_ui.h'); }); it('a inlined file changes', async () => { await testFileModification(path.join(dataPath, 'index.js'), 'html_ui.h'); }); it('a settings file changes', async () => { await testFileModification(path.join(dataPath, 'settings_leds.htm'), 'html_ui.h'); }); it('the favicon changes', async () => { await testFileModification(path.join(dataPath, 'favicon.ico'), 'html_ui.h'); }); it('cdata.js changes', async () => { await testFileModification('tools/cdata.js', 'html_ui.h'); }); it('package.json changes', async () => { await testFileModification('package.json', 'html_ui.h'); }); }); describe('should not build if', () => { it('the files are already built', async () => { await deleteBuiltFiles(); // run script cdata.js and wait for it to finish let startTime = Date.now(); await execPromise('node tools/cdata.js'); const firstRunTime = Date.now() - startTime; // run script cdata.js and wait for it to finish startTime = Date.now(); await execPromise('node tools/cdata.js'); const secondRunTime = Date.now() - startTime; // check if second run was faster than the first (must be at least 2x faster) assert(secondRunTime < firstRunTime / 2, 'html_*.h files were rebuilt'); }); }); }); ================================================ FILE: tools/cdata.js ================================================ /** * Writes compressed C arrays of data files (web interface) * How to use it? * * 1) Install Node 20+ and npm * 2) npm install * 3) npm run build * * If you change data folder often, you can run it in monitoring mode (it will recompile and update *.h on every file change) * * > npm run dev * * How it works? * * It uses NodeJS packages to inline, minify and GZIP files. See writeHtmlGzipped and writeChunks invocations at the bottom of the page. */ const fs = require("node:fs"); const path = require("path"); const inline = require("web-resource-inliner"); const zlib = require("node:zlib"); const CleanCSS = require("clean-css"); const minifyHtml = require("html-minifier-terser").minify; const packageJson = require("../package.json"); // Export functions for testing module.exports = { isFileNewerThan, isAnyFileInFolderNewerThan }; const output = ["wled00/html_ui.h", "wled00/html_pixart.h", "wled00/html_cpal.h", "wled00/html_edit.h", "wled00/html_pxmagic.h", "wled00/html_pixelforge.h", "wled00/html_settings.h", "wled00/html_other.h", "wled00/js_iro.h", "wled00/js_omggif.h"] // \x1b[34m is blue, \x1b[36m is cyan, \x1b[0m is reset const wledBanner = ` \t\x1b[34m ## ## ## ###### ###### \t\x1b[34m## ## ## ## ## ## ## \t\x1b[34m## ## ## ## ###### ## ## \t\x1b[34m## ## ## ## ## ## ## \t\x1b[34m ## ## ###### ###### ###### \t\t\x1b[36m build script for web UI \x1b[0m`; // Generate build timestamp as UNIX timestamp (seconds since epoch) function generateBuildTime() { return Math.floor(Date.now() / 1000); } const singleHeader = `/* * Binary array for the Web UI. * gzip is used for smaller size and improved speeds. * * Please see https://kno.wled.ge/advanced/custom-features/#changing-web-ui * to find out how to easily modify the web UI source! */ // Automatically generated build time for cache busting (UNIX timestamp) #define WEB_BUILD_TIME ${generateBuildTime()} `; const multiHeader = `/* * More web UI HTML source arrays. * This file is auto generated, please don't make any changes manually. * * Instead, see https://kno.wled.ge/advanced/custom-features/#changing-web-ui * to find out how to easily modify the web UI source! */ `; function hexdump(buffer, isHex = false) { let lines = []; for (let i = 0; i < buffer.length; i += (isHex ? 32 : 16)) { var block; let hexArray = []; if (isHex) { block = buffer.slice(i, i + 32) for (let j = 0; j < block.length; j += 2) { hexArray.push("0x" + block.slice(j, j + 2)) } } else { block = buffer.slice(i, i + 16); // cut buffer into blocks of 16 for (let value of block) { hexArray.push("0x" + value.toString(16).padStart(2, "0")); } } let hexString = hexArray.join(", "); let line = ` ${hexString}`; lines.push(line); } return lines.join(",\n"); } function adoptVersionAndRepo(html) { let repoUrl = packageJson.repository ? packageJson.repository.url : undefined; if (repoUrl) { repoUrl = repoUrl.replace(/^git\+/, ""); repoUrl = repoUrl.replace(/\.git$/, ""); html = html.replaceAll("https://github.com/atuline/WLED", repoUrl); html = html.replaceAll("https://github.com/wled-dev/WLED", repoUrl); } let version = packageJson.version; if (version) { html = html.replaceAll("##VERSION##", version); } return html; } async function minify(str, type = "plain") { const options = { collapseWhitespace: true, conservativeCollapse: true, // preserve spaces in text collapseBooleanAttributes: true, collapseInlineTagWhitespace: true, minifyCSS: true, minifyJS: true, removeAttributeQuotes: true, removeComments: true, sortAttributes: true, sortClassName: true, }; if (type == "plain") { return str; } else if (type == "css-minify") { return new CleanCSS({}).minify(str).styles; } else if (type == "js-minify") { let js = await minifyHtml('', options); return js.replace(/<[\/]*script>/g, ''); } else if (type == "html-minify") { return await minifyHtml(str, options); } throw new Error("Unknown filter: " + type); } async function writeHtmlGzipped(sourceFile, resultFile, page, inlineCss = true) { console.info("Reading " + sourceFile); inline.html({ fileContent: fs.readFileSync(sourceFile, "utf8"), relativeTo: path.dirname(sourceFile), strict: inlineCss, // when not inlining css, ignore errors (enables linking style.css from subfolder htm files) stylesheets: inlineCss // when true (default), css is inlined }, async function (error, html) { if (error) throw error; html = adoptVersionAndRepo(html); const originalLength = html.length; html = await minify(html, "html-minify"); const result = zlib.gzipSync(html, { level: zlib.constants.Z_BEST_COMPRESSION }); console.info("Minified and compressed " + sourceFile + " from " + originalLength + " to " + result.length + " bytes"); const array = hexdump(result); let src = singleHeader; src += `const uint16_t PAGE_${page}_length = ${result.length};\n`; src += `const uint8_t PAGE_${page}[] PROGMEM = {\n${array}\n};\n\n`; console.info("Writing " + resultFile); fs.writeFileSync(resultFile, src); }); } async function specToChunk(srcDir, s) { const buf = fs.readFileSync(srcDir + "/" + s.file); let chunk = `\n// Autogenerated from ${srcDir}/${s.file}, do not edit!!\n` if (s.method == "plaintext" || s.method == "gzip") { let str = buf.toString("utf-8"); str = adoptVersionAndRepo(str); const originalLength = str.length; if (s.method == "gzip") { if (s.mangle) str = s.mangle(str); const zip = zlib.gzipSync(await minify(str, s.filter), { level: zlib.constants.Z_BEST_COMPRESSION }); console.info("Minified and compressed " + s.file + " from " + originalLength + " to " + zip.length + " bytes"); const result = hexdump(zip); chunk += `const uint16_t ${s.name}_length = ${zip.length};\n`; chunk += `const uint8_t ${s.name}[] PROGMEM = {\n${result}\n};\n\n`; return chunk; } else { const minified = await minify(str, s.filter); console.info("Minified " + s.file + " from " + originalLength + " to " + minified.length + " bytes"); chunk += `const char ${s.name}[] PROGMEM = R"${s.prepend || ""}${minified}${s.append || ""}";\n\n`; return s.mangle ? s.mangle(chunk) : chunk; } } else if (s.method == "binary") { const result = hexdump(buf); chunk += `const uint16_t ${s.name}_length = ${buf.length};\n`; chunk += `const uint8_t ${s.name}[] PROGMEM = {\n${result}\n};\n\n`; return chunk; } throw new Error("Unknown method: " + s.method); } async function writeChunks(srcDir, specs, resultFile) { let src = multiHeader; for (const s of specs) { console.info("Reading " + srcDir + "/" + s.file + " as " + s.name); src += await specToChunk(srcDir, s); } console.info("Writing " + src.length + " characters into " + resultFile); fs.writeFileSync(resultFile, src); } // Check if a file is newer than a given time function isFileNewerThan(filePath, time) { const stats = fs.statSync(filePath); return stats.mtimeMs > time; } // Check if any file in a folder (or its subfolders) is newer than a given time function isAnyFileInFolderNewerThan(folderPath, time) { const files = fs.readdirSync(folderPath, { withFileTypes: true }); for (const file of files) { const filePath = path.join(folderPath, file.name); if (isFileNewerThan(filePath, time)) { return true; } if (file.isDirectory() && isAnyFileInFolderNewerThan(filePath, time)) { return true; } } return false; } // Check if the web UI is already built function isAlreadyBuilt(webUIPath, packageJsonPath = "package.json") { let lastBuildTime = Infinity; for (const file of output) { try { lastBuildTime = Math.min(lastBuildTime, fs.statSync(file).mtimeMs); } catch (e) { if (e.code !== 'ENOENT') throw e; console.info("File " + file + " does not exist. Rebuilding..."); return false; } } return !isAnyFileInFolderNewerThan(webUIPath, lastBuildTime) && !isFileNewerThan(packageJsonPath, lastBuildTime) && !isFileNewerThan(__filename, lastBuildTime); } // Don't run this script if we're in a test environment if (process.env.NODE_ENV === 'test') { return; } console.info(wledBanner); if (isAlreadyBuilt("wled00/data") && process.argv[2] !== '--force' && process.argv[2] !== '-f') { console.info("Web UI is already built"); return; } writeHtmlGzipped("wled00/data/index.htm", "wled00/html_ui.h", 'index'); writeHtmlGzipped("wled00/data/pixart/pixart.htm", "wled00/html_pixart.h", 'pixart'); writeHtmlGzipped("wled00/data/pxmagic/pxmagic.htm", "wled00/html_pxmagic.h", 'pxmagic'); writeHtmlGzipped("wled00/data/pixelforge/pixelforge.htm", "wled00/html_pixelforge.h", 'pixelforge', false); // do not inline css //writeHtmlGzipped("wled00/data/edit.htm", "wled00/html_edit.h", 'edit'); writeChunks( "wled00/data/", [ { file: "iro.js", name: "JS_iro", method: "gzip", filter: "plain", // no minification, it is already minified mangle: (s) => s.replace(/^\/\*![\s\S]*?\*\//, '') // remove license comment at the top } ], "wled00/js_iro.h" ); writeChunks( "wled00/data/pixelforge", [ { file: "omggif.js", name: "JS_omggif", method: "gzip", filter: "js-minify", mangle: (s) => s.replace(/^\/\*![\s\S]*?\*\//, '') // remove license comment at the top } ], "wled00/js_omggif.h" ); writeChunks( "wled00/data", [ { file: "edit.htm", name: "PAGE_edit", method: "gzip", filter: "html-minify" } ], "wled00/html_edit.h" ); writeChunks( "wled00/data/cpal", [ { file: "cpal.htm", name: "PAGE_cpal", method: "gzip", filter: "html-minify" } ], "wled00/html_cpal.h" ); writeChunks( "wled00/data", [ { file: "style.css", name: "PAGE_settingsCss", method: "gzip", filter: "css-minify", mangle: (str) => str .replace("%%", "%") }, { file: "common.js", name: "JS_common", method: "gzip", filter: "js-minify", }, { file: "settings.htm", name: "PAGE_settings", method: "gzip", filter: "html-minify", }, { file: "settings_wifi.htm", name: "PAGE_settings_wifi", method: "gzip", filter: "html-minify", }, { file: "settings_leds.htm", name: "PAGE_settings_leds", method: "gzip", filter: "html-minify", }, { file: "settings_dmx.htm", name: "PAGE_settings_dmx", method: "gzip", filter: "html-minify", }, { file: "settings_ui.htm", name: "PAGE_settings_ui", method: "gzip", filter: "html-minify", }, { file: "settings_sync.htm", name: "PAGE_settings_sync", method: "gzip", filter: "html-minify", }, { file: "settings_time.htm", name: "PAGE_settings_time", method: "gzip", filter: "html-minify", }, { file: "settings_sec.htm", name: "PAGE_settings_sec", method: "gzip", filter: "html-minify", }, { file: "settings_um.htm", name: "PAGE_settings_um", method: "gzip", filter: "html-minify", }, { file: "settings_2D.htm", name: "PAGE_settings_2D", method: "gzip", filter: "html-minify", }, { file: "settings_pin.htm", name: "PAGE_settings_pin", method: "gzip", filter: "html-minify" }, { file: "settings_pininfo.htm", name: "PAGE_settings_pininfo", method: "gzip", filter: "html-minify" } ], "wled00/html_settings.h" ); writeChunks( "wled00/data", [ { file: "usermod.htm", name: "PAGE_usermod", method: "gzip", filter: "html-minify", mangle: (str) => str.replace(/fetch\("http\:\/\/.*\/win/gms, 'fetch("/win'), }, { file: "msg.htm", name: "PAGE_msg", prepend: "=====(", append: ")=====", method: "plaintext", filter: "html-minify", mangle: (str) => str.replace(/\.*\<\/body\>/gms, "

%MSG%"), }, { file: "dmxmap.htm", name: "PAGE_dmxmap", prepend: "=====(", append: ")=====", method: "plaintext", filter: "html-minify", mangle: (str) => ` #ifdef WLED_ENABLE_DMX ${str.replace(/function FM\(\)[ ]?\{/gms, "function FM() {%DMXVARS%\n")} #else const char PAGE_dmxmap[] PROGMEM = R"=====()====="; #endif `, }, { file: "update.htm", name: "PAGE_update", method: "gzip", filter: "html-minify", }, { file: "welcome.htm", name: "PAGE_welcome", method: "gzip", filter: "html-minify", }, { file: "liveview.htm", name: "PAGE_liveview", method: "gzip", filter: "html-minify", }, { file: "liveviewws2D.htm", name: "PAGE_liveviewws2D", method: "gzip", filter: "html-minify", }, { file: "404.htm", name: "PAGE_404", method: "gzip", filter: "html-minify", }, { file: "favicon.ico", name: "favicon", method: "binary", } ], "wled00/html_other.h" ); ================================================ FILE: tools/dynarray_espressif32.ld ================================================ /* ESP32 linker script fragment to add dynamic array section to binary */ SECTIONS { .dynarray : { . = ALIGN(0x10); KEEP(*(SORT_BY_INIT_PRIORITY(.dynarray.*))) } > default_rodata_seg } INSERT AFTER .flash.rodata; ================================================ FILE: tools/fps_test.htm ================================================ WLED frame rate test tool

Starship monitoring dashboard

(or rather just a WLED frame rate tester lol)

IP:
Time per effect: s
Effects to test:
Extra JSON:

LEDs: -, Seg: -, Bri: -
FPS min: -, max: -, avg: -


================================================ FILE: tools/json_test.htm ================================================ JSON client

JSON API test tool

URL:

Body:

Response:

================================================ FILE: tools/multi-update.cmd ================================================ @echo off SETLOCAL SET FWPATH=c:\path\to\your\WLED\build_output\firmware GOTO ESPS :UPDATEONE IF NOT EXIST %FWPATH%\%2 GOTO SKIP ping -w 1000 -n 1 %1 | find "TTL=" || GOTO SKIP ECHO Updating %1 curl -s -F "update=@%FWPATH%/%2" %1/update >nul :SKIP GOTO:EOF :ESPS call :UPDATEONE 192.168.x.x firmware.bin call :UPDATEONE .... ================================================ FILE: tools/multi-update.sh ================================================ #!/bin/bash FWPATH=/path/to/your/WLED/build_output/firmware update_one() { if [ -f $FWPATH/$2 ]; then ping -c 1 $1 >/dev/null PINGRESULT=$? if [ $PINGRESULT -eq 0 ]; then echo Updating $1 curl -s -F "update=@${FWPATH}/$2" $1/update >/dev/null return 0 fi return 1 fi } update_one 192.168.x.x firmware.bin update_one 192.168.x.x firmware.bin # ... ================================================ FILE: tools/partitions-16MB_spiffs-tinyuf2.csv ================================================ # ESP-IDF Partition Table # Name, Type, SubType, Offset, Size, Flags # bootloader.bin,, 0x1000, 32K # partition table,, 0x8000, 4K nvs, data, nvs, 0x9000, 20K, otadata, data, ota, 0xe000, 8K, ota_0, app, ota_0, 0x10000, 2048K, ota_1, app, ota_1, 0x210000, 2048K, uf2, app, factory,0x410000, 256K, spiffs, data, spiffs, 0x450000, 11968K, ================================================ FILE: tools/partitions-4MB_spiffs-tinyuf2.csv ================================================ # ESP-IDF Partition Table # Name, Type, SubType, Offset, Size, Flags # bootloader.bin,, 0x1000, 32K # partition table, 0x8000, 4K nvs, data, nvs, 0x9000, 20K, otadata, data, ota, 0xe000, 8K, ota_0, 0, ota_0, 0x10000, 1408K, ota_1, 0, ota_1, 0x170000, 1408K, uf2, app, factory,0x2d0000, 256K, spiffs, data, spiffs, 0x310000, 960K, ================================================ FILE: tools/partitions-8MB_spiffs-tinyuf2.csv ================================================ # ESP-IDF Partition Table # Name, Type, SubType, Offset, Size, Flags # bootloader.bin,, 0x1000, 32K # partition table,, 0x8000, 4K nvs, data, nvs, 0x9000, 20K, otadata, data, ota, 0xe000, 8K, ota_0, app, ota_0, 0x10000, 2048K, ota_1, app, ota_1, 0x210000, 2048K, uf2, app, factory,0x410000, 256K, spiffs, data, spiffs, 0x450000, 3776K, ================================================ FILE: tools/stress_test.sh ================================================ #!/bin/bash # Some web server stress tests # # Perform a large number of parallel requests, stress testing the web server # TODO: some kind of performance metrics # Accepts three command line arguments: # - first argument - mandatory - IP or hostname of target server # - second argument - target type (optional) # - third argument - xfer count (for replicated targets) (optional) HOST=$1 declare -n TARGET_STR="${2:-JSON_LARGER}_TARGETS" REPLICATE_COUNT=$(("${3:-10}")) PARALLEL_MAX=${PARALLEL_MAX:-50} CURL_ARGS="--compressed --parallel --parallel-immediate --parallel-max ${PARALLEL_MAX}" CURL_PRINT_RESPONSE_ARGS="-w %{http_code}\n" JSON_TARGETS=('json/state' 'json/info' 'json/si', 'json/palettes' 'json/fxdata' 'settings/s.js?p=2') FILE_TARGETS=('' 'iro.js' 'rangetouch.js' 'settings' 'settings/wifi') # Replicate one target many times function replicate() { printf "${1}?%d " $(seq 1 ${REPLICATE_COUNT}) } read -a JSON_TINY_TARGETS <<< $(replicate "json/nodes") read -a JSON_SMALL_TARGETS <<< $(replicate "json/info") read -a JSON_LARGE_TARGETS <<< $(replicate "json/si") read -a JSON_LARGER_TARGETS <<< $(replicate "json/fxdata") read -a INDEX_TARGETS <<< $(replicate "") # Expand target URLS to full arguments for curl TARGETS=(${TARGET_STR[@]}) #echo "${TARGETS[@]}" FULL_TGT_OPTIONS=$(printf "http://${HOST}/%s -o /dev/null " "${TARGETS[@]}") #echo ${FULL_TGT_OPTIONS} time curl ${CURL_ARGS} ${FULL_TGT_OPTIONS} ================================================ FILE: tools/udp_test.py ================================================ import numpy as np import socket class WledRealtimeClient: def __init__(self, wled_controller_ip, num_pixels, udp_port=21324, max_pixels_per_packet=126): self.wled_controller_ip = wled_controller_ip self.num_pixels = num_pixels self.udp_port = udp_port self.max_pixels_per_packet = max_pixels_per_packet self._sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) self._prev_pixels = np.full((3, self.num_pixels), 253, dtype=np.uint8) self.pixels = np.full((3, self.num_pixels), 1, dtype=np.uint8) def update(self): # Truncate values and cast to integer self.pixels = np.clip(self.pixels, 0, 255).astype(np.uint8) p = np.copy(self.pixels) idx = np.where(~np.all(p == self._prev_pixels, axis=0))[0] num_pixels = len(idx) n_packets = (num_pixels + self.max_pixels_per_packet - 1) // self.max_pixels_per_packet idx_split = np.array_split(idx, n_packets) header = bytes([1, 2]) # WARLS protocol header for packet_indices in idx_split: data = bytearray(header) for i in packet_indices: data.extend([i, *p[:, i]]) # Index and RGB values self._sock.sendto(bytes(data), (self.wled_controller_ip, self.udp_port)) self._prev_pixels = np.copy(p) ################################## LED blink test ################################## if __name__ == "__main__": WLED_CONTROLLER_IP = "192.168.1.153" NUM_PIXELS = 255 # Amount of LEDs on your strip import time wled = WledRealtimeClient(WLED_CONTROLLER_IP, NUM_PIXELS) print('Starting LED blink test') while True: for i in range(NUM_PIXELS): wled.pixels[1, i] = 255 if wled.pixels[1, i] == 0 else 0 wled.update() time.sleep(.01) ================================================ FILE: tools/wled-tools ================================================ #!/bin/bash # WLED Tools # A utility for managing WLED devices in a local network # https://github.com/wled/WLED # Color Definitions GREEN="\e[32m" RED="\e[31m" BLUE="\e[34m" YELLOW="\e[33m" RESET="\e[0m" # Logging function log() { local category="$1" local color="$2" local text="$3" if [ "$quiet" = true ]; then return fi if [ -t 1 ]; then # Check if output is a terminal echo -e "${color}[${category}]${RESET} ${text}" else echo "[${category}] ${text}" fi } # Fetch a URL to a destination file, validating status codes. # Usage: fetch "" "" "200 404" fetch() { local url="$1" local dest="$2" local accepted="${3:-200}" # If no dest given, just discard body local out if [ -n "$dest" ]; then # Write to ".tmp" files first, then move when success, to ensure we don't write partial files out="${dest}.tmp" else out="/dev/null" fi response=$(curl --connect-timeout 5 --max-time 30 -s -w "%{http_code}" -o "$out" "$url") local curl_exit_code=$? if [ $curl_exit_code -ne 0 ]; then [ -n "$dest" ] && rm -f "$out" log "ERROR" "$RED" "Connection error during request to $url (curl exit code: $curl_exit_code)." return 1 fi for code in $accepted; do if [ "$response" = "$code" ]; then # Accepted; only persist body for 2xx responses if [ -n "$dest" ]; then if [[ "$response" =~ ^2 ]]; then mv "$out" "$dest" else rm -f "$out" fi fi return 0 fi done # not accepted [ -n "$dest" ] && rm -f "$out" log "ERROR" "$RED" "Unexpected response from $url (HTTP $response)." return 2 } # POST a file to a URL, validating status codes. # Usage: post_file "" "" "200" post_file() { local url="$1" local file="$2" local accepted="${3:-200}" response=$(curl --connect-timeout 5 --max-time 300 -s -w "%{http_code}" -o /dev/null -X POST -F "file=@$file" "$url") local curl_exit_code=$? if [ $curl_exit_code -ne 0 ]; then log "ERROR" "$RED" "Connection error during POST to $url (curl exit code: $curl_exit_code)." return 1 fi for code in $accepted; do if [ "$response" -eq "$code" ]; then return 0 fi done log "ERROR" "$RED" "Unexpected response from $url (HTTP $response)." return 2 } # Print help message show_help() { cat << EOF Usage: wled-tools.sh [OPTIONS] COMMAND [ARGS...] Options: -h, --help Show this help message and exit. -t, --target Specify a single WLED device by IP address or hostname. -D, --discover Discover multiple WLED devices using mDNS. -d, --directory Specify a directory for saving backups (default: working directory). -f, --firmware Specify the firmware file for updating devices. -q, --quiet Suppress logging output (also makes discover output hostnames only). Commands: backup Backup the current state of a WLED device or multiple discovered devices. update Update the firmware of a WLED device or multiple discovered devices. discover Discover WLED devices using mDNS and list their IP addresses and names. Examples: # Discover all WLED devices on the network ./wled-tools discover # Backup a specific WLED device ./wled-tools -t 192.168.1.100 backup # Backup all discovered WLED devices to a specific directory ./wled-tools -D -d /path/to/backups backup # Update firmware on all discovered WLED devices ./wled-tools -D -f /path/to/firmware.bin update EOF } # Discover devices using mDNS discover_devices() { if ! command -v avahi-browse &> /dev/null; then log "ERROR" "$RED" "'avahi-browse' is required but not installed, please install avahi-utils using your preferred package manager." exit 1 fi # Map avahi responses to strings seperated by 0x1F (unit separator) mapfile -t raw_devices < <(avahi-browse _wled._tcp --terminate -r -p | awk -F';' '/^=/ {print $7"\x1F"$8"\x1F"$9}') local devices_array=() for device in "${raw_devices[@]}"; do IFS=$'\x1F' read -r hostname address port <<< "$device" devices_array+=("$hostname" "$address" "$port") done echo "${devices_array[@]}" } # Backup one device backup_one() { local hostname="$1" local address="$2" local port="$3" log "INFO" "$YELLOW" "Backing up device config/presets/ir: $hostname ($address:$port)" mkdir -p "$backup_dir" local file_prefix="${backup_dir}/${hostname}" if ! fetch "http://$address:$port/cfg.json" "${file_prefix}.cfg.json"; then log "ERROR" "$RED" "Failed to backup configuration for $hostname" return 1 fi if ! fetch "http://$address:$port/presets.json" "${file_prefix}.presets.json"; then log "ERROR" "$RED" "Failed to backup presets for $hostname" return 1 fi # ir.json is optional if ! fetch "http://$address:$port/ir.json" "${file_prefix}.ir.json" "200 404"; then log "ERROR" "$RED" "Failed to backup ir configs for $hostname" fi log "INFO" "$GREEN" "Successfully backed up config and presets for $hostname" return 0 } # Update one device update_one() { local hostname="$1" local address="$2" local port="$3" local firmware="$4" log "INFO" "$YELLOW" "Starting firmware update for device: $hostname ($address:$port)" local url="http://$address:$port/update" if ! post_file "$url" "$firmware" "200"; then log "ERROR" "$RED" "Failed to update firmware for $hostname" return 1 fi log "INFO" "$GREEN" "Successfully initiated firmware update for $hostname" return 0 } # Command-line arguments processing command="" target="" discover=false quiet=false backup_dir="./" firmware_file="" if [ $# -eq 0 ]; then show_help exit 0 fi while [[ $# -gt 0 ]]; do case "$1" in -h|--help) show_help exit 0 ;; -t|--target) if [ -z "$2" ] || [[ "$2" == -* ]]; then log "ERROR" "$RED" "The --target option requires an argument." exit 1 fi target="$2" shift 2 ;; -D|--discover) discover=true shift ;; -d|--directory) if [ -z "$2" ] || [[ "$2" == -* ]]; then log "ERROR" "$RED" "The --directory option requires an argument." exit 1 fi backup_dir="$2" shift 2 ;; -f|--firmware) if [ -z "$2" ] || [[ "$2" == -* ]]; then log "ERROR" "$RED" "The --firmware option requires an argument." exit 1 fi firmware_file="$2" shift 2 ;; -q|--quiet) quiet=true shift ;; backup|update|discover) command="$1" shift ;; *) log "ERROR" "$RED" "Unknown argument: $1" exit 1 ;; esac done # Execute the appropriate command case "$command" in discover) read -ra devices <<< "$(discover_devices)" for ((i=0; i<${#devices[@]}; i+=3)); do hostname="${devices[$i]}" address="${devices[$i+1]}" port="${devices[$i+2]}" if [ "$quiet" = true ]; then echo "$hostname" else log "INFO" "$BLUE" "Discovered device: Hostname=$hostname, Address=$address, Port=$port" fi done ;; backup) if [ -n "$target" ]; then # Assume target is both the hostname and address, with port 80 backup_one "$target" "$target" "80" elif [ "$discover" = true ]; then read -ra devices <<< "$(discover_devices)" for ((i=0; i<${#devices[@]}; i+=3)); do hostname="${devices[$i]}" address="${devices[$i+1]}" port="${devices[$i+2]}" backup_one "$hostname" "$address" "$port" done else log "ERROR" "$RED" "No target specified. Use --target or --discover." exit 1 fi ;; update) # Validate firmware before proceeding if [ -z "$firmware_file" ] || [ ! -f "$firmware_file" ]; then log "ERROR" "$RED" "Please provide a file in --firmware that exists" exit 1 fi if [ -n "$target" ]; then # Assume target is both the hostname and address, with port 80 update_one "$target" "$target" "80" "$firmware_file" elif [ "$discover" = true ]; then read -ra devices <<< "$(discover_devices)" for ((i=0; i<${#devices[@]}; i+=3)); do hostname="${devices[$i]}" address="${devices[$i+1]}" port="${devices[$i+2]}" update_one "$hostname" "$address" "$port" "$firmware_file" done else log "ERROR" "$RED" "No target specified. Use --target or --discover." exit 1 fi ;; *) show_help exit 1 ;; esac ================================================ FILE: usermods/ADS1115_v2/ADS1115_v2.cpp ================================================ #include "wled.h" #include #include #include "ChannelSettings.h" using namespace ADS1115; class ADS1115Usermod : public Usermod { public: void setup() { ads.setGain(GAIN_ONE); // 1x gain +/- 4.096V if (!ads.begin()) { Serial.println("Failed to initialize ADS"); return; } if (!initChannel()) { isInitialized = true; return; } startReading(); isEnabled = true; isInitialized = true; } void loop() { if (isEnabled && millis() - lastTime > loopInterval) { lastTime = millis(); // If we don't have new data, skip this iteration. if (!ads.conversionComplete()) { return; } updateResult(); moveToNextChannel(); startReading(); } } void addToJsonInfo(JsonObject& root) { if (!isEnabled) { return; } JsonObject user = root[F("u")]; if (user.isNull()) user = root.createNestedObject(F("u")); for (uint8_t i = 0; i < channelsCount; i++) { ChannelSettings* settingsPtr = &(channelSettings[i]); if (!settingsPtr->isEnabled) { continue; } JsonArray lightArr = user.createNestedArray(settingsPtr->name); //name float value = round((readings[i] + settingsPtr->offset) * settingsPtr->multiplier, settingsPtr->decimals); lightArr.add(value); //value lightArr.add(" " + settingsPtr->units); //unit } } void addToConfig(JsonObject& root) { JsonObject top = root.createNestedObject(F("ADC ADS1115")); for (uint8_t i = 0; i < channelsCount; i++) { ChannelSettings* settingsPtr = &(channelSettings[i]); JsonObject channel = top.createNestedObject(settingsPtr->settingName); channel[F("Enabled")] = settingsPtr->isEnabled; channel[F("Name")] = settingsPtr->name; channel[F("Units")] = settingsPtr->units; channel[F("Multiplier")] = settingsPtr->multiplier; channel[F("Offset")] = settingsPtr->offset; channel[F("Decimals")] = settingsPtr->decimals; } top[F("Loop Interval")] = loopInterval; } bool readFromConfig(JsonObject& root) { JsonObject top = root[F("ADC ADS1115")]; bool configComplete = !top.isNull(); bool hasEnabledChannels = false; for (uint8_t i = 0; i < channelsCount && configComplete; i++) { ChannelSettings* settingsPtr = &(channelSettings[i]); JsonObject channel = top[settingsPtr->settingName]; configComplete &= !channel.isNull(); configComplete &= getJsonValue(channel[F("Enabled")], settingsPtr->isEnabled); configComplete &= getJsonValue(channel[F("Name")], settingsPtr->name); configComplete &= getJsonValue(channel[F("Units")], settingsPtr->units); configComplete &= getJsonValue(channel[F("Multiplier")], settingsPtr->multiplier); configComplete &= getJsonValue(channel[F("Offset")], settingsPtr->offset); configComplete &= getJsonValue(channel[F("Decimals")], settingsPtr->decimals); hasEnabledChannels |= settingsPtr->isEnabled; } configComplete &= getJsonValue(top[F("Loop Interval")], loopInterval); isEnabled = isInitialized && configComplete && hasEnabledChannels; return configComplete; } uint16_t getId() { return USERMOD_ID_ADS1115; } private: static const uint8_t channelsCount = 8; ChannelSettings channelSettings[channelsCount] = { { "Differential reading from AIN0 (P) and AIN1 (N)", false, "Differential AIN0 AIN1", "V", ADS1X15_REG_CONFIG_MUX_DIFF_0_1, 1, 0, 3 }, { "Differential reading from AIN0 (P) and AIN3 (N)", false, "Differential AIN0 AIN3", "V", ADS1X15_REG_CONFIG_MUX_DIFF_0_3, 1, 0, 3 }, { "Differential reading from AIN1 (P) and AIN3 (N)", false, "Differential AIN1 AIN3", "V", ADS1X15_REG_CONFIG_MUX_DIFF_1_3, 1, 0, 3 }, { "Differential reading from AIN2 (P) and AIN3 (N)", false, "Differential AIN2 AIN3", "V", ADS1X15_REG_CONFIG_MUX_DIFF_2_3, 1, 0, 3 }, { "Single-ended reading from AIN0", false, "Single-ended AIN0", "V", ADS1X15_REG_CONFIG_MUX_SINGLE_0, 1, 0, 3 }, { "Single-ended reading from AIN1", false, "Single-ended AIN1", "V", ADS1X15_REG_CONFIG_MUX_SINGLE_1, 1, 0, 3 }, { "Single-ended reading from AIN2", false, "Single-ended AIN2", "V", ADS1X15_REG_CONFIG_MUX_SINGLE_2, 1, 0, 3 }, { "Single-ended reading from AIN3", false, "Single-ended AIN3", "V", ADS1X15_REG_CONFIG_MUX_SINGLE_3, 1, 0, 3 }, }; float readings[channelsCount] = {0, 0, 0, 0, 0, 0, 0, 0}; unsigned long loopInterval = 1000; unsigned long lastTime = 0; Adafruit_ADS1115 ads; uint8_t activeChannel; bool isEnabled = false; bool isInitialized = false; static float round(float value, uint8_t decimals) { return roundf(value * powf(10, decimals)) / powf(10, decimals); } bool initChannel() { for (uint8_t i = 0; i < channelsCount; i++) { if (channelSettings[i].isEnabled) { activeChannel = i; return true; } } activeChannel = 0; return false; } void moveToNextChannel() { uint8_t oldActiveChannel = activeChannel; do { if (++activeChannel >= channelsCount){ activeChannel = 0; } } while (!channelSettings[activeChannel].isEnabled && oldActiveChannel != activeChannel); } void startReading() { ads.startADCReading(channelSettings[activeChannel].mux, /*continuous=*/false); } void updateResult() { int16_t results = ads.getLastConversionResults(); readings[activeChannel] = ads.computeVolts(results); } }; static ADS1115Usermod ads1115_v2; REGISTER_USERMOD(ads1115_v2); ================================================ FILE: usermods/ADS1115_v2/ChannelSettings.h ================================================ #include "wled.h" namespace ADS1115 { struct ChannelSettings { const String settingName; bool isEnabled; String name; String units; const uint16_t mux; float multiplier; float offset; uint8_t decimals; }; } ================================================ FILE: usermods/ADS1115_v2/library.json ================================================ { "name": "ADS1115_v2", "build": { "libArchive": false }, "dependencies": { "Adafruit BusIO": "https://github.com/adafruit/Adafruit_BusIO#1.13.2", "Adafruit ADS1X15": "https://github.com/adafruit/Adafruit_ADS1X15#2.4.0" } } ================================================ FILE: usermods/ADS1115_v2/readme.md ================================================ # ADS1115 16-Bit ADC with four inputs This usermod will read from an ADS1115 ADC. The voltages are displayed in the Info section of the web UI. Configuration is performed via the Usermod menu. There are no parameters to set in code! ## Installation Add 'ADS1115' to `custom_usermods` in your platformio environment. ================================================ FILE: usermods/AHT10_v2/AHT10_v2.cpp ================================================ #include "wled.h" #include #define AHT10_SUCCESS 1 class UsermodAHT10 : public Usermod { private: static const char _name[]; unsigned long _lastLoopCheck = 0; bool _settingEnabled : 1; // Enable the usermod bool _mqttPublish : 1; // Publish mqtt values bool _mqttPublishAlways : 1; // Publish always, regardless if there is a change bool _mqttHomeAssistant : 1; // Enable Home Assistant docs bool _initDone : 1; // Initialization is done // Settings. Some of these are stored in a different format than they're user settings - so we don't have to convert at runtime uint8_t _i2cAddress = AHT10_ADDRESS_0X38; ASAIR_I2C_SENSOR _ahtType = AHT10_SENSOR; uint16_t _checkInterval = 60000; // milliseconds, user settings is in seconds float _decimalFactor = 100; // a power of 10 factor. 1 would be no change, 10 is one decimal, 100 is two etc. User sees a power of 10 (0, 1, 2, ..) uint8_t _lastStatus = 0; float _lastHumidity = 0; float _lastTemperature = 0; #ifndef WLED_MQTT_DISABLE float _lastHumiditySent = 0; float _lastTemperatureSent = 0; #endif AHT10 *_aht = nullptr; float truncateDecimals(float val) { return roundf(val * _decimalFactor) / _decimalFactor; } void initializeAht() { if (_aht != nullptr) { delete _aht; } _aht = new AHT10(_i2cAddress, _ahtType); _lastStatus = 0; _lastHumidity = 0; _lastTemperature = 0; } #ifndef WLED_DISABLE_MQTT void mqttInitialize() { // This is a generic "setup mqtt" function, So we must abort if we're not to do mqtt if (!WLED_MQTT_CONNECTED || !_mqttPublish || !_mqttHomeAssistant) return; char topic[128]; snprintf_P(topic, 127, "%s/temperature", mqttDeviceTopic); mqttCreateHassSensor(F("Temperature"), topic, F("temperature"), F("°C")); snprintf_P(topic, 127, "%s/humidity", mqttDeviceTopic); mqttCreateHassSensor(F("Humidity"), topic, F("humidity"), F("%")); } void mqttPublishIfChanged(const __FlashStringHelper *topic, float &lastState, float state, float minChange) { // Check if MQTT Connected, otherwise it will crash the 8266 // Only report if the change is larger than the required diff if (WLED_MQTT_CONNECTED && _mqttPublish && (_mqttPublishAlways || fabsf(lastState - state) > minChange)) { char subuf[128]; snprintf_P(subuf, 127, PSTR("%s/%s"), mqttDeviceTopic, (const char *)topic); mqtt->publish(subuf, 0, false, String(state).c_str()); lastState = state; } } // Create an MQTT Sensor for Home Assistant Discovery purposes, this includes a pointer to the topic that is published to in the Loop. void mqttCreateHassSensor(const String &name, const String &topic, const String &deviceClass, const String &unitOfMeasurement) { String t = String(F("homeassistant/sensor/")) + mqttClientID + "/" + name + F("/config"); StaticJsonDocument<600> doc; doc[F("name")] = name; doc[F("state_topic")] = topic; doc[F("unique_id")] = String(mqttClientID) + name; if (unitOfMeasurement != "") doc[F("unit_of_measurement")] = unitOfMeasurement; if (deviceClass != "") doc[F("device_class")] = deviceClass; doc[F("expire_after")] = 1800; JsonObject device = doc.createNestedObject(F("device")); // attach the sensor to the same device device[F("name")] = serverDescription; device[F("identifiers")] = "wled-sensor-" + String(mqttClientID); device[F("manufacturer")] = F(WLED_BRAND); device[F("model")] = F(WLED_PRODUCT_NAME); device[F("sw_version")] = versionString; String temp; serializeJson(doc, temp); DEBUG_PRINTLN(t); DEBUG_PRINTLN(temp); mqtt->publish(t.c_str(), 0, true, temp.c_str()); } #endif public: void setup() { initializeAht(); } void loop() { // if usermod is disabled or called during strip updating just exit // NOTE: on very long strips strip.isUpdating() may always return true so update accordingly if (!_settingEnabled || strip.isUpdating()) return; // do your magic here unsigned long currentTime = millis(); if (currentTime - _lastLoopCheck < _checkInterval) return; _lastLoopCheck = currentTime; _lastStatus = _aht->readRawData(); if (_lastStatus == AHT10_ERROR) { // Perform softReset and retry DEBUG_PRINTLN(F("AHTxx returned error, doing softReset")); if (!_aht->softReset()) { DEBUG_PRINTLN(F("softReset failed")); return; } _lastStatus = _aht->readRawData(); } if (_lastStatus == AHT10_SUCCESS) { float temperature = truncateDecimals(_aht->readTemperature(AHT10_USE_READ_DATA)); float humidity = truncateDecimals(_aht->readHumidity(AHT10_USE_READ_DATA)); #ifndef WLED_DISABLE_MQTT // Push to MQTT // We can avoid reporting if the change is insignificant. The threshold chosen is below the level of accuracy, but way above 0.01 which is the precision of the value provided. // The AHT10/15/20 has an accuracy of 0.3C in the temperature readings mqttPublishIfChanged(F("temperature"), _lastTemperatureSent, temperature, 0.1f); // The AHT10/15/20 has an accuracy in the humidity sensor of 2% mqttPublishIfChanged(F("humidity"), _lastHumiditySent, humidity, 0.5f); #endif // Store _lastTemperature = temperature; _lastHumidity = humidity; } } #ifndef WLED_DISABLE_MQTT void onMqttConnect(bool sessionPresent) { mqttInitialize(); } #endif uint16_t getId() { return USERMOD_ID_AHT10; } void addToJsonInfo(JsonObject &root) override { // if "u" object does not exist yet wee need to create it JsonObject user = root["u"]; if (user.isNull()) user = root.createNestedObject("u"); #ifdef USERMOD_AHT10_DEBUG JsonArray temp = user.createNestedArray(F("AHT last loop")); temp.add(_lastLoopCheck); temp = user.createNestedArray(F("AHT last status")); temp.add(_lastStatus); #endif JsonArray jsonTemp = user.createNestedArray(F("Temperature")); JsonArray jsonHumidity = user.createNestedArray(F("Humidity")); if (_lastLoopCheck == 0) { // Before first run jsonTemp.add(F("Not read yet")); jsonHumidity.add(F("Not read yet")); return; } if (_lastStatus != AHT10_SUCCESS) { jsonTemp.add(F("An error occurred")); jsonHumidity.add(F("An error occurred")); return; } jsonTemp.add(_lastTemperature); jsonTemp.add(F("°C")); jsonHumidity.add(_lastHumidity); jsonHumidity.add(F("%")); } void addToConfig(JsonObject &root) { JsonObject top = root.createNestedObject(FPSTR(_name)); top[F("Enabled")] = _settingEnabled; top[F("I2CAddress")] = static_cast(_i2cAddress); top[F("SensorType")] = _ahtType; top[F("CheckInterval")] = _checkInterval / 1000; top[F("Decimals")] = log10f(_decimalFactor); #ifndef WLED_DISABLE_MQTT top[F("MqttPublish")] = _mqttPublish; top[F("MqttPublishAlways")] = _mqttPublishAlways; top[F("MqttHomeAssistantDiscovery")] = _mqttHomeAssistant; #endif DEBUG_PRINTLN(F("AHT10 config saved.")); } bool readFromConfig(JsonObject &root) override { // default settings values could be set here (or below using the 3-argument getJsonValue()) instead of in the class definition or constructor // setting them inside readFromConfig() is slightly more robust, handling the rare but plausible use case of single value being missing after boot (e.g. if the cfg.json was manually edited and a value was removed) JsonObject top = root[FPSTR(_name)]; bool configComplete = !top.isNull(); if (!configComplete) return false; bool tmpBool = false; configComplete &= getJsonValue(top[F("Enabled")], tmpBool); if (configComplete) _settingEnabled = tmpBool; configComplete &= getJsonValue(top[F("I2CAddress")], _i2cAddress); configComplete &= getJsonValue(top[F("CheckInterval")], _checkInterval); if (configComplete) { if (1 <= _checkInterval && _checkInterval <= 600) _checkInterval *= 1000; else // Invalid input _checkInterval = 60000; } configComplete &= getJsonValue(top[F("Decimals")], _decimalFactor); if (configComplete) { if (0 <= _decimalFactor && _decimalFactor <= 5) _decimalFactor = pow10f(_decimalFactor); else // Invalid input _decimalFactor = 100; } uint8_t tmpAhtType; configComplete &= getJsonValue(top[F("SensorType")], tmpAhtType); if (configComplete) { if (0 <= tmpAhtType && tmpAhtType <= 2) _ahtType = static_cast(tmpAhtType); else // Invalid input _ahtType = ASAIR_I2C_SENSOR::AHT10_SENSOR; } #ifndef WLED_DISABLE_MQTT configComplete &= getJsonValue(top[F("MqttPublish")], tmpBool); if (configComplete) _mqttPublish = tmpBool; configComplete &= getJsonValue(top[F("MqttPublishAlways")], tmpBool); if (configComplete) _mqttPublishAlways = tmpBool; configComplete &= getJsonValue(top[F("MqttHomeAssistantDiscovery")], tmpBool); if (configComplete) _mqttHomeAssistant = tmpBool; #endif if (_initDone) { // Reloading config initializeAht(); #ifndef WLED_DISABLE_MQTT mqttInitialize(); #endif } _initDone = true; return configComplete; } ~UsermodAHT10() { delete _aht; _aht = nullptr; } }; const char UsermodAHT10::_name[] PROGMEM = "AHTxx"; static UsermodAHT10 aht10_v2; REGISTER_USERMOD(aht10_v2); ================================================ FILE: usermods/AHT10_v2/README.md ================================================ # Usermod AHT10 This Usermod is designed to read a `AHT10`, `AHT15` or `AHT20` sensor and output the following: - Temperature - Humidity Configuration is performed via the Usermod menu. The following settings can be configured in the Usermod Menu: - I2CAddress: The i2c address in decimal. Set it to either 56 (0x38, the default) or 57 (0x39). - SensorType, one of: - 0 - AHT10 - 1 - AHT15 - 2 - AHT20 - CheckInterval: Number of seconds between readings - Decimals: Number of decimals to put in the output Dependencies, These must be added under `lib_deps` in your `platform.ini` (or `platform_override.ini`). - Libraries - `enjoyneering/AHT10@~1.1.0` (by [enjoyneering](https://registry.platformio.org/libraries/enjoyneering/AHT10)) - `Wire` ## Author [@LordMike](https://github.com/LordMike) # Compiling To enable, add 'AHT10' to `custom_usermods` in your platformio encrionment (e.g. in `platformio_override.ini`) ```ini [env:aht10_example] extends = env:esp32dev custom_usermods = ${env:esp32dev.custom_usermods} AHT10 ``` ================================================ FILE: usermods/AHT10_v2/library.json ================================================ { "name": "AHT10_v2", "build": { "libArchive": false }, "dependencies": { "enjoyneering/AHT10":"~1.1.0" } } ================================================ FILE: usermods/Analog_Clock/Analog_Clock.cpp ================================================ #include "wled.h" /* * Usermod for analog clock */ class AnalogClockUsermod : public Usermod { private: static constexpr uint32_t refreshRate = 50; // per second static constexpr uint32_t refreshDelay = 1000 / refreshRate; struct Segment { // config int16_t firstLed = 0; int16_t lastLed = 59; int16_t centerLed = 0; // runtime int16_t size; Segment() { update(); } void validateAndUpdate() { if (firstLed < 0 || firstLed >= strip.getLengthTotal() || lastLed < firstLed || lastLed >= strip.getLengthTotal()) { *this = {}; return; } if (centerLed < firstLed || centerLed > lastLed) { centerLed = firstLed; } update(); } void update() { size = lastLed - firstLed + 1; } }; // configuration (available in API and stored in flash) bool enabled = false; Segment mainSegment; bool hourMarksEnabled = true; uint32_t hourMarkColor = 0xFF0000; uint32_t hourColor = 0x0000FF; uint32_t minuteColor = 0x00FF00; bool secondsEnabled = true; Segment secondsSegment; uint32_t secondColor = 0xFF0000; bool blendColors = true; uint16_t secondsEffect = 0; // runtime bool initDone = false; uint32_t lastOverlayDraw = 0; void validateAndUpdate() { mainSegment.validateAndUpdate(); secondsSegment.validateAndUpdate(); if (secondsEffect < 0 || secondsEffect > 1) { secondsEffect = 0; } } int16_t adjustToSegment(double progress, Segment const& segment) { int16_t led = segment.centerLed + progress * segment.size; return led > segment.lastLed ? segment.firstLed + led - segment.lastLed - 1 : led; } void setPixelColor(uint16_t n, uint32_t c) { if (!blendColors) { strip.setPixelColor(n, c); } else { uint32_t oldC = strip.getPixelColor(n); strip.setPixelColor(n, qadd32(oldC, c)); } } String colorToHexString(uint32_t c) { char buffer[9]; sprintf(buffer, "%06X", c); return buffer; } bool hexStringToColor(String const& s, uint32_t& c, uint32_t def) { char *ep; unsigned long long r = strtoull(s.c_str(), &ep, 16); if (*ep == 0) { c = r; return true; } else { c = def; return false; } } void secondsEffectSineFade(int16_t secondLed, Toki::Time const& time) { uint32_t ms = time.ms % 1000; uint8_t b0 = (cos8_t(ms * 64 / 1000) - 128) * 2; setPixelColor(secondLed, scale32(secondColor, b0)); uint8_t b1 = (sin8_t(ms * 64 / 1000) - 128) * 2; setPixelColor(inc(secondLed, 1, secondsSegment), scale32(secondColor, b1)); } static inline uint32_t qadd32(uint32_t c1, uint32_t c2) { return RGBW32( qadd8(R(c1), R(c2)), qadd8(G(c1), G(c2)), qadd8(B(c1), B(c2)), qadd8(W(c1), W(c2)) ); } static inline uint32_t scale32(uint32_t c, fract8 scale) { return RGBW32( scale8(R(c), scale), scale8(G(c), scale), scale8(B(c), scale), scale8(W(c), scale) ); } static inline int16_t dec(int16_t n, int16_t i, Segment const& seg) { return n - seg.firstLed >= i ? n - i : seg.lastLed - seg.firstLed - i + n + 1; } static inline int16_t inc(int16_t n, int16_t i, Segment const& seg) { int16_t r = n + i; if (r > seg.lastLed) { return seg.firstLed + n - seg.lastLed; } return r; } public: AnalogClockUsermod() { } void setup() override { initDone = true; validateAndUpdate(); } void loop() override { if (millis() - lastOverlayDraw > refreshDelay) { strip.trigger(); } } void handleOverlayDraw() override { if (!enabled) { return; } lastOverlayDraw = millis(); auto time = toki.getTime(); double secondP = second(localTime) / 60.0; double minuteP = minute(localTime) / 60.0; double hourP = (hour(localTime) % 12) / 12.0 + minuteP / 12.0; if (hourMarksEnabled) { for (int Led = 0; Led <= 55; Led = Led + 5) { int16_t hourmarkled = adjustToSegment(Led / 60.0, mainSegment); setPixelColor(hourmarkled, hourMarkColor); } } if (secondsEnabled) { int16_t secondLed = adjustToSegment(secondP, secondsSegment); switch (secondsEffect) { case 0: // no effect setPixelColor(secondLed, secondColor); break; case 1: // fading seconds secondsEffectSineFade(secondLed, time); break; } // TODO: move to secondsTrailEffect // for (uint16_t i = 1; i < secondsTrail + 1; ++i) { // uint16_t trailLed = dec(secondLed, i, secondsSegment); // uint8_t trailBright = 255 / (secondsTrail + 1) * (secondsTrail - i + 1); // setPixelColor(trailLed, scale32(secondColor, trailBright)); // } } setPixelColor(adjustToSegment(minuteP, mainSegment), minuteColor); setPixelColor(adjustToSegment(hourP, mainSegment), hourColor); } void addToConfig(JsonObject& root) override { validateAndUpdate(); JsonObject top = root.createNestedObject(F("Analog Clock")); top[F("Overlay Enabled")] = enabled; top[F("First LED (Main Ring)")] = mainSegment.firstLed; top[F("Last LED (Main Ring)")] = mainSegment.lastLed; top[F("Center/12h LED (Main Ring)")] = mainSegment.centerLed; top[F("Hour Marks Enabled")] = hourMarksEnabled; top[F("Hour Mark Color (RRGGBB)")] = colorToHexString(hourMarkColor); top[F("Hour Color (RRGGBB)")] = colorToHexString(hourColor); top[F("Minute Color (RRGGBB)")] = colorToHexString(minuteColor); top[F("Show Seconds")] = secondsEnabled; top[F("First LED (Seconds Ring)")] = secondsSegment.firstLed; top[F("Last LED (Seconds Ring)")] = secondsSegment.lastLed; top[F("Center/12h LED (Seconds Ring)")] = secondsSegment.centerLed; top[F("Second Color (RRGGBB)")] = colorToHexString(secondColor); top[F("Seconds Effect (0-1)")] = secondsEffect; top[F("Blend Colors")] = blendColors; } bool readFromConfig(JsonObject& root) override { JsonObject top = root[F("Analog Clock")]; bool configComplete = !top.isNull(); String color; configComplete &= getJsonValue(top[F("Overlay Enabled")], enabled, false); configComplete &= getJsonValue(top[F("First LED (Main Ring)")], mainSegment.firstLed, 0); configComplete &= getJsonValue(top[F("Last LED (Main Ring)")], mainSegment.lastLed, 59); configComplete &= getJsonValue(top[F("Center/12h LED (Main Ring)")], mainSegment.centerLed, 0); configComplete &= getJsonValue(top[F("Hour Marks Enabled")], hourMarksEnabled, false); configComplete &= getJsonValue(top[F("Hour Mark Color (RRGGBB)")], color, F("161616")) && hexStringToColor(color, hourMarkColor, 0x161616); configComplete &= getJsonValue(top[F("Hour Color (RRGGBB)")], color, F("0000FF")) && hexStringToColor(color, hourColor, 0x0000FF); configComplete &= getJsonValue(top[F("Minute Color (RRGGBB)")], color, F("00FF00")) && hexStringToColor(color, minuteColor, 0x00FF00); configComplete &= getJsonValue(top[F("Show Seconds")], secondsEnabled, true); configComplete &= getJsonValue(top[F("First LED (Seconds Ring)")], secondsSegment.firstLed, 0); configComplete &= getJsonValue(top[F("Last LED (Seconds Ring)")], secondsSegment.lastLed, 59); configComplete &= getJsonValue(top[F("Center/12h LED (Seconds Ring)")], secondsSegment.centerLed, 0); configComplete &= getJsonValue(top[F("Second Color (RRGGBB)")], color, F("FF0000")) && hexStringToColor(color, secondColor, 0xFF0000); configComplete &= getJsonValue(top[F("Seconds Effect (0-1)")], secondsEffect, 0); configComplete &= getJsonValue(top[F("Blend Colors")], blendColors, true); if (initDone) { validateAndUpdate(); } return configComplete; } uint16_t getId() override { return USERMOD_ID_ANALOG_CLOCK; } }; static AnalogClockUsermod analog_clock; REGISTER_USERMOD(analog_clock); ================================================ FILE: usermods/Analog_Clock/library.json ================================================ { "name": "Analog_Clock", "build": { "libArchive": false } } ================================================ FILE: usermods/Animated_Staircase/Animated_Staircase.cpp ================================================ /* * Usermod for detecting people entering/leaving a staircase and switching the * staircase on/off. * * Edit the Animated_Staircase_config.h file to compile this usermod for your * specific configuration. * * See the accompanying README.md file for more info. */ #include "wled.h" class Animated_Staircase : public Usermod { private: /* configuration (available in API and stored in flash) */ bool enabled = false; // Enable this usermod unsigned long segment_delay_ms = 150; // Time between switching each segment unsigned long on_time_ms = 30000; // The time for the light to stay on int8_t topPIRorTriggerPin = -1; // disabled int8_t bottomPIRorTriggerPin = -1; // disabled int8_t topEchoPin = -1; // disabled int8_t bottomEchoPin = -1; // disabled bool useUSSensorTop = false; // using PIR or UltraSound sensor? bool useUSSensorBottom = false; // using PIR or UltraSound sensor? unsigned int topMaxDist = 50; // default maximum measured distance in cm, top unsigned int bottomMaxDist = 50; // default maximum measured distance in cm, bottom bool togglePower = false; // toggle power on/off with staircase on/off /* runtime variables */ bool initDone = false; // Time between checking of the sensors const unsigned int scanDelay = 100; // Lights on or off. // Flipping this will start a transition. bool on = false; // Swipe direction for current transition #define SWIPE_UP true #define SWIPE_DOWN false bool swipe = SWIPE_UP; // Indicates which Sensor was seen last (to determine // the direction when swiping off) #define LOWER false #define UPPER true bool lastSensor = LOWER; // Time of the last transition action unsigned long lastTime = 0; // Time of the last sensor check unsigned long lastScanTime = 0; // Last time the lights were switched on or off unsigned long lastSwitchTime = 0; // segment id between onIndex and offIndex are on. // controll the swipe by setting/moving these indices around. // onIndex must be less than or equal to offIndex byte onIndex = 0; byte offIndex = 0; // The maximum number of configured segments. // Dynamically updated based on user configuration. byte maxSegmentId = 1; byte minSegmentId = 0; // These values are used by the API to read the // last sensor state, or trigger a sensor // through the API bool topSensorRead = false; bool topSensorWrite = false; bool bottomSensorRead = false; bool bottomSensorWrite = false; bool topSensorState = false; bool bottomSensorState = false; // strings to reduce flash memory usage (used more than twice) static const char _name[]; static const char _enabled[]; static const char _segmentDelay[]; static const char _onTime[]; static const char _useTopUltrasoundSensor[]; static const char _topPIRorTrigger_pin[]; static const char _topEcho_pin[]; static const char _useBottomUltrasoundSensor[]; static const char _bottomPIRorTrigger_pin[]; static const char _bottomEcho_pin[]; static const char _topEchoCm[]; static const char _bottomEchoCm[]; static const char _togglePower[]; void publishMqtt(bool bottom, const char* state) { #ifndef WLED_DISABLE_MQTT //Check if MQTT Connected, otherwise it will crash the 8266 if (WLED_MQTT_CONNECTED){ char subuf[64]; sprintf_P(subuf, PSTR("%s/motion/%d"), mqttDeviceTopic, (int)bottom); mqtt->publish(subuf, 0, false, state); } #endif } void updateSegments() { for (int i = minSegmentId; i < maxSegmentId; i++) { Segment &seg = strip.getSegment(i); if (!seg.isActive()) continue; // skip gaps if (i >= onIndex && i < offIndex) { seg.setOption(SEG_OPTION_ON, true); // We may need to copy mode and colors from segment 0 to make sure // changes are propagated even when the config is changed during a wipe // seg.setMode(mainsegment.mode); // seg.setColor(0, mainsegment.colors[0]); } else { seg.setOption(SEG_OPTION_ON, false); } // Always mark segments as "transitional", we are animating the staircase //seg.setOption(SEG_OPTION_TRANSITIONAL, true); // not needed anymore as setOption() does it } strip.trigger(); // force strip refresh stateChanged = true; // inform external devices/UI of change colorUpdated(CALL_MODE_DIRECT_CHANGE); } /* * Detects if an object is within ultrasound range. * signalPin: The pin where the pulse is sent * echoPin: The pin where the echo is received * maxTimeUs: Detection timeout in microseconds. If an echo is * received within this time, an object is detected * and the function will return true. * * The speed of sound is 343 meters per second at 20 degrees Celsius. * Since the sound has to travel back and forth, the detection * distance for the sensor in cm is (0.0343 * maxTimeUs) / 2. * * For practical reasons, here are some useful distances: * * Distance = maxtime * 5 cm = 292 uS * 10 cm = 583 uS * 20 cm = 1166 uS * 30 cm = 1749 uS * 50 cm = 2915 uS * 100 cm = 5831 uS */ bool ultrasoundRead(int8_t signalPin, int8_t echoPin, unsigned int maxTimeUs) { if (signalPin<0 || echoPin<0) return false; digitalWrite(signalPin, LOW); delayMicroseconds(2); digitalWrite(signalPin, HIGH); delayMicroseconds(10); digitalWrite(signalPin, LOW); return pulseIn(echoPin, HIGH, maxTimeUs) > 0; } bool checkSensors() { bool sensorChanged = false; if ((millis() - lastScanTime) > scanDelay) { lastScanTime = millis(); bottomSensorRead = bottomSensorWrite || (!useUSSensorBottom ? (bottomPIRorTriggerPin<0 ? false : digitalRead(bottomPIRorTriggerPin)) : ultrasoundRead(bottomPIRorTriggerPin, bottomEchoPin, bottomMaxDist*59) // cm to us ); topSensorRead = topSensorWrite || (!useUSSensorTop ? (topPIRorTriggerPin<0 ? false : digitalRead(topPIRorTriggerPin)) : ultrasoundRead(topPIRorTriggerPin, topEchoPin, topMaxDist*59) // cm to us ); if (bottomSensorRead != bottomSensorState) { bottomSensorState = bottomSensorRead; // change previous state sensorChanged = true; publishMqtt(true, bottomSensorState ? "on" : "off"); DEBUG_PRINTLN(F("Bottom sensor changed.")); } if (topSensorRead != topSensorState) { topSensorState = topSensorRead; // change previous state sensorChanged = true; publishMqtt(false, topSensorState ? "on" : "off"); DEBUG_PRINTLN(F("Top sensor changed.")); } // Values read, reset the flags for next API call topSensorWrite = false; bottomSensorWrite = false; if (topSensorRead != bottomSensorRead) { lastSwitchTime = millis(); if (on) { lastSensor = topSensorRead; } else { if (togglePower && onIndex == offIndex && offMode) toggleOnOff(); // toggle power on if off // If the bottom sensor triggered, we need to swipe up, ON swipe = bottomSensorRead; DEBUG_PRINT(F("ON -> Swipe ")); DEBUG_PRINTLN(swipe ? F("up.") : F("down.")); if (onIndex == offIndex) { // Position the indices for a correct on-swipe if (swipe == SWIPE_UP) { onIndex = minSegmentId; } else { onIndex = maxSegmentId; } offIndex = onIndex; } on = true; } } } return sensorChanged; } void autoPowerOff() { if ((millis() - lastSwitchTime) > on_time_ms) { // if sensors are still on, do nothing if (bottomSensorState || topSensorState) return; // Swipe OFF in the direction of the last sensor detection swipe = lastSensor; on = false; DEBUG_PRINT(F("OFF -> Swipe ")); DEBUG_PRINTLN(swipe ? F("up.") : F("down.")); } } void updateSwipe() { if ((millis() - lastTime) > segment_delay_ms) { lastTime = millis(); byte oldOn = onIndex; byte oldOff = offIndex; if (on) { // Turn on all segments onIndex = MAX(minSegmentId, onIndex - 1); offIndex = MIN(maxSegmentId, offIndex + 1); } else { if (swipe == SWIPE_UP) { onIndex = MIN(offIndex, onIndex + 1); } else { offIndex = MAX(onIndex, offIndex - 1); } } if (oldOn != onIndex || oldOff != offIndex) { updateSegments(); // reduce the number of updates to necessary ones if (togglePower && onIndex == offIndex && !offMode && !on) toggleOnOff(); // toggle power off for all segments off } } } // send sensor values to JSON API void writeSensorsToJson(JsonObject& staircase) { staircase[F("top-sensor")] = topSensorRead; staircase[F("bottom-sensor")] = bottomSensorRead; } // allow overrides from JSON API void readSensorsFromJson(JsonObject& staircase) { bottomSensorWrite = bottomSensorState || (staircase[F("bottom-sensor")].as()); topSensorWrite = topSensorState || (staircase[F("top-sensor")].as()); } void enable(bool enable) { if (enable) { DEBUG_PRINTLN(F("Animated Staircase enabled.")); DEBUG_PRINT(F("Delay between steps: ")); DEBUG_PRINT(segment_delay_ms); DEBUG_PRINT(F(" milliseconds.\nStairs switch off after: ")); DEBUG_PRINT(on_time_ms / 1000); DEBUG_PRINTLN(F(" seconds.")); if (!useUSSensorBottom) pinMode(bottomPIRorTriggerPin, INPUT_PULLUP); else { pinMode(bottomPIRorTriggerPin, OUTPUT); pinMode(bottomEchoPin, INPUT); } if (!useUSSensorTop) pinMode(topPIRorTriggerPin, INPUT_PULLUP); else { pinMode(topPIRorTriggerPin, OUTPUT); pinMode(topEchoPin, INPUT); } onIndex = minSegmentId = strip.getMainSegmentId(); // it may not be the best idea to start with main segment as it may not be the first one offIndex = maxSegmentId = strip.getLastActiveSegmentId() + 1; // shorten the strip transition time to be equal or shorter than segment delay transitionDelay = segment_delay_ms; strip.setTransition(segment_delay_ms); strip.trigger(); } else { if (togglePower && !on && offMode) toggleOnOff(); // toggle power on if off // Restore segment options for (int i = 0; i <= strip.getLastActiveSegmentId(); i++) { Segment &seg = strip.getSegment(i); if (!seg.isActive()) continue; // skip vector gaps seg.setOption(SEG_OPTION_ON, true); } strip.trigger(); // force strip update stateChanged = true; // inform external devices/UI of change colorUpdated(CALL_MODE_DIRECT_CHANGE); DEBUG_PRINTLN(F("Animated Staircase disabled.")); } enabled = enable; } public: void setup() { // standardize invalid pin numbers to -1 if (topPIRorTriggerPin < 0) topPIRorTriggerPin = -1; if (topEchoPin < 0) topEchoPin = -1; if (bottomPIRorTriggerPin < 0) bottomPIRorTriggerPin = -1; if (bottomEchoPin < 0) bottomEchoPin = -1; // allocate pins PinManagerPinType pins[4] = { { topPIRorTriggerPin, useUSSensorTop }, { topEchoPin, false }, { bottomPIRorTriggerPin, useUSSensorBottom }, { bottomEchoPin, false }, }; // NOTE: this *WILL* return TRUE if all the pins are set to -1. // this is *BY DESIGN*. if (!PinManager::allocateMultiplePins(pins, 4, PinOwner::UM_AnimatedStaircase)) { topPIRorTriggerPin = -1; topEchoPin = -1; bottomPIRorTriggerPin = -1; bottomEchoPin = -1; enabled = false; } enable(enabled); initDone = true; } void loop() { if (!enabled || strip.isUpdating()) return; minSegmentId = strip.getMainSegmentId(); // it may not be the best idea to start with main segment as it may not be the first one maxSegmentId = strip.getLastActiveSegmentId() + 1; checkSensors(); if (on) autoPowerOff(); updateSwipe(); } uint16_t getId() { return USERMOD_ID_ANIMATED_STAIRCASE; } #ifndef WLED_DISABLE_MQTT /** * handling of MQTT message * topic only contains stripped topic (part after /wled/MAC) * topic should look like: /swipe with amessage of [up|down] */ bool onMqttMessage(char* topic, char* payload) { if (strlen(topic) == 6 && strncmp_P(topic, PSTR("/swipe"), 6) == 0) { String action = payload; if (action == "up") { bottomSensorWrite = true; return true; } else if (action == "down") { topSensorWrite = true; return true; } else if (action == "on") { enable(true); return true; } else if (action == "off") { enable(false); return true; } } return false; } /** * subscribe to MQTT topic for controlling usermod */ void onMqttConnect(bool sessionPresent) { //(re)subscribe to required topics char subuf[64]; if (mqttDeviceTopic[0] != 0) { strcpy(subuf, mqttDeviceTopic); strcat_P(subuf, PSTR("/swipe")); mqtt->subscribe(subuf, 0); } } #endif void addToJsonState(JsonObject& root) { JsonObject staircase = root[FPSTR(_name)]; if (staircase.isNull()) { staircase = root.createNestedObject(FPSTR(_name)); } writeSensorsToJson(staircase); DEBUG_PRINTLN(F("Staircase sensor state exposed in API.")); } /* * Reads configuration settings from the json API. * See void addToJsonState(JsonObject& root) */ void readFromJsonState(JsonObject& root) { if (!initDone) return; // prevent crash on boot applyPreset() bool en = enabled; JsonObject staircase = root[FPSTR(_name)]; if (!staircase.isNull()) { if (staircase[FPSTR(_enabled)].is()) { en = staircase[FPSTR(_enabled)].as(); } else { String str = staircase[FPSTR(_enabled)]; // checkbox -> off or on en = (bool)(str!="off"); // off is guaranteed to be present } if (en != enabled) enable(en); readSensorsFromJson(staircase); DEBUG_PRINTLN(F("Staircase sensor state read from API.")); } } void appendConfigData() { //oappend(F("dd=addDropdown('staircase','selectfield');")); //oappend(F("addOption(dd,'1st value',0);")); //oappend(F("addOption(dd,'2nd value',1);")); //oappend(F("addInfo('staircase:selectfield',1,'additional info');")); // 0 is field type, 1 is actual field } /* * Writes the configuration to internal flash memory. */ void addToConfig(JsonObject& root) { JsonObject staircase = root[FPSTR(_name)]; if (staircase.isNull()) { staircase = root.createNestedObject(FPSTR(_name)); } staircase[FPSTR(_enabled)] = enabled; staircase[FPSTR(_segmentDelay)] = segment_delay_ms; staircase[FPSTR(_onTime)] = on_time_ms / 1000; staircase[FPSTR(_useTopUltrasoundSensor)] = useUSSensorTop; staircase[FPSTR(_topPIRorTrigger_pin)] = topPIRorTriggerPin; staircase[FPSTR(_topEcho_pin)] = useUSSensorTop ? topEchoPin : -1; staircase[FPSTR(_useBottomUltrasoundSensor)] = useUSSensorBottom; staircase[FPSTR(_bottomPIRorTrigger_pin)] = bottomPIRorTriggerPin; staircase[FPSTR(_bottomEcho_pin)] = useUSSensorBottom ? bottomEchoPin : -1; staircase[FPSTR(_topEchoCm)] = topMaxDist; staircase[FPSTR(_bottomEchoCm)] = bottomMaxDist; staircase[FPSTR(_togglePower)] = togglePower; DEBUG_PRINTLN(F("Staircase config saved.")); } /* * Reads the configuration to internal flash memory before setup() is called. * * The function should return true if configuration was successfully loaded or false if there was no configuration. */ bool readFromConfig(JsonObject& root) { bool oldUseUSSensorTop = useUSSensorTop; bool oldUseUSSensorBottom = useUSSensorBottom; int8_t oldTopAPin = topPIRorTriggerPin; int8_t oldTopBPin = topEchoPin; int8_t oldBottomAPin = bottomPIRorTriggerPin; int8_t oldBottomBPin = bottomEchoPin; JsonObject top = root[FPSTR(_name)]; if (top.isNull()) { DEBUG_PRINT(FPSTR(_name)); DEBUG_PRINTLN(F(": No config found. (Using defaults.)")); return false; } enabled = top[FPSTR(_enabled)] | enabled; segment_delay_ms = top[FPSTR(_segmentDelay)] | segment_delay_ms; segment_delay_ms = (unsigned long) min((unsigned long)10000,max((unsigned long)10,(unsigned long)segment_delay_ms)); // max delay 10s on_time_ms = top[FPSTR(_onTime)] | on_time_ms/1000; on_time_ms = min(900,max(10,(int)on_time_ms)) * 1000; // min 10s, max 15min useUSSensorTop = top[FPSTR(_useTopUltrasoundSensor)] | useUSSensorTop; topPIRorTriggerPin = top[FPSTR(_topPIRorTrigger_pin)] | topPIRorTriggerPin; topEchoPin = top[FPSTR(_topEcho_pin)] | topEchoPin; useUSSensorBottom = top[FPSTR(_useBottomUltrasoundSensor)] | useUSSensorBottom; bottomPIRorTriggerPin = top[FPSTR(_bottomPIRorTrigger_pin)] | bottomPIRorTriggerPin; bottomEchoPin = top[FPSTR(_bottomEcho_pin)] | bottomEchoPin; topMaxDist = top[FPSTR(_topEchoCm)] | topMaxDist; topMaxDist = min(150,max(30,(int)topMaxDist)); // max distance ~1.5m (a lag of 9ms may be expected) bottomMaxDist = top[FPSTR(_bottomEchoCm)] | bottomMaxDist; bottomMaxDist = min(150,max(30,(int)bottomMaxDist)); // max distance ~1.5m (a lag of 9ms may be expected) togglePower = top[FPSTR(_togglePower)] | togglePower; // staircase toggles power on/off DEBUG_PRINT(FPSTR(_name)); if (!initDone) { // first run: reading from cfg.json DEBUG_PRINTLN(F(" config loaded.")); } else { // changing parameters from settings page DEBUG_PRINTLN(F(" config (re)loaded.")); bool changed = false; if ((oldUseUSSensorTop != useUSSensorTop) || (oldUseUSSensorBottom != useUSSensorBottom) || (oldTopAPin != topPIRorTriggerPin) || (oldTopBPin != topEchoPin) || (oldBottomAPin != bottomPIRorTriggerPin) || (oldBottomBPin != bottomEchoPin)) { changed = true; PinManager::deallocatePin(oldTopAPin, PinOwner::UM_AnimatedStaircase); PinManager::deallocatePin(oldTopBPin, PinOwner::UM_AnimatedStaircase); PinManager::deallocatePin(oldBottomAPin, PinOwner::UM_AnimatedStaircase); PinManager::deallocatePin(oldBottomBPin, PinOwner::UM_AnimatedStaircase); } if (changed) setup(); } // use "return !top["newestParameter"].isNull();" when updating Usermod with new features return !top[FPSTR(_togglePower)].isNull(); } /* * Shows the delay between steps and power-off time in the "info" * tab of the web-UI. */ void addToJsonInfo(JsonObject& root) { JsonObject user = root["u"]; if (user.isNull()) { user = root.createNestedObject("u"); } JsonArray infoArr = user.createNestedArray(FPSTR(_name)); // name String uiDomString = F(""); infoArr.add(uiDomString); } }; // strings to reduce flash memory usage (used more than twice) const char Animated_Staircase::_name[] PROGMEM = "staircase"; const char Animated_Staircase::_enabled[] PROGMEM = "enabled"; const char Animated_Staircase::_segmentDelay[] PROGMEM = "segment-delay-ms"; const char Animated_Staircase::_onTime[] PROGMEM = "on-time-s"; const char Animated_Staircase::_useTopUltrasoundSensor[] PROGMEM = "useTopUltrasoundSensor"; const char Animated_Staircase::_topPIRorTrigger_pin[] PROGMEM = "topPIRorTrigger_pin"; const char Animated_Staircase::_topEcho_pin[] PROGMEM = "topEcho_pin"; const char Animated_Staircase::_useBottomUltrasoundSensor[] PROGMEM = "useBottomUltrasoundSensor"; const char Animated_Staircase::_bottomPIRorTrigger_pin[] PROGMEM = "bottomPIRorTrigger_pin"; const char Animated_Staircase::_bottomEcho_pin[] PROGMEM = "bottomEcho_pin"; const char Animated_Staircase::_topEchoCm[] PROGMEM = "top-dist-cm"; const char Animated_Staircase::_bottomEchoCm[] PROGMEM = "bottom-dist-cm"; const char Animated_Staircase::_togglePower[] PROGMEM = "toggle-on-off"; static Animated_Staircase animated_staircase; REGISTER_USERMOD(animated_staircase); ================================================ FILE: usermods/Animated_Staircase/README.md ================================================ # Usermod Animated Staircase This usermod makes your staircase look cool by illuminating it with an animation. It uses PIR or ultrasonic sensors at the top and bottom of your stairs to: - Light up the steps in the direction you're walking. - Switch off the steps after you, in the direction of the last detected movement. - Always switch on when one of the sensors detects movement, even if an effect is still running. It can gracefully handle multiple people on the stairs. The Animated Staircase can be controlled by the WLED API. Change settings such as speed, on/off time and distance by sending an HTTP request, see below. ## WLED integration To include this usermod in your WLED setup, you have to be able to [compile WLED from source](https://kno.wled.ge/advanced/compiling-wled/). Before compiling, you have to make the following modifications: Edit your environment in `platformio_override.ini` 1. Open `platformio_override.ini` 2. add `Animated_Staircase` to the `custom_usermods` line for your environment You can configure usermod using the Usermods settings page. Please enter GPIO pins for PIR or ultrasonic sensors (trigger and echo). If you use PIR sensor enter -1 for echo pin. Maximum distance for ultrasonic sensor can be configured as the time needed for an echo (see below). ## Hardware installation 1. Attach the LED strip to each step of the stairs. 2. Connect the ESP8266 pin D4 or ESP32 pin D2 to the first LED data pin at the bottom step. 3. Connect the data-out pin at the end of each strip per step to the data-in pin on the next step, creating one large virtual LED strip. 4. Mount sensors of choice at the bottom and top of the stairs and connect them to the ESP. 5. To make sure all LEDs get enough power and have your staircase lighted evenly, power each step from one side, using at least AWG14 or 2.5mm^2 cable. Don't connect them serial as you do for the datacable! You _may_ need to use 10k pull-down resistors on the selected PIR pins, depending on the sensor. ## WLED configuration 1. In the WLED UI, configure a segment for each step. The lowest step of the stairs is the lowest segment id. 2. Save your segments into a preset. 3. Ideally, add the preset in the config > LED setup menu to the "apply preset **n** at boot" setting. ## Changing behavior through API The Staircase settings can be changed through the WLED JSON api. **NOTE:** We are using [curl](https://curl.se/) to send HTTP POSTs to the WLED API. If you're using Windows and want to use the curl commands, replace the `\` with a `^` or remove them and put everything on one line. | Setting | Description | Default | |------------------|---------------------------------------------------------------|---------| | enabled | Enable or disable the usermod | true | | bottom-sensor | Manually trigger a down to up animation via API | false | | top-sensor | Manually trigger an up to down animation via API | false | To read the current settings, open a browser to `http://xxx.xxx.xxx.xxx/json/state` (use your WLED device IP address). The device will respond with a json object containing all WLED settings. The staircase settings and sensor states are inside the WLED "state" element: ```json { "state": { "staircase": { "enabled": true, "bottom-sensor": false, "top-sensor": false }, } ``` ### Enable/disable the usermod By disabling the usermod you will be able to keep the LED's on, independent from the sensor activity. This enables you to play with the lights without the usermod switching them on or off. To disable the usermod: ```bash curl -X POST -H "Content-Type: application/json" \ -d {"staircase":{"enabled":false}} \ xxx.xxx.xxx.xxx/json/state ``` To enable the usermod again, use `"enabled":true`. Alternatively you can use _Usermod_ Settings page where you can change other parameters as well. ### Changing animation parameters and detection range of the ultrasonic HC-SR04 sensor Using _Usermod_ Settings page you can define different usermod parameters, including sensor pins, delay between segment activation etc. When an ultrasonic sensor is enabled you can enter maximum detection distance in centimeters separately for top and bottom sensors. **Please note:** using an HC-SR04 sensor, particularly when detecting echos at longer distances creates delays in the WLED software, _might_ introduce timing hiccups in your animation or a less responsive web interface. It is therefore advised to keep the detection distance as short as possible. ### Animation triggering through the API In addition to activation by one of the stair sensors, you can also trigger the animation manually via the API. To simulate triggering the bottom sensor, use: ```bash curl -X POST -H "Content-Type: application/json" \ -d '{"staircase":{"bottom-sensor":true}}' \ xxx.xxx.xxx.xxx/json/state ``` Likewise, to trigger the top sensor: ```bash curl -X POST -H "Content-Type: application/json" \ -d '{"staircase":{"top-sensor":true}}' \ xxx.xxx.xxx.xxx/json/state ``` **MQTT** You can publish a message with either `up` or `down` on topic `/swipe` to trigger animation. You can also use `on` or `off` for enabling or disabling the usermod. Have fun with this usermod `www.rolfje.com` Modifications @blazoncek ## Change log 2021-04 - Adaptation for runtime configuration. ================================================ FILE: usermods/Animated_Staircase/library.json ================================================ { "name": "Animated_Staircase", "build": { "libArchive": false } } ================================================ FILE: usermods/Artemis_reciever/readme.md ================================================ Usermod to allow WLED to receive via UDP port from RGB.NET (and therefore add as a device to be controlled within artemis on PC) This is only a very simple code to support a single led strip, it does not support the full function of the RGB.NET sketch for esp8266 only what is needed to be used with Artemis. It will show as a ws281x device in artemis when you provide the correct hostname or ip. Artemis queries the number of LEDs via the web interface (/config) but communication to set the LEDs is all done via the UDP interface. To install, copy the usermod.cpp file to wled00 folder and recompile ================================================ FILE: usermods/Artemis_reciever/usermod.cpp ================================================ /* * RGB.NET (artemis) receiver * * This works via the UDP, http is not supported apart from reporting LED count * * */ #include "wled.h" #include WiFiUDP UDP; const unsigned int RGBNET_localUdpPort = 1872; // local port to listen on unsigned char RGBNET_packet[770]; long lastTime = 0; int delayMs = 10; bool isRGBNETUDPEnabled; void RGBNET_readValues() { int RGBNET_packetSize = UDP.parsePacket(); if (RGBNET_packetSize) { // receive incoming UDP packets int sequenceNumber = UDP.read(); int channel = UDP.read(); //channel data is not used we only supports one channel int len = UDP.read(RGBNET_packet, strip.getLengthTotal()*3); if(len==0){ return; } for (int i = 0; i < len; i=i+3) { strip.setPixelColor(i/3, RGBNET_packet[i], RGBNET_packet[i+1], RGBNET_packet[i+2], 0); } //strip.show(); } } //update LED strip void RGBNET_show() { strip.show(); lastTime = millis(); } //This function provides a json with info on the number of LEDs connected // it is needed by artemis to know how many LEDs to display on the surface void handleConfig(AsyncWebServerRequest *request) { String config = (String)"{\ \"channels\": [\ {\ \"channel\": 1,\ \"leds\": " + strip.getLengthTotal() + "\ },\ {\ \"channel\": 2,\ \"leds\": " + "0" + "\ },\ {\ \"channel\": 3,\ \"leds\": " + "0" + "\ },\ {\ \"channel\": 4,\ \"leds\": " + "0" + "\ }\ ]\ }"; request->send(200, "application/json", config); } void userSetup() { server.on("/config", HTTP_GET, [](AsyncWebServerRequest *request){ handleConfig(request); }); } void userConnected() { // new wifi, who dis? UDP.begin(RGBNET_localUdpPort); isRGBNETUDPEnabled = true; } void userLoop() { RGBNET_readValues(); if (millis()-lastTime > delayMs) { RGBNET_show(); } } ================================================ FILE: usermods/BH1750_v2/BH1750_v2.cpp ================================================ // force the compiler to show a warning to confirm that this file is included #warning **** Included USERMOD_BH1750 **** #include "wled.h" #include "BH1750_v2.h" #ifdef WLED_DISABLE_MQTT #error "This user mod requires MQTT to be enabled." #endif static bool checkBoundSensor(float newValue, float prevValue, float maxDiff) { return isnan(prevValue) || newValue <= prevValue - maxDiff || newValue >= prevValue + maxDiff || (newValue == 0.0 && prevValue > 0.0); } void Usermod_BH1750::_mqttInitialize() { mqttLuminanceTopic = String(mqttDeviceTopic) + F("/brightness"); if (HomeAssistantDiscovery) _createMqttSensor(F("Brightness"), mqttLuminanceTopic, F("Illuminance"), F(" lx")); } // Create an MQTT Sensor for Home Assistant Discovery purposes, this includes a pointer to the topic that is published to in the Loop. void Usermod_BH1750::_createMqttSensor(const String &name, const String &topic, const String &deviceClass, const String &unitOfMeasurement) { String t = String(F("homeassistant/sensor/")) + mqttClientID + F("/") + name + F("/config"); StaticJsonDocument<600> doc; doc[F("name")] = String(serverDescription) + " " + name; doc[F("state_topic")] = topic; doc[F("unique_id")] = String(mqttClientID) + name; if (unitOfMeasurement != "") doc[F("unit_of_measurement")] = unitOfMeasurement; if (deviceClass != "") doc[F("device_class")] = deviceClass; doc[F("expire_after")] = 1800; JsonObject device = doc.createNestedObject(F("device")); // attach the sensor to the same device device[F("name")] = serverDescription; device[F("identifiers")] = "wled-sensor-" + String(mqttClientID); device[F("manufacturer")] = F(WLED_BRAND); device[F("model")] = F(WLED_PRODUCT_NAME); device[F("sw_version")] = versionString; String temp; serializeJson(doc, temp); DEBUG_PRINTLN(t); DEBUG_PRINTLN(temp); mqtt->publish(t.c_str(), 0, true, temp.c_str()); } void Usermod_BH1750::setup() { if (i2c_scl<0 || i2c_sda<0) { enabled = false; return; } sensorFound = lightMeter.begin(); initDone = true; } void Usermod_BH1750::loop() { if ((!enabled) || strip.isUpdating()) return; unsigned long now = millis(); // check to see if we are due for taking a measurement // lastMeasurement will not be updated until the conversion // is complete the the reading is finished if (now - lastMeasurement < minReadingInterval) { return; } bool shouldUpdate = now - lastSend > maxReadingInterval; float lux = lightMeter.readLightLevel(); lastMeasurement = millis(); getLuminanceComplete = true; if (shouldUpdate || checkBoundSensor(lux, lastLux, offset)) { lastLux = lux; lastSend = millis(); if (WLED_MQTT_CONNECTED) { if (!mqttInitialized) { _mqttInitialize(); mqttInitialized = true; } mqtt->publish(mqttLuminanceTopic.c_str(), 0, true, String(lux).c_str()); DEBUG_PRINTLN(F("Brightness: ") + String(lux) + F("lx")); } else { DEBUG_PRINTLN(F("Missing MQTT connection. Not publishing data")); } } } void Usermod_BH1750::addToJsonInfo(JsonObject &root) { JsonObject user = root[F("u")]; if (user.isNull()) user = root.createNestedObject(F("u")); JsonArray lux_json = user.createNestedArray(F("Luminance")); if (!enabled) { lux_json.add(F("disabled")); } else if (!sensorFound) { // if no sensor lux_json.add(F("BH1750 ")); lux_json.add(F("Not Found")); } else if (!getLuminanceComplete) { // if we haven't read the sensor yet, let the user know // that we are still waiting for the first measurement lux_json.add((USERMOD_BH1750_FIRST_MEASUREMENT_AT - millis()) / 1000); lux_json.add(F(" sec until read")); return; } else { lux_json.add(lastLux); lux_json.add(F(" lx")); } } // (called from set.cpp) stores persistent properties to cfg.json void Usermod_BH1750::addToConfig(JsonObject &root) { // we add JSON object. JsonObject top = root.createNestedObject(FPSTR(_name)); // usermodname top[FPSTR(_enabled)] = enabled; top[FPSTR(_maxReadInterval)] = maxReadingInterval; top[FPSTR(_minReadInterval)] = minReadingInterval; top[FPSTR(_HomeAssistantDiscovery)] = HomeAssistantDiscovery; top[FPSTR(_offset)] = offset; DEBUG_PRINTLN(F("BH1750 config saved.")); } // called before setup() to populate properties from values stored in cfg.json bool Usermod_BH1750::readFromConfig(JsonObject &root) { // we look for JSON object. JsonObject top = root[FPSTR(_name)]; if (top.isNull()) { DEBUG_PRINT(FPSTR(_name)); DEBUG_PRINT(F("BH1750")); DEBUG_PRINTLN(F(": No config found. (Using defaults.)")); return false; } bool configComplete = !top.isNull(); configComplete &= getJsonValue(top[FPSTR(_enabled)], enabled, false); configComplete &= getJsonValue(top[FPSTR(_maxReadInterval)], maxReadingInterval, 10000); //ms configComplete &= getJsonValue(top[FPSTR(_minReadInterval)], minReadingInterval, 500); //ms configComplete &= getJsonValue(top[FPSTR(_HomeAssistantDiscovery)], HomeAssistantDiscovery, false); configComplete &= getJsonValue(top[FPSTR(_offset)], offset, 1); DEBUG_PRINT(FPSTR(_name)); if (!initDone) { DEBUG_PRINTLN(F(" config loaded.")); } else { DEBUG_PRINTLN(F(" config (re)loaded.")); } return configComplete; } // strings to reduce flash memory usage (used more than twice) const char Usermod_BH1750::_name[] PROGMEM = "BH1750"; const char Usermod_BH1750::_enabled[] PROGMEM = "enabled"; const char Usermod_BH1750::_maxReadInterval[] PROGMEM = "max-read-interval-ms"; const char Usermod_BH1750::_minReadInterval[] PROGMEM = "min-read-interval-ms"; const char Usermod_BH1750::_HomeAssistantDiscovery[] PROGMEM = "HomeAssistantDiscoveryLux"; const char Usermod_BH1750::_offset[] PROGMEM = "offset-lx"; static Usermod_BH1750 bh1750_v2; REGISTER_USERMOD(bh1750_v2); ================================================ FILE: usermods/BH1750_v2/BH1750_v2.h ================================================ #pragma once #include "wled.h" #include #ifdef WLED_DISABLE_MQTT #error "This user mod requires MQTT to be enabled." #endif // the max frequency to check photoresistor, 10 seconds #ifndef USERMOD_BH1750_MAX_MEASUREMENT_INTERVAL #define USERMOD_BH1750_MAX_MEASUREMENT_INTERVAL 10000 #endif // the min frequency to check photoresistor, 500 ms #ifndef USERMOD_BH1750_MIN_MEASUREMENT_INTERVAL #define USERMOD_BH1750_MIN_MEASUREMENT_INTERVAL 500 #endif // how many seconds after boot to take first measurement, 10 seconds #ifndef USERMOD_BH1750_FIRST_MEASUREMENT_AT #define USERMOD_BH1750_FIRST_MEASUREMENT_AT 10000 #endif // only report if difference grater than offset value #ifndef USERMOD_BH1750_OFFSET_VALUE #define USERMOD_BH1750_OFFSET_VALUE 1 #endif class Usermod_BH1750 : public Usermod { private: int8_t offset = USERMOD_BH1750_OFFSET_VALUE; unsigned long maxReadingInterval = USERMOD_BH1750_MAX_MEASUREMENT_INTERVAL; unsigned long minReadingInterval = USERMOD_BH1750_MIN_MEASUREMENT_INTERVAL; unsigned long lastMeasurement = UINT32_MAX - (USERMOD_BH1750_MAX_MEASUREMENT_INTERVAL - USERMOD_BH1750_FIRST_MEASUREMENT_AT); unsigned long lastSend = UINT32_MAX - (USERMOD_BH1750_MAX_MEASUREMENT_INTERVAL - USERMOD_BH1750_FIRST_MEASUREMENT_AT); // flag to indicate we have finished the first readLightLevel call // allows this library to report to the user how long until the first // measurement bool getLuminanceComplete = false; // flag set at startup bool enabled = true; // strings to reduce flash memory usage (used more than twice) static const char _name[]; static const char _enabled[]; static const char _maxReadInterval[]; static const char _minReadInterval[]; static const char _offset[]; static const char _HomeAssistantDiscovery[]; bool initDone = false; bool sensorFound = false; // Home Assistant and MQTT String mqttLuminanceTopic; bool mqttInitialized = false; bool HomeAssistantDiscovery = true; // Publish Home Assistant Discovery messages BH1750 lightMeter; float lastLux = -1000; // set up Home Assistant discovery entries void _mqttInitialize(); // Create an MQTT Sensor for Home Assistant Discovery purposes, this includes a pointer to the topic that is published to in the Loop. void _createMqttSensor(const String &name, const String &topic, const String &deviceClass, const String &unitOfMeasurement); public: void setup(); void loop(); inline float getIlluminance() { return (float)lastLux; } void addToJsonInfo(JsonObject &root); // (called from set.cpp) stores persistent properties to cfg.json void addToConfig(JsonObject &root); // called before setup() to populate properties from values stored in cfg.json bool readFromConfig(JsonObject &root); inline uint16_t getId() { return USERMOD_ID_BH1750; } }; ================================================ FILE: usermods/BH1750_v2/library.json ================================================ { "name": "BH1750_v2", "build": { "libArchive": false }, "dependencies": { "claws/BH1750":"^1.2.0" } } ================================================ FILE: usermods/BH1750_v2/readme.md ================================================ # BH1750 usermod This usermod will read from an ambient light sensor like the BH1750. The luminance is displayed in both the Info section of the web UI, as well as published to the `/luminance` MQTT topic if enabled. ## Dependencies - Libraries - `claws/BH1750 @^1.2.0` - Data is published over MQTT - make sure you've enabled the MQTT sync interface. ## Compilation To enable, compile with `BH1750` in `custom_usermods` (e.g. in `platformio_override.ini`) ### Configuration Options The following settings can be set at compile-time but are configurable on the usermod menu (except First Measurement time): - `USERMOD_BH1750_MAX_MEASUREMENT_INTERVAL` - the max number of milliseconds between measurements, defaults to 10000ms - `USERMOD_BH1750_MIN_MEASUREMENT_INTERVAL` - the min number of milliseconds between measurements, defaults to 500ms - `USERMOD_BH1750_OFFSET_VALUE` - the offset value to report on, defaults to 1 - `USERMOD_BH1750_FIRST_MEASUREMENT_AT` - the number of milliseconds after boot to take first measurement, defaults to 10000 ms In addition, the Usermod screen allows you to: - enable/disable the usermod - Enable Home Assistant Discovery of usermod - Configure the SCL/SDA pins ## API The following method is available to interact with the usermod from other code modules: - `getIlluminance` read the brightness from the sensor ## Change Log Jul 2022 - Added Home Assistant Discovery - Implemented PinManager to register pins - Made pins configurable in usermod menu - Added API call to read luminance from other modules - Enhanced info-screen outputs - Updated `readme.md` ================================================ FILE: usermods/BME280_v2/BME280_v2.cpp ================================================ // force the compiler to show a warning to confirm that this file is included #warning **** Included USERMOD_BME280 version 2.0 **** #include "wled.h" #include #include // BME280 sensor #include // BME280 extended measurements #ifdef WLED_DISABLE_MQTT #error "This user mod requires MQTT to be enabled." #endif class UsermodBME280 : public Usermod { private: // NOTE: Do not implement any compile-time variables, anything the user needs to configure // should be configurable from the Usermod menu using the methods below // key settings set via usermod menu uint8_t TemperatureDecimals = 0; // Number of decimal places in published temperaure values uint8_t HumidityDecimals = 0; // Number of decimal places in published humidity values uint8_t PressureDecimals = 0; // Number of decimal places in published pressure values uint16_t TemperatureInterval = 5; // Interval to measure temperature (and humidity, dew point if available) in seconds uint16_t PressureInterval = 300; // Interval to measure pressure in seconds BME280I2C::I2CAddr I2CAddress = BME280I2C::I2CAddr_0x76; // i2c address, defaults to 0x76 bool PublishAlways = false; // Publish values even when they have not changed bool UseCelsius = true; // Use Celsius for Reporting bool HomeAssistantDiscovery = false; // Publish Home Assistant Device Information bool enabled = true; // set the default pins based on the architecture, these get overridden by Usermod menu settings #ifdef ESP8266 //uint8_t RST_PIN = 16; // Un-comment for Heltec WiFi-Kit-8 #endif bool initDone = false; BME280I2C bme; uint8_t sensorType; // Measurement timers long timer; long lastTemperatureMeasure = 0; long lastPressureMeasure = 0; // Current sensor values float sensorTemperature; float sensorHumidity; float sensorHeatIndex; float sensorDewPoint; float sensorPressure; String tempScale; // Track previous sensor values float lastTemperature; float lastHumidity; float lastHeatIndex; float lastDewPoint; float lastPressure; // MQTT topic strings for publishing Home Assistant discovery topics bool mqttInitialized = false; // strings to reduce flash memory usage (used more than twice) static const char _name[]; static const char _enabled[]; // Read the BME280/BMP280 Sensor (which one runs depends on whether Celsius or Fahrenheit being set in Usermod Menu) void UpdateBME280Data(int SensorType) { float _temperature, _humidity, _pressure; if (UseCelsius) { BME280::TempUnit tempUnit(BME280::TempUnit_Celsius); EnvironmentCalculations::TempUnit envTempUnit(EnvironmentCalculations::TempUnit_Celsius); BME280::PresUnit presUnit(BME280::PresUnit_hPa); bme.read(_pressure, _temperature, _humidity, tempUnit, presUnit); sensorTemperature = _temperature; sensorHumidity = _humidity; sensorPressure = _pressure; tempScale = F("°C"); if (sensorType == 1) { sensorHeatIndex = EnvironmentCalculations::HeatIndex(_temperature, _humidity, envTempUnit); sensorDewPoint = EnvironmentCalculations::DewPoint(_temperature, _humidity, envTempUnit); } } else { BME280::TempUnit tempUnit(BME280::TempUnit_Fahrenheit); EnvironmentCalculations::TempUnit envTempUnit(EnvironmentCalculations::TempUnit_Fahrenheit); BME280::PresUnit presUnit(BME280::PresUnit_hPa); bme.read(_pressure, _temperature, _humidity, tempUnit, presUnit); sensorTemperature = _temperature; sensorHumidity = _humidity; sensorPressure = _pressure; tempScale = F("°F"); if (sensorType == 1) { sensorHeatIndex = EnvironmentCalculations::HeatIndex(_temperature, _humidity, envTempUnit); sensorDewPoint = EnvironmentCalculations::DewPoint(_temperature, _humidity, envTempUnit); } } } // Procedure to define all MQTT discovery Topics void _mqttInitialize() { char mqttTemperatureTopic[128]; char mqttHumidityTopic[128]; char mqttPressureTopic[128]; char mqttHeatIndexTopic[128]; char mqttDewPointTopic[128]; snprintf_P(mqttTemperatureTopic, 127, PSTR("%s/temperature"), mqttDeviceTopic); snprintf_P(mqttPressureTopic, 127, PSTR("%s/pressure"), mqttDeviceTopic); snprintf_P(mqttHumidityTopic, 127, PSTR("%s/humidity"), mqttDeviceTopic); snprintf_P(mqttHeatIndexTopic, 127, PSTR("%s/heat_index"), mqttDeviceTopic); snprintf_P(mqttDewPointTopic, 127, PSTR("%s/dew_point"), mqttDeviceTopic); if (HomeAssistantDiscovery) { _createMqttSensor(F("Temperature"), mqttTemperatureTopic, "temperature", tempScale); _createMqttSensor(F("Pressure"), mqttPressureTopic, "pressure", F("hPa")); _createMqttSensor(F("Humidity"), mqttHumidityTopic, "humidity", F("%")); _createMqttSensor(F("HeatIndex"), mqttHeatIndexTopic, "temperature", tempScale); _createMqttSensor(F("DewPoint"), mqttDewPointTopic, "temperature", tempScale); } } // Create an MQTT Sensor for Home Assistant Discovery purposes, this includes a pointer to the topic that is published to in the Loop. void _createMqttSensor(const String &name, const String &topic, const String &deviceClass, const String &unitOfMeasurement) { String t = String(F("homeassistant/sensor/")) + mqttClientID + F("/") + name + F("/config"); StaticJsonDocument<600> doc; doc[F("name")] = String(serverDescription) + " " + name; doc[F("state_topic")] = topic; doc[F("unique_id")] = String(mqttClientID) + name; if (unitOfMeasurement != "") doc[F("unit_of_measurement")] = unitOfMeasurement; if (deviceClass != "") doc[F("device_class")] = deviceClass; doc[F("expire_after")] = 1800; JsonObject device = doc.createNestedObject(F("device")); // attach the sensor to the same device device[F("name")] = serverDescription; device[F("identifiers")] = "wled-sensor-" + String(mqttClientID); device[F("manufacturer")] = F(WLED_BRAND); device[F("model")] = F(WLED_PRODUCT_NAME); device[F("sw_version")] = versionString; String temp; serializeJson(doc, temp); DEBUG_PRINTLN(t); DEBUG_PRINTLN(temp); mqtt->publish(t.c_str(), 0, true, temp.c_str()); } void publishMqtt(const char *topic, const char* state) { //Check if MQTT Connected, otherwise it will crash the 8266 if (WLED_MQTT_CONNECTED){ char subuf[128]; snprintf_P(subuf, 127, PSTR("%s/%s"), mqttDeviceTopic, topic); mqtt->publish(subuf, 0, false, state); } } void initializeBmeComms() { BME280I2C::Settings settings{ BME280::OSR_X16, // Temperature oversampling x16 BME280::OSR_X16, // Humidity oversampling x16 BME280::OSR_X16, // Pressure oversampling x16 BME280::Mode_Forced, BME280::StandbyTime_1000ms, BME280::Filter_Off, BME280::SpiEnable_False, I2CAddress }; bme.setSettings(settings); if (!bme.begin()) { sensorType = 0; DEBUG_PRINTLN(F("Could not find BME280 I2C sensor!")); } else { switch (bme.chipModel()) { case BME280::ChipModel_BME280: sensorType = 1; DEBUG_PRINTLN(F("Found BME280 sensor! Success.")); break; case BME280::ChipModel_BMP280: sensorType = 2; DEBUG_PRINTLN(F("Found BMP280 sensor! No Humidity available.")); break; default: sensorType = 0; DEBUG_PRINTLN(F("Found UNKNOWN sensor! Error!")); } } } public: void setup() { if (i2c_scl<0 || i2c_sda<0) { enabled = false; sensorType = 0; return; } initializeBmeComms(); initDone = true; } void loop() { if (!enabled || strip.isUpdating()) return; // BME280 sensor MQTT publishing // Check if sensor present and Connected, otherwise it will crash the MCU if (sensorType != 0) { // Timer to fetch new temperature, humidity and pressure data at intervals timer = millis(); if (timer - lastTemperatureMeasure >= TemperatureInterval * 1000) { lastTemperatureMeasure = timer; UpdateBME280Data(sensorType); float temperature = roundf(sensorTemperature * powf(10, TemperatureDecimals)) / powf(10, TemperatureDecimals); float humidity, heatIndex, dewPoint; // If temperature has changed since last measure, create string populated with device topic // from the UI and values read from sensor, then publish to broker if (temperature != lastTemperature || PublishAlways) { publishMqtt("temperature", String(temperature, (unsigned) TemperatureDecimals).c_str()); } lastTemperature = temperature; // Update last sensor temperature for next loop if (sensorType == 1) // Only if sensor is a BME280 { humidity = roundf(sensorHumidity * powf(10, HumidityDecimals)) / powf(10, HumidityDecimals); heatIndex = roundf(sensorHeatIndex * powf(10, TemperatureDecimals)) / powf(10, TemperatureDecimals); dewPoint = roundf(sensorDewPoint * powf(10, TemperatureDecimals)) / powf(10, TemperatureDecimals); if (humidity != lastHumidity || PublishAlways) { publishMqtt("humidity", String(humidity, (unsigned) HumidityDecimals).c_str()); } if (heatIndex != lastHeatIndex || PublishAlways) { publishMqtt("heat_index", String(heatIndex, (unsigned) TemperatureDecimals).c_str()); } if (dewPoint != lastDewPoint || PublishAlways) { publishMqtt("dew_point", String(dewPoint, (unsigned) TemperatureDecimals).c_str()); } lastHumidity = humidity; lastHeatIndex = heatIndex; lastDewPoint = dewPoint; } } if (timer - lastPressureMeasure >= PressureInterval * 1000) { lastPressureMeasure = timer; float pressure = roundf(sensorPressure * powf(10, PressureDecimals)) / powf(10, PressureDecimals); if (pressure != lastPressure || PublishAlways) { publishMqtt("pressure", String(pressure, (unsigned) PressureDecimals).c_str()); } lastPressure = pressure; } } } void onMqttConnect(bool sessionPresent) { if (WLED_MQTT_CONNECTED && !mqttInitialized) { _mqttInitialize(); mqttInitialized = true; } } /* * API calls te enable data exchange between WLED modules */ inline float getTemperatureC() { if (UseCelsius) { return (float)roundf(sensorTemperature * powf(10, TemperatureDecimals)) / powf(10, TemperatureDecimals); } else { return (float)roundf(sensorTemperature * powf(10, TemperatureDecimals)) / powf(10, TemperatureDecimals) * 1.8f + 32; } } inline float getTemperatureF() { if (UseCelsius) { return ((float)roundf(sensorTemperature * powf(10, TemperatureDecimals)) / powf(10, TemperatureDecimals) -32) * 0.56f; } else { return (float)roundf(sensorTemperature * powf(10, TemperatureDecimals)) / powf(10, TemperatureDecimals); } } inline float getHumidity() { return (float)roundf(sensorHumidity * powf(10, HumidityDecimals)); } inline float getPressure() { return (float)roundf(sensorPressure * powf(10, PressureDecimals)); } inline float getDewPointC() { if (UseCelsius) { return (float)roundf(sensorDewPoint * powf(10, TemperatureDecimals)) / powf(10, TemperatureDecimals); } else { return (float)roundf(sensorDewPoint * powf(10, TemperatureDecimals)) / powf(10, TemperatureDecimals) * 1.8f + 32; } } inline float getDewPointF() { if (UseCelsius) { return ((float)roundf(sensorDewPoint * powf(10, TemperatureDecimals)) / powf(10, TemperatureDecimals) -32) * 0.56f; } else { return (float)roundf(sensorDewPoint * powf(10, TemperatureDecimals)) / powf(10, TemperatureDecimals); } } inline float getHeatIndexC() { if (UseCelsius) { return (float)roundf(sensorHeatIndex * powf(10, TemperatureDecimals)) / powf(10, TemperatureDecimals); } else { return (float)roundf(sensorHeatIndex * powf(10, TemperatureDecimals)) / powf(10, TemperatureDecimals) * 1.8f + 32; } } inline float getHeatIndexF() { if (UseCelsius) { return ((float)roundf(sensorHeatIndex * powf(10, TemperatureDecimals)) / powf(10, TemperatureDecimals) -32) * 0.56f; } else { return (float)roundf(sensorHeatIndex * powf(10, TemperatureDecimals)) / powf(10, TemperatureDecimals); } } // Publish Sensor Information to Info Page void addToJsonInfo(JsonObject &root) { JsonObject user = root[F("u")]; if (user.isNull()) user = root.createNestedObject(F("u")); if (sensorType==0) //No Sensor { // if we sensor not detected, let the user know JsonArray temperature_json = user.createNestedArray(F("BME/BMP280 Sensor")); temperature_json.add(F("Not Found")); } else if (sensorType==2) //BMP280 { JsonArray temperature_json = user.createNestedArray(F("Temperature")); JsonArray pressure_json = user.createNestedArray(F("Pressure")); temperature_json.add(roundf(sensorTemperature * powf(10, TemperatureDecimals)) / powf(10, TemperatureDecimals)); temperature_json.add(tempScale); pressure_json.add(roundf(sensorPressure * powf(10, PressureDecimals)) / powf(10, PressureDecimals)); pressure_json.add(F("hPa")); } else if (sensorType==1) //BME280 { JsonArray temperature_json = user.createNestedArray(F("Temperature")); JsonArray humidity_json = user.createNestedArray(F("Humidity")); JsonArray pressure_json = user.createNestedArray(F("Pressure")); JsonArray heatindex_json = user.createNestedArray(F("Heat Index")); JsonArray dewpoint_json = user.createNestedArray(F("Dew Point")); temperature_json.add(roundf(sensorTemperature * powf(10, TemperatureDecimals)) / powf(10, TemperatureDecimals)); temperature_json.add(tempScale); humidity_json.add(roundf(sensorHumidity * powf(10, HumidityDecimals)) / powf(10, HumidityDecimals)); humidity_json.add(F("%")); pressure_json.add(roundf(sensorPressure * powf(10, PressureDecimals)) / powf(10, PressureDecimals)); pressure_json.add(F("hPa")); heatindex_json.add(roundf(sensorHeatIndex * powf(10, TemperatureDecimals)) / powf(10, TemperatureDecimals)); heatindex_json.add(tempScale); dewpoint_json.add(roundf(sensorDewPoint * powf(10, TemperatureDecimals)) / powf(10, TemperatureDecimals)); dewpoint_json.add(tempScale); } return; } // Save Usermod Config Settings void addToConfig(JsonObject& root) { JsonObject top = root.createNestedObject(FPSTR(_name)); top[FPSTR(_enabled)] = enabled; top[F("I2CAddress")] = static_cast(I2CAddress); top[F("TemperatureDecimals")] = TemperatureDecimals; top[F("HumidityDecimals")] = HumidityDecimals; top[F("PressureDecimals")] = PressureDecimals; top[F("TemperatureInterval")] = TemperatureInterval; top[F("PressureInterval")] = PressureInterval; top[F("PublishAlways")] = PublishAlways; top[F("UseCelsius")] = UseCelsius; top[F("HomeAssistantDiscovery")] = HomeAssistantDiscovery; DEBUG_PRINTLN(F("BME280 config saved.")); } // Read Usermod Config Settings bool readFromConfig(JsonObject& root) { // default settings values could be set here (or below using the 3-argument getJsonValue()) instead of in the class definition or constructor // setting them inside readFromConfig() is slightly more robust, handling the rare but plausible use case of single value being missing after boot (e.g. if the cfg.json was manually edited and a value was removed) JsonObject top = root[FPSTR(_name)]; if (top.isNull()) { DEBUG_PRINT(F(_name)); DEBUG_PRINTLN(F(": No config found. (Using defaults.)")); return false; } bool configComplete = !top.isNull(); configComplete &= getJsonValue(top[FPSTR(_enabled)], enabled); // A 3-argument getJsonValue() assigns the 3rd argument as a default value if the Json value is missing uint8_t tmpI2cAddress; configComplete &= getJsonValue(top[F("I2CAddress")], tmpI2cAddress, 0x76); I2CAddress = static_cast(tmpI2cAddress); configComplete &= getJsonValue(top[F("TemperatureDecimals")], TemperatureDecimals, 1); configComplete &= getJsonValue(top[F("HumidityDecimals")], HumidityDecimals, 0); configComplete &= getJsonValue(top[F("PressureDecimals")], PressureDecimals, 0); configComplete &= getJsonValue(top[F("TemperatureInterval")], TemperatureInterval, 30); configComplete &= getJsonValue(top[F("PressureInterval")], PressureInterval, 30); configComplete &= getJsonValue(top[F("PublishAlways")], PublishAlways, false); configComplete &= getJsonValue(top[F("UseCelsius")], UseCelsius, true); configComplete &= getJsonValue(top[F("HomeAssistantDiscovery")], HomeAssistantDiscovery, false); DEBUG_PRINT(FPSTR(_name)); if (!initDone) { // first run: reading from cfg.json DEBUG_PRINTLN(F(" config loaded.")); } else { // changing parameters from settings page DEBUG_PRINTLN(F(" config (re)loaded.")); // Reset all known values sensorType = 0; sensorTemperature = 0; sensorHumidity = 0; sensorHeatIndex = 0; sensorDewPoint = 0; sensorPressure = 0; lastTemperature = 0; lastHumidity = 0; lastHeatIndex = 0; lastDewPoint = 0; lastPressure = 0; initializeBmeComms(); } return configComplete; } uint16_t getId() { return USERMOD_ID_BME280; } }; const char UsermodBME280::_name[] PROGMEM = "BME280/BMP280"; const char UsermodBME280::_enabled[] PROGMEM = "enabled"; static UsermodBME280 bme280_v2; REGISTER_USERMOD(bme280_v2); ================================================ FILE: usermods/BME280_v2/README.md ================================================ # Usermod BME280 This Usermod is designed to read a `BME280` or `BMP280` sensor and output the following: - Temperature - Humidity (`BME280` only) - Pressure - Heat Index (`BME280` only) - Dew Point (`BME280` only) Configuration is performed via the Usermod menu. There are no parameters to set in code! The following settings can be configured in the Usermod Menu: - The i2c address in decimal. Set it to either 118 (0x76, the default) or 119 (0x77). - Temperature Decimals (number of decimal places to output) - Humidity Decimals - Pressure Decimals - Temperature Interval (how many seconds between temperature and humidity measurements) - Pressure Interval - Publish Always (turn off to only publish changes, on to publish whether or not value changed) - Use Celsius (turn off to use Fahrenheit) - Home Assistant Discovery (turn on to sent MQTT Discovery entries for Home Assistant) - SCL/SDA GPIO Pins Dependencies - Libraries - `BME280@~3.0.0` (by [finitespace](https://github.com/finitespace/BME280)) - `Wire` - Data is published over MQTT - make sure you've enabled the MQTT sync interface. - This usermod also writes to serial (GPIO1 on ESP8266). Please make sure nothing else is listening to the serial TX pin or your board will get confused by log messages! In addition to outputting via MQTT, you can read the values from the Info Screen on the dashboard page of the device's web interface. Methods also exist to read the read/calculated values from other WLED modules through code. - `getTemperatureC()` - `getTemperatureF()` - `getHumidity()` - `getPressure()` - `getDewPointC()` - `getDewPointF()` - `getHeatIndexC()` - `getHeatIndexF()` # Compiling To enable, add `BME280_v2` to your `custom_usermods` (e.g. in `platformio_override.ini`) ```ini [env:usermod_bme280_d1_mini] extends = env:d1_mini custom_usermods = ${env:d1_mini.custom_usermods} BME280_v2 ``` # MQTT MQTT topics are as follows (`` is set in MQTT section of Sync Setup menu): Measurement type | MQTT topic --- | --- Temperature | `/temperature` Humidity | `/humidity` Pressure | `/pressure` Heat index | `/heat_index` Dew point | `/dew_point` If you are using Home Assistant, and `Home Assistant Discovery` is turned on, Home Assistant should automatically detect a new device, provided you have the MQTT integration installed. The device is separate from the main WLED device and will contain sensors for Pressure, Humidity, Temperature, Dew Point and Heat Index. # Revision History Jul 2022 - Added Home Assistant Discovery - Added API interface to output data - Removed compile-time variables - Added usermod menu interface - Added value outputs to info screen - Updated `readme.md` - Registered usermod - Implemented PinManager for usermod - Implemented reallocation of pins without reboot Apr 2021 - Added `Publish Always` option Dec 2020 - Ported to V2 Usermod format - Customizable `measure intervals` - Customizable number of `decimal places` in published sensor values - Pressure measured in units of hPa instead of Pa - Calculation of heat index (apparent temperature) and dew point - `16x oversampling` of sensor during measurement - Values only published if they are different from the previous value ================================================ FILE: usermods/BME280_v2/library.json ================================================ { "name": "BME280_v2", "build": { "libArchive": false }, "dependencies": { "finitespace/BME280":"~3.0.0" } } ================================================ FILE: usermods/BME68X_v2/BME68X_v2.cpp ================================================ /** * @file usermod_BMW68X.cpp * @author Gabriel A. Sieben (GeoGab) * @brief Usermod for WLED to implement the BME680/BME688 sensor * @version 1.0.2 * @date 28 March 2025 */ #define UMOD_DEVICE "ESP32" // NOTE - Set your hardware here #define HARDWARE_VERSION "1.0" // NOTE - Set your hardware version here #define UMOD_BME680X_SW_VERSION "1.0.2" // NOTE - Version of the User Mod #define CALIB_FILE_NAME "/BME680X-Calib.hex" // NOTE - Calibration file name #define UMOD_NAME "BME680X" // NOTE - User module name #define UMOD_DEBUG_NAME "UM-BME680X: " // NOTE - Debug print module name addon #define ESC "\033" #define ESC_CSI ESC "[" #define ESC_STYLE_RESET ESC_CSI "0m" #define ESC_CURSOR_COLUMN(n) ESC_CSI #n "G" #define ESC_FGCOLOR_BLACK ESC_CSI "30m" #define ESC_FGCOLOR_RED ESC_CSI "31m" #define ESC_FGCOLOR_GREEN ESC_CSI "32m" #define ESC_FGCOLOR_YELLOW ESC_CSI "33m" #define ESC_FGCOLOR_BLUE ESC_CSI "34m" #define ESC_FGCOLOR_MAGENTA ESC_CSI "35m" #define ESC_FGCOLOR_CYAN ESC_CSI "36m" #define ESC_FGCOLOR_WHITE ESC_CSI "37m" #define ESC_FGCOLOR_DEFAULT ESC_CSI "39m" /* Debug Print Special Text */ #define INFO_COLUMN ESC_CURSOR_COLUMN(60) #define GOGAB_OK INFO_COLUMN "[" ESC_FGCOLOR_GREEN "OK" ESC_STYLE_RESET "]" #define GOGAB_FAIL INFO_COLUMN "[" ESC_FGCOLOR_RED "FAIL" ESC_STYLE_RESET "]" #define GOGAB_WARN INFO_COLUMN "[" ESC_FGCOLOR_YELLOW "WARN" ESC_STYLE_RESET "]" #define GOGAB_DONE INFO_COLUMN "[" ESC_FGCOLOR_CYAN "DONE" ESC_STYLE_RESET "]" #include "bsec.h" // Bosch sensor library #include "wled.h" #include /* UsermodBME68X class definition */ class UsermodBME68X : public Usermod { public: /* Public: Functions */ uint16_t getId(); void loop(); // Loop of the user module called by wled main in loop void setup(); // Setup of the user module called by wled main void addToConfig(JsonObject& root); // Extends the settings/user module settings page to include the user module requirements. The settings are written from the wled core to the configuration file. void appendConfigData(); // Adds extra info to the config page of weld bool readFromConfig(JsonObject& root); // Reads config values void addToJsonInfo(JsonObject& root); // Adds user module info to the weld info page /* Wled internal functions which can be used by the core or other user mods */ inline float getTemperature(); // Get Temperature in the selected scale of °C or °F inline float getHumidity(); // ... inline float getPressure(); inline float getGasResistance(); inline float getAbsoluteHumidity(); inline float getDewPoint(); inline float getIaq(); inline float getStaticIaq(); inline float getCo2(); inline float getVoc(); inline float getGasPerc(); inline uint8_t getIaqAccuracy(); inline uint8_t getStaticIaqAccuracy(); inline uint8_t getCo2Accuracy(); inline uint8_t getVocAccuracy(); inline uint8_t getGasPercAccuracy(); inline bool getStabStatus(); inline bool getRunInStatus(); private: /* Private: Functions */ void HomeAssistantDiscovery(); void MQTT_PublishHASensor(const String& name, const String& deviceClass, const String& unitOfMeasurement, const int8_t& digs, const uint8_t& option = 0); void MQTT_publish(const char* topic, const float& value, const int8_t& dig); void onMqttConnect(bool sessionPresent); void checkIaqSensorStatus(); void InfoHelper(JsonObject& root, const char* name, const float& sensorvalue, const int8_t& decimals, const char* unit); void InfoHelper(JsonObject& root, const char* name, const String& sensorvalue, const bool& status); void loadState(); void saveState(); void getValues(); /*** V A R I A B L E s & C O N S T A N T s ***/ /* Private: Settings of Usermod BME68X */ struct settings_t { bool enabled; // true if user module is active byte I2cadress; // Depending on the manufacturer, the BME680 has the address 0x76 or 0x77 uint8_t Interval; // Interval of reading sensor data in seconds uint16_t MaxAge; // Force the publication of the value of a sensor after these defined seconds at the latest bool pubAcc; // Publish the accuracy values bool publishSensorState; // Publisch the sensor calibration state bool publishAfterCalibration ; // The IAQ/CO2/VOC/GAS value are only valid after the sensor has been calibrated. If this switch is active, the values are only sent after calibration bool PublischChange; // Publish values even when they have not changed bool PublishIAQVerbal; // Publish Index of Air Quality (IAQ) classification Verbal bool PublishStaticIAQVerbal; // Publish Static Index of Air Quality (Static IAQ) Verbal byte tempScale; // 0 -> Use Celsius, 1-> Use Fahrenheit float tempOffset; // Temperature Offset bool HomeAssistantDiscovery; // Publish Home Assistant Device Information bool pauseOnActiveWled ; // If this is set to true, the user mod ist not executed while wled is active /* Decimal Places (-1 means inactive) */ struct decimals_t { int8_t temperature; int8_t humidity; int8_t pressure; int8_t gasResistance; int8_t absHumidity; int8_t drewPoint; int8_t iaq; int8_t staticIaq; int8_t co2; int8_t Voc; int8_t gasPerc; } decimals; } settings; /* Private: Flags */ struct flags_t { bool InitSuccessful = false; // Initialation was un-/successful bool MqttInitialized = false; // MQTT Initialation done flag (first MQTT Connect) bool SaveState = false; // Save the calibration data flag bool DeleteCaibration = false; // If set the calib file will be deleted on the next round } flags; /* Private: Measurement timers */ struct timer_t { long actual; // Actual time stamp long lastRun; // Last measurement time stamp } timer; /* Private: Various variables */ String stringbuff; // General string stringbuff buffer char charbuffer[128]; // General char stringbuff buffer String InfoPageStatusLine; // Shown on the info page of WLED String tempScale; // °C or °F uint8_t bsecState[BSEC_MAX_STATE_BLOB_SIZE]; // Calibration data array uint16_t stateUpdateCounter; // Save state couter static const uint8_t bsec_config_iaq[]; // Calibration Buffer Bsec iaqSensor; // Sensor variable /* Private: Sensor values */ struct values_t { float temperature; // Temp [°C] (Sensor-compensated) float humidity; // Relative humidity [%] (Sensor-compensated) float pressure; // raw pressure [hPa] float gasResistance; // raw gas restistance [Ohm] float absHumidity; // UserMod calculated: Absolute Humidity [g/m³] float drewPoint; // UserMod calculated: drew point [°C/°F] float iaq; // IAQ (Indoor Air Quallity) float staticIaq; // Satic IAQ float co2; // CO2 [PPM] float Voc; // VOC in [PPM] float gasPerc; // Gas Percentage in [%] uint8_t iaqAccuracy; // IAQ accuracy - IAQ Accuracy = 1 means value is inaccurate, IAQ Accuracy = 2 means sensor is being calibrated, IAQ Accuracy = 3 means sensor successfully calibrated. uint8_t staticIaqAccuracy; // Static IAQ accuracy uint8_t co2Accuracy; // co2 accuracy uint8_t VocAccuracy; // voc accuracy uint8_t gasPercAccuracy; // Gas percentage accuracy bool stabStatus; // Indicates if the sensor is undergoing initial stabilization during its first use after production bool runInStatus; // Indicates when the sensor is ready after after switch-on } valuesA, valuesB, *ValuesPtr, *PrevValuesPtr, *swap; // Data Scructur A, Data Structur B, Pointers to switch between data channel A & B struct cvalues_t { String iaqVerbal; // IAQ verbal String staticIaqVerbal; // Static IAQ verbal } cvalues; /* Private: Sensor settings */ bsec_virtual_sensor_t sensorList[13] = { BSEC_OUTPUT_IAQ, // Index for Air Quality estimate [0-500] Index for Air Quality (IAQ) gives an indication of the relative change in ambient TVOCs detected by BME680. BSEC_OUTPUT_STATIC_IAQ, // Unscaled Index for Air Quality estimate BSEC_OUTPUT_CO2_EQUIVALENT, // CO2 equivalent estimate [ppm] BSEC_OUTPUT_BREATH_VOC_EQUIVALENT, // Breath VOC concentration estimate [ppm] BSEC_OUTPUT_RAW_TEMPERATURE, // Temperature sensor signal [degrees Celsius] Temperature directly measured by BME680 in degree Celsius. This value is cross-influenced by the sensor heating and device specific heating. BSEC_OUTPUT_RAW_PRESSURE, // Pressure sensor signal [Pa] Pressure directly measured by the BME680 in Pa. BSEC_OUTPUT_RAW_HUMIDITY, // Relative humidity sensor signal [%] Relative humidity directly measured by the BME680 in %. This value is cross-influenced by the sensor heating and device specific heating. BSEC_OUTPUT_RAW_GAS, // Gas sensor signal [Ohm] Gas resistance measured directly by the BME680 in Ohm.The resistance value changes due to varying VOC concentrations (the higher the concentration of reducing VOCs, the lower the resistance and vice versa). BSEC_OUTPUT_STABILIZATION_STATUS, // Gas sensor stabilization status [boolean] Indicates initial stabilization status of the gas sensor element: stabilization is ongoing (0) or stabilization is finished (1). BSEC_OUTPUT_RUN_IN_STATUS, // Gas sensor run-in status [boolean] Indicates power-on stabilization status of the gas sensor element: stabilization is ongoing (0) or stabilization is finished (1) BSEC_OUTPUT_SENSOR_HEAT_COMPENSATED_TEMPERATURE, // Sensor heat compensated temperature [degrees Celsius] Temperature measured by BME680 which is compensated for the influence of sensor (heater) in degree Celsius. The self heating introduced by the heater is depending on the sensor operation mode and the sensor supply voltage. BSEC_OUTPUT_SENSOR_HEAT_COMPENSATED_HUMIDITY, // Sensor heat compensated humidity [%] Relative measured by BME680 which is compensated for the influence of sensor (heater) in %. It converts the ::BSEC_INPUT_HUMIDITY from temperature ::BSEC_INPUT_TEMPERATURE to temperature ::BSEC_OUTPUT_SENSOR_HEAT_COMPENSATED_TEMPERATURE. BSEC_OUTPUT_GAS_PERCENTAGE // Percentage of min and max filtered gas value [%] }; /*** V A R I A B L E s & C O N S T A N T s ***/ /* Public: strings to reduce flash memory usage (used more than twice) */ static const char _enabled[]; static const char _hadtopic[]; /* Public: Settings Strings*/ static const char _nameI2CAdr[]; static const char _nameInterval[]; static const char _nameMaxAge[]; static const char _namePubAc[]; static const char _namePubSenState[]; static const char _namePubAfterCalib[]; static const char _namePublishChange[]; static const char _nameTempScale[]; static const char _nameTempOffset[]; static const char _nameHADisc[]; static const char _nameDelCalib[]; /* Public: Sensor names / Sensor short names */ static const char _nameTemp[]; static const char _nameHum[]; static const char _namePress[]; static const char _nameGasRes[]; static const char _nameAHum[]; static const char _nameDrewP[]; static const char _nameIaq[]; static const char _nameIaqAc[]; static const char _nameIaqVerb[]; static const char _nameStaticIaq[]; static const char _nameStaticIaqVerb[]; static const char _nameStaticIaqAc[]; static const char _nameCo2[]; static const char _nameCo2Ac[]; static const char _nameVoc[]; static const char _nameVocAc[]; static const char _nameComGasAc[]; static const char _nameGasPer[]; static const char _nameGasPerAc[]; static const char _namePauseOnActWL[]; static const char _nameStabStatus[]; static const char _nameRunInStatus[]; /* Public: Sensor Units */ static const char _unitTemp[]; static const char _unitHum[]; static const char _unitPress[]; static const char _unitGasres[]; static const char _unitAHum[]; static const char _unitDrewp[]; static const char _unitIaq[]; static const char _unitStaticIaq[]; static const char _unitCo2[]; static const char _unitVoc[]; static const char _unitGasPer[]; static const char _unitNone[]; static const char _unitCelsius[]; static const char _unitFahrenheit[]; }; // UsermodBME68X class definition End /*** Setting C O N S T A N T S ***/ /* Private: Settings Strings*/ const char UsermodBME68X::_enabled[] PROGMEM = "Enabled"; const char UsermodBME68X::_hadtopic[] PROGMEM = "homeassistant/sensor/"; const char UsermodBME68X::_nameI2CAdr[] PROGMEM = "i2C Address"; const char UsermodBME68X::_nameInterval[] PROGMEM = "Interval"; const char UsermodBME68X::_nameMaxAge[] PROGMEM = "Max Age"; const char UsermodBME68X::_namePublishChange[] PROGMEM = "Pub changes only"; const char UsermodBME68X::_namePubAc[] PROGMEM = "Pub Accuracy"; const char UsermodBME68X::_namePubSenState[] PROGMEM = "Pub Calib State"; const char UsermodBME68X::_namePubAfterCalib[] PROGMEM = "Pub After Calib"; const char UsermodBME68X::_nameTempScale[] PROGMEM = "Temp Scale"; const char UsermodBME68X::_nameTempOffset[] PROGMEM = "Temp Offset"; const char UsermodBME68X::_nameHADisc[] PROGMEM = "HA Discovery"; const char UsermodBME68X::_nameDelCalib[] PROGMEM = "Del Calibration Hist"; const char UsermodBME68X::_namePauseOnActWL[] PROGMEM = "Pause while WLED active"; /* Private: Sensor names / Sensor short name */ const char UsermodBME68X::_nameTemp[] PROGMEM = "Temperature"; const char UsermodBME68X::_nameHum[] PROGMEM = "Humidity"; const char UsermodBME68X::_namePress[] PROGMEM = "Pressure"; const char UsermodBME68X::_nameGasRes[] PROGMEM = "Gas-Resistance"; const char UsermodBME68X::_nameAHum[] PROGMEM = "Absolute-Humidity"; const char UsermodBME68X::_nameDrewP[] PROGMEM = "Drew-Point"; const char UsermodBME68X::_nameIaq[] PROGMEM = "IAQ"; const char UsermodBME68X::_nameIaqVerb[] PROGMEM = "IAQ-Verbal"; const char UsermodBME68X::_nameStaticIaq[] PROGMEM = "Static-IAQ"; const char UsermodBME68X::_nameStaticIaqVerb[] PROGMEM = "Static-IAQ-Verbal"; const char UsermodBME68X::_nameCo2[] PROGMEM = "CO2"; const char UsermodBME68X::_nameVoc[] PROGMEM = "VOC"; const char UsermodBME68X::_nameGasPer[] PROGMEM = "Gas-Percentage"; const char UsermodBME68X::_nameIaqAc[] PROGMEM = "IAQ-Accuracy"; const char UsermodBME68X::_nameStaticIaqAc[] PROGMEM = "Static-IAQ-Accuracy"; const char UsermodBME68X::_nameCo2Ac[] PROGMEM = "CO2-Accuracy"; const char UsermodBME68X::_nameVocAc[] PROGMEM = "VOC-Accuracy"; const char UsermodBME68X::_nameGasPerAc[] PROGMEM = "Gas-Percentage-Accuracy"; const char UsermodBME68X::_nameStabStatus[] PROGMEM = "Stab-Status"; const char UsermodBME68X::_nameRunInStatus[] PROGMEM = "Run-In-Status"; /* Private Units */ const char UsermodBME68X::_unitTemp[] PROGMEM = " "; // NOTE - Is set with the selectable temperature unit const char UsermodBME68X::_unitHum[] PROGMEM = "%"; const char UsermodBME68X::_unitPress[] PROGMEM = "hPa"; const char UsermodBME68X::_unitGasres[] PROGMEM = "kΩ"; const char UsermodBME68X::_unitAHum[] PROGMEM = "g/m³"; const char UsermodBME68X::_unitDrewp[] PROGMEM = " "; // NOTE - Is set with the selectable temperature unit const char UsermodBME68X::_unitIaq[] PROGMEM = " "; // No unit const char UsermodBME68X::_unitStaticIaq[] PROGMEM = " "; // No unit const char UsermodBME68X::_unitCo2[] PROGMEM = "ppm"; const char UsermodBME68X::_unitVoc[] PROGMEM = "ppm"; const char UsermodBME68X::_unitGasPer[] PROGMEM = "%"; const char UsermodBME68X::_unitNone[] PROGMEM = ""; const char UsermodBME68X::_unitCelsius[] PROGMEM = "°C"; // Symbol for Celsius const char UsermodBME68X::_unitFahrenheit[] PROGMEM = "°F"; // Symbol for Fahrenheit /* Load Sensor Settings */ const uint8_t UsermodBME68X::bsec_config_iaq[] = { #include "config/generic_33v_3s_28d/bsec_iaq.txt" // Allow 28 days for calibration because the WLED module normally stays in the same place anyway }; /************************************************************************************************************/ /********************************************* M A I N C O D E *********************************************/ /************************************************************************************************************/ /** * @brief Called by WLED: Setup of the usermod */ void UsermodBME68X::setup() { DEBUG_PRINTLN(F(UMOD_DEBUG_NAME ESC_FGCOLOR_CYAN "Initialize" ESC_STYLE_RESET)); /* Check, if i2c is activated */ if (i2c_scl < 0 || i2c_sda < 0) { settings.enabled = false; // Disable usermod once i2c is not running DEBUG_PRINTLN(F(UMOD_DEBUG_NAME "I2C is not activated. Please activate I2C first." GOGAB_FAIL)); return; } flags.InitSuccessful = true; // Will be set to false on need /* Set data structure pointers */ ValuesPtr = &valuesA; PrevValuesPtr = &valuesB; /* Init Library*/ iaqSensor.begin(settings.I2cadress, Wire); // BME68X_I2C_ADDR_LOW stringbuff = "BSEC library version " + String(iaqSensor.version.major) + "." + String(iaqSensor.version.minor) + "." + String(iaqSensor.version.major_bugfix) + "." + String(iaqSensor.version.minor_bugfix); DEBUG_PRINT(F(UMOD_NAME)); DEBUG_PRINTLN(F(stringbuff.c_str())); /* Init Sensor*/ iaqSensor.setConfig(bsec_config_iaq); iaqSensor.updateSubscription(sensorList, 13, BSEC_SAMPLE_RATE_LP); iaqSensor.setTPH(BME68X_OS_2X, BME68X_OS_16X, BME68X_OS_1X); // Set the temperature, Pressure and Humidity over-sampling iaqSensor.setTemperatureOffset(settings.tempOffset); // set the temperature offset in degree Celsius loadState(); // Load the old calibration data checkIaqSensorStatus(); // Check the sensor status // HomeAssistantDiscovery(); DEBUG_PRINTLN(F(INFO_COLUMN GOGAB_DONE)); } /** * @brief Called by WLED: Main loop called by WLED * */ void UsermodBME68X::loop() { if (!settings.enabled || strip.isUpdating() || !flags.InitSuccessful) return; // Leave if not enabled or string is updating or init failed if (settings.pauseOnActiveWled && strip.getBrightness()) return; // Workarround Known Issue: handing led update - Leave once pause on activ wled is active and wled is active timer.actual = millis(); // Timer to fetch new temperature, humidity and pressure data at intervals if (timer.actual - timer.lastRun >= settings.Interval * 1000) { timer.lastRun = timer.actual; /* Get the sonsor measurments and publish them */ if (iaqSensor.run()) { // iaqSensor.run() getValues(); // Get the new values if (ValuesPtr->temperature != PrevValuesPtr->temperature || !settings.PublischChange) { // NOTE - negative dig means inactive MQTT_publish(_nameTemp, ValuesPtr->temperature, settings.decimals.temperature); } if (ValuesPtr->humidity != PrevValuesPtr->humidity || !settings.PublischChange) { MQTT_publish(_nameHum, ValuesPtr->humidity, settings.decimals.humidity); } if (ValuesPtr->pressure != PrevValuesPtr->pressure || !settings.PublischChange) { MQTT_publish(_namePress, ValuesPtr->pressure, settings.decimals.humidity); } if (ValuesPtr->gasResistance != PrevValuesPtr->gasResistance || !settings.PublischChange) { MQTT_publish(_nameGasRes, ValuesPtr->gasResistance, settings.decimals.gasResistance); } if (ValuesPtr->absHumidity != PrevValuesPtr->absHumidity || !settings.PublischChange) { MQTT_publish(_nameAHum, PrevValuesPtr->absHumidity, settings.decimals.absHumidity); } if (ValuesPtr->drewPoint != PrevValuesPtr->drewPoint || !settings.PublischChange) { MQTT_publish(_nameDrewP, PrevValuesPtr->drewPoint, settings.decimals.drewPoint); } if (ValuesPtr->iaq != PrevValuesPtr->iaq || !settings.PublischChange) { MQTT_publish(_nameIaq, ValuesPtr->iaq, settings.decimals.iaq); if (settings.pubAcc) MQTT_publish(_nameIaqAc, ValuesPtr->iaqAccuracy, 0); if (settings.decimals.iaq>-1) { if (settings.PublishIAQVerbal) { if (ValuesPtr->iaq <= 50) cvalues.iaqVerbal = F("Excellent"); else if (ValuesPtr->iaq <= 100) cvalues.iaqVerbal = F("Good"); else if (ValuesPtr->iaq <= 150) cvalues.iaqVerbal = F("Lightly polluted"); else if (ValuesPtr->iaq <= 200) cvalues.iaqVerbal = F("Moderately polluted"); else if (ValuesPtr->iaq <= 250) cvalues.iaqVerbal = F("Heavily polluted"); else if (ValuesPtr->iaq <= 350) cvalues.iaqVerbal = F("Severely polluted"); else cvalues.iaqVerbal = F("Extremely polluted"); snprintf_P(charbuffer, 127, PSTR("%s/%s"), mqttDeviceTopic, _nameIaqVerb); if (WLED_MQTT_CONNECTED) mqtt->publish(charbuffer, 0, false, cvalues.iaqVerbal.c_str()); } } } if (ValuesPtr->staticIaq != PrevValuesPtr->staticIaq || !settings.PublischChange) { MQTT_publish(_nameStaticIaq, ValuesPtr->staticIaq, settings.decimals.staticIaq); if (settings.pubAcc) MQTT_publish(_nameStaticIaqAc, ValuesPtr->staticIaqAccuracy, 0); if (settings.decimals.staticIaq>-1) { if (settings.PublishIAQVerbal) { if (ValuesPtr->staticIaq <= 50) cvalues.staticIaqVerbal = F("Excellent"); else if (ValuesPtr->staticIaq <= 100) cvalues.staticIaqVerbal = F("Good"); else if (ValuesPtr->staticIaq <= 150) cvalues.staticIaqVerbal = F("Lightly polluted"); else if (ValuesPtr->staticIaq <= 200) cvalues.staticIaqVerbal = F("Moderately polluted"); else if (ValuesPtr->staticIaq <= 250) cvalues.staticIaqVerbal = F("Heavily polluted"); else if (ValuesPtr->staticIaq <= 350) cvalues.staticIaqVerbal = F("Severely polluted"); else cvalues.staticIaqVerbal = F("Extremely polluted"); snprintf_P(charbuffer, 127, PSTR("%s/%s"), mqttDeviceTopic, _nameStaticIaqVerb); if (WLED_MQTT_CONNECTED) mqtt->publish(charbuffer, 0, false, cvalues.staticIaqVerbal.c_str()); } } } if (ValuesPtr->co2 != PrevValuesPtr->co2 || !settings.PublischChange) { MQTT_publish(_nameCo2, ValuesPtr->co2, settings.decimals.co2); if (settings.pubAcc) MQTT_publish(_nameCo2Ac, ValuesPtr->co2Accuracy, 0); } if (ValuesPtr->Voc != PrevValuesPtr->Voc || !settings.PublischChange) { MQTT_publish(_nameVoc, ValuesPtr->Voc, settings.decimals.Voc); if (settings.pubAcc) MQTT_publish(_nameVocAc, ValuesPtr->VocAccuracy, 0); } if (ValuesPtr->gasPerc != PrevValuesPtr->gasPerc || !settings.PublischChange) { MQTT_publish(_nameGasPer, ValuesPtr->gasPerc, settings.decimals.gasPerc); if (settings.pubAcc) MQTT_publish(_nameGasPerAc, ValuesPtr->gasPercAccuracy, 0); } /**** Publish Sensor State Entrys *****/ if ((ValuesPtr->stabStatus != PrevValuesPtr->stabStatus || !settings.PublischChange) && settings.publishSensorState) MQTT_publish(_nameStabStatus, ValuesPtr->stabStatus, 0); if ((ValuesPtr->runInStatus != PrevValuesPtr->runInStatus || !settings.PublischChange) && settings.publishSensorState) MQTT_publish(_nameRunInStatus, ValuesPtr->runInStatus, 0); /* Check accuracies - if accurasy level 3 is reached -> save calibration data */ if ((ValuesPtr->iaqAccuracy != PrevValuesPtr->iaqAccuracy) && ValuesPtr->iaqAccuracy == 3) flags.SaveState = true; // Save after calibration / recalibration if ((ValuesPtr->staticIaqAccuracy != PrevValuesPtr->staticIaqAccuracy) && ValuesPtr->staticIaqAccuracy == 3) flags.SaveState = true; if ((ValuesPtr->co2Accuracy != PrevValuesPtr->co2Accuracy) && ValuesPtr->co2Accuracy == 3) flags.SaveState = true; if ((ValuesPtr->VocAccuracy != PrevValuesPtr->VocAccuracy) && ValuesPtr->VocAccuracy == 3) flags.SaveState = true; if ((ValuesPtr->gasPercAccuracy != PrevValuesPtr->gasPercAccuracy) && ValuesPtr->gasPercAccuracy == 3) flags.SaveState = true; if (flags.SaveState) saveState(); // Save if the save state flag is set } } } /** * @brief Retrieves the sensor data and truncates it to the requested decimal places * */ void UsermodBME68X::getValues() { /* Swap the point to the data structures */ swap = PrevValuesPtr; PrevValuesPtr = ValuesPtr; ValuesPtr = swap; /* Float Values */ ValuesPtr->temperature = roundf(iaqSensor.temperature * powf(10, settings.decimals.temperature)) / powf(10, settings.decimals.temperature); ValuesPtr->humidity = roundf(iaqSensor.humidity * powf(10, settings.decimals.humidity)) / powf(10, settings.decimals.humidity); ValuesPtr->pressure = roundf(iaqSensor.pressure * powf(10, settings.decimals.pressure)) / powf(10, settings.decimals.pressure) /100; // Pa 2 hPa ValuesPtr->gasResistance = roundf(iaqSensor.gasResistance * powf(10, settings.decimals.gasResistance)) /powf(10, settings.decimals.gasResistance) /1000; // Ohm 2 KOhm ValuesPtr->iaq = roundf(iaqSensor.iaq * powf(10, settings.decimals.iaq)) / powf(10, settings.decimals.iaq); ValuesPtr->staticIaq = roundf(iaqSensor.staticIaq * powf(10, settings.decimals.staticIaq)) / powf(10, settings.decimals.staticIaq); ValuesPtr->co2 = roundf(iaqSensor.co2Equivalent * powf(10, settings.decimals.co2)) / powf(10, settings.decimals.co2); ValuesPtr->Voc = roundf(iaqSensor.breathVocEquivalent * powf(10, settings.decimals.Voc)) / powf(10, settings.decimals.Voc); ValuesPtr->gasPerc = roundf(iaqSensor.gasPercentage * powf(10, settings.decimals.gasPerc)) / powf(10, settings.decimals.gasPerc); /* Calculate Absolute Humidity [g/m³] */ if (settings.decimals.absHumidity>-1) { const float mw = 18.01534; // molar mass of water g/mol const float r = 8.31447215; // Universal gas constant J/mol/K ValuesPtr->absHumidity = (6.112 * powf(2.718281828, (17.67 * ValuesPtr->temperature) / (ValuesPtr->temperature + 243.5)) * ValuesPtr->humidity * mw) / ((273.15 + ValuesPtr->temperature) * r); // in ppm } /* Calculate Drew Point (C°) */ if (settings.decimals.drewPoint>-1) { ValuesPtr->drewPoint = (243.5 * (log( ValuesPtr->humidity / 100) + ((17.67 * ValuesPtr->temperature) / (243.5 + ValuesPtr->temperature))) / (17.67 - log(ValuesPtr->humidity / 100) - ((17.67 * ValuesPtr->temperature) / (243.5 + ValuesPtr->temperature)))); } /* Convert to Fahrenheit when selected */ if (settings.tempScale) { // settings.tempScale = 0 => Celsius, = 1 => Fahrenheit ValuesPtr->temperature = ValuesPtr->temperature * 1.8 + 32; // Value stored in Fahrenheit ValuesPtr->drewPoint = ValuesPtr->drewPoint * 1.8 + 32; } /* Integer Values */ ValuesPtr->iaqAccuracy = iaqSensor.iaqAccuracy; ValuesPtr->staticIaqAccuracy = iaqSensor.staticIaqAccuracy; ValuesPtr->co2Accuracy = iaqSensor.co2Accuracy; ValuesPtr->VocAccuracy = iaqSensor.breathVocAccuracy; ValuesPtr->gasPercAccuracy = iaqSensor.gasPercentageAccuracy; ValuesPtr->stabStatus = iaqSensor.stabStatus; ValuesPtr->runInStatus = iaqSensor.runInStatus; } /** * @brief Sends the current sensor data via MQTT * @param topic Suptopic of the sensor as const char * @param value Current sensor value as float */ void UsermodBME68X::MQTT_publish(const char* topic, const float& value, const int8_t& dig) { if (dig<0) return; if (WLED_MQTT_CONNECTED) { snprintf_P(charbuffer, 127, PSTR("%s/%s"), mqttDeviceTopic, topic); mqtt->publish(charbuffer, 0, false, String(value, dig).c_str()); } } /** * @brief Called by WLED: Initialize the MQTT parts when the connection to the MQTT server is established. * @param bool Session Present */ void UsermodBME68X::onMqttConnect(bool sessionPresent) { DEBUG_PRINTLN(UMOD_DEBUG_NAME "OnMQTTConnect event fired"); HomeAssistantDiscovery(); if (!flags.MqttInitialized) { flags.MqttInitialized=true; DEBUG_PRINTLN(UMOD_DEBUG_NAME "MQTT first connect"); } } /** * @brief MQTT initialization to generate the mqtt topic strings. This initialization also creates the HomeAssistat device configuration (HA Discovery), which home assinstant automatically evaluates to create a device. */ void UsermodBME68X::HomeAssistantDiscovery() { if (!settings.HomeAssistantDiscovery || !flags.InitSuccessful || !settings.enabled) return; // Leave once HomeAssistant Discovery is inactive DEBUG_PRINTLN(UMOD_DEBUG_NAME ESC_FGCOLOR_CYAN "Creating HomeAssistant Discovery Mqtt-Entrys" ESC_STYLE_RESET); /* Sensor Values */ MQTT_PublishHASensor(_nameTemp, "TEMPERATURE", tempScale.c_str(), settings.decimals.temperature ); // Temperature MQTT_PublishHASensor(_namePress, "ATMOSPHERIC_PRESSURE", _unitPress, settings.decimals.pressure ); // Pressure MQTT_PublishHASensor(_nameHum, "HUMIDITY", _unitHum, settings.decimals.humidity ); // Humidity MQTT_PublishHASensor(_nameGasRes, "GAS", _unitGasres, settings.decimals.gasResistance ); // There is no device class for resistance in HA yet: https://developers.home-assistant.io/docs/core/entity/sensor/ MQTT_PublishHASensor(_nameAHum, "HUMIDITY", _unitAHum, settings.decimals.absHumidity ); // Absolute Humidity MQTT_PublishHASensor(_nameDrewP, "TEMPERATURE", tempScale.c_str(), settings.decimals.drewPoint ); // Drew Point MQTT_PublishHASensor(_nameIaq, "AQI", _unitIaq, settings.decimals.iaq ); // IAQ MQTT_PublishHASensor(_nameIaqVerb, "", _unitNone, settings.PublishIAQVerbal, 2); // IAQ Verbal / Set Option 2 (text sensor) MQTT_PublishHASensor(_nameStaticIaq, "AQI", _unitNone, settings.decimals.staticIaq ); // Static IAQ MQTT_PublishHASensor(_nameStaticIaqVerb, "", _unitNone, settings.PublishStaticIAQVerbal, 2); // IAQ Verbal / Set Option 2 (text sensor MQTT_PublishHASensor(_nameCo2, "CO2", _unitCo2, settings.decimals.co2 ); // CO2 MQTT_PublishHASensor(_nameVoc, "VOLATILE_ORGANIC_COMPOUNDS", _unitVoc, settings.decimals.Voc ); // VOC MQTT_PublishHASensor(_nameGasPer, "AQI", _unitGasPer, settings.decimals.gasPerc ); // Gas % /* Accuracys - switched off once publishAccuracy=0 or the main value is switched of by digs set to a negative number */ MQTT_PublishHASensor(_nameIaqAc, "AQI", _unitNone, settings.pubAcc - 1 + settings.decimals.iaq * settings.pubAcc, 1); // Option 1: Diagnostics Sektion MQTT_PublishHASensor(_nameStaticIaqAc, "", _unitNone, settings.pubAcc - 1 + settings.decimals.staticIaq * settings.pubAcc, 1); MQTT_PublishHASensor(_nameCo2Ac, "", _unitNone, settings.pubAcc - 1 + settings.decimals.co2 * settings.pubAcc, 1); MQTT_PublishHASensor(_nameVocAc, "", _unitNone, settings.pubAcc - 1 + settings.decimals.Voc * settings.pubAcc, 1); MQTT_PublishHASensor(_nameGasPerAc, "", _unitNone, settings.pubAcc - 1 + settings.decimals.gasPerc * settings.pubAcc, 1); MQTT_PublishHASensor(_nameStabStatus, "", _unitNone, settings.publishSensorState - 1, 1); MQTT_PublishHASensor(_nameRunInStatus, "", _unitNone, settings.publishSensorState - 1, 1); DEBUG_PRINTLN(UMOD_DEBUG_NAME GOGAB_DONE); } /** * @brief These MQTT entries are responsible for the Home Assistant Discovery of the sensors. HA is shown here where to look for the sensor data. This entry therefore only needs to be sent once. * Important note: In order to find everything that is sent from this device to Home Assistant via MQTT under the same device name, the "device/identifiers" entry must be the same. * I use the MQTT device name here. If other user mods also use the HA Discovery, it is recommended to set the identifier the same. Otherwise you would have several devices, * even though it is one device. I therefore only use the MQTT client name set in WLED here. * @param name Name of the sensor * @param topic Topic of the live sensor data * @param unitOfMeasurement Unit of the measurment * @param digs Number of decimal places * @param option Set to true if the sensor is part of diagnostics (dafault 0) */ void UsermodBME68X::MQTT_PublishHASensor(const String& name, const String& deviceClass, const String& unitOfMeasurement, const int8_t& digs, const uint8_t& option) { DEBUG_PRINT(UMOD_DEBUG_NAME "\t" + name); snprintf_P(charbuffer, 127, PSTR("%s/%s"), mqttDeviceTopic, name.c_str()); // Current values will be posted here String basetopic = String(_hadtopic) + mqttClientID + F("/") + name + F("/config"); // This is the place where Home Assinstant Discovery will check for new devices if (digs < 0) { // if digs are set to -1 -> entry deactivated /* Delete MQTT Entry */ if (WLED_MQTT_CONNECTED) { mqtt->publish(basetopic.c_str(), 0, true, ""); // Send emty entry to delete DEBUG_PRINTLN(INFO_COLUMN "deleted"); } } else { /* Create all the necessary HAD MQTT entrys - see: https://www.home-assistant.io/integrations/sensor.mqtt/#configuration-variables */ DynamicJsonDocument jdoc(700); // json document // See: https://www.home-assistant.io/integrations/mqtt/ JsonObject avail = jdoc.createNestedObject(F("avty")); // 'avty': 'availability' avail[F("topic")] = mqttDeviceTopic + String("/status"); // An MQTT topic subscribed to receive availability (online/offline) updates. avail[F("payload_available")] = "online"; avail[F("payload_not_available")] = "offline"; JsonObject device = jdoc.createNestedObject(F("device")); // Information about the device this sensor is a part of to tie it into the device registry. Only works when unique_id is set. At least one of identifiers or connections must be present to identify the device. device[F("name")] = serverDescription; device[F("identifiers")] = String(mqttClientID); device[F("manufacturer")] = F("WLED"); device[F("model")] = UMOD_DEVICE; device[F("sw_version")] = versionString; device[F("hw_version")] = F(HARDWARE_VERSION); if (deviceClass != "") jdoc[F("device_class")] = deviceClass; // The type/class of the sensor to set the icon in the frontend. The device_class can be null if (option == 1) jdoc[F("entity_category")] = "diagnostic"; // Option 1: The category of the entity | When set, the entity category must be diagnostic for sensors. if (option == 2) jdoc[F("mode")] = "text"; // Option 2: Set text mode | jdoc[F("expire_after")] = 1800; // If set, it defines the number of seconds after the sensor’s state expires, if it’s not updated. After expiry, the sensor’s state becomes unavailable. Default the sensors state never expires. jdoc[F("name")] = name; // The name of the MQTT sensor. Without server/module/device name. The device name will be added by HomeAssinstant anyhow if (unitOfMeasurement != "") jdoc[F("state_class")] = "measurement"; // NOTE: This entry is missing in some other usermods. But it is very important. Because only with this entry, you can use statistics (such as statistical graphs). jdoc[F("state_topic")] = charbuffer; // The MQTT topic subscribed to receive sensor values. If device_class, state_class, unit_of_measurement or suggested_display_precision is set, and a numeric value is expected, an empty value '' will be ignored and will not update the state, a 'null' value will set the sensor to an unknown state. The device_class can be null. jdoc[F("unique_id")] = String(mqttClientID) + "-" + name; // An ID that uniquely identifies this sensor. If two sensors have the same unique ID, Home Assistant will raise an exception. if (unitOfMeasurement != "") jdoc[F("unit_of_measurement")] = unitOfMeasurement; // Defines the units of measurement of the sensor, if any. The unit_of_measurement can be null. DEBUG_PRINTF(" (%d bytes)", jdoc.memoryUsage()); stringbuff = ""; // clear string buffer serializeJson(jdoc, stringbuff); // JSON to String if (WLED_MQTT_CONNECTED) { // Check if MQTT Connected, otherwise it will crash the 8266 mqtt->publish(basetopic.c_str(), 0, true, stringbuff.c_str()); // Publish the HA discovery sensor entry DEBUG_PRINTLN(INFO_COLUMN "published"); } } } /** * @brief Called by WLED: Publish Sensor Information to Info Page * @param JsonObject Pointer */ void UsermodBME68X::addToJsonInfo(JsonObject& root) { //DEBUG_PRINTLN(F(UMOD_DEBUG_NAME "Add to info event")); JsonObject user = root[F("u")]; if (user.isNull()) user = root.createNestedObject(F("u")); if (!flags.InitSuccessful) { // Init was not seccessful - let the user know JsonArray temperature_json = user.createNestedArray(F("BME68X Sensor")); temperature_json.add(F("not found")); JsonArray humidity_json = user.createNestedArray(F("BMW68x Reason")); humidity_json.add(InfoPageStatusLine); } else if (!settings.enabled) { JsonArray temperature_json = user.createNestedArray(F("BME68X Sensor")); temperature_json.add(F("disabled")); } else { InfoHelper(user, _nameTemp, ValuesPtr->temperature, settings.decimals.temperature, tempScale.c_str()); InfoHelper(user, _nameHum, ValuesPtr->humidity, settings.decimals.humidity, _unitHum); InfoHelper(user, _namePress, ValuesPtr->pressure, settings.decimals.pressure, _unitPress); InfoHelper(user, _nameGasRes, ValuesPtr->gasResistance, settings.decimals.gasResistance, _unitGasres); InfoHelper(user, _nameAHum, ValuesPtr->absHumidity, settings.decimals.absHumidity, _unitAHum); InfoHelper(user, _nameDrewP, ValuesPtr->drewPoint, settings.decimals.drewPoint, tempScale.c_str()); InfoHelper(user, _nameIaq, ValuesPtr->iaq, settings.decimals.iaq, _unitIaq); InfoHelper(user, _nameIaqVerb, cvalues.iaqVerbal, settings.PublishIAQVerbal); InfoHelper(user, _nameStaticIaq, ValuesPtr->staticIaq, settings.decimals.staticIaq, _unitStaticIaq); InfoHelper(user, _nameStaticIaqVerb,cvalues.staticIaqVerbal, settings.PublishStaticIAQVerbal); InfoHelper(user, _nameCo2, ValuesPtr->co2, settings.decimals.co2, _unitCo2); InfoHelper(user, _nameVoc, ValuesPtr->Voc, settings.decimals.Voc, _unitVoc); InfoHelper(user, _nameGasPer, ValuesPtr->gasPerc, settings.decimals.gasPerc, _unitGasPer); if (settings.pubAcc) { if (settings.decimals.iaq >= 0) InfoHelper(user, _nameIaqAc, ValuesPtr->iaqAccuracy, 0, " "); if (settings.decimals.staticIaq >= 0) InfoHelper(user, _nameStaticIaqAc, ValuesPtr->staticIaqAccuracy, 0, " "); if (settings.decimals.co2 >= 0) InfoHelper(user, _nameCo2Ac, ValuesPtr->co2Accuracy, 0, " "); if (settings.decimals.Voc >= 0) InfoHelper(user, _nameVocAc, ValuesPtr->VocAccuracy, 0, " "); if (settings.decimals.gasPerc >= 0) InfoHelper(user, _nameGasPerAc, ValuesPtr->gasPercAccuracy, 0, " "); } if (settings.publishSensorState) { InfoHelper(user, _nameStabStatus, ValuesPtr->stabStatus, 0, " "); InfoHelper(user, _nameRunInStatus, ValuesPtr->runInStatus, 0, " "); } } } /** * @brief Info Page helper function * @param root JSON object * @param name Name of the sensor as char * @param sensorvalue Value of the sensor as float * @param decimals Decimal places of the value * @param unit Unit of the sensor */ void UsermodBME68X::InfoHelper(JsonObject& root, const char* name, const float& sensorvalue, const int8_t& decimals, const char* unit) { if (decimals > -1) { JsonArray sub_json = root.createNestedArray(name); sub_json.add(roundf(sensorvalue * powf(10, decimals)) / powf(10, decimals)); sub_json.add(unit); } } /** * @brief Info Page helper function (overload) * @param root JSON object * @param name Name of the sensor * @param sensorvalue Value of the sensor as string * @param status Status of the value (active/inactive) */ void UsermodBME68X::InfoHelper(JsonObject& root, const char* name, const String& sensorvalue, const bool& status) { if (status) { JsonArray sub_json = root.createNestedArray(name); sub_json.add(sensorvalue); } } /** * @brief Called by WLED: Adds the usermodul neends on the config page for user modules * @param JsonObject Pointer * * @see Usermod::addToConfig() * @see UsermodManager::addToConfig() */ void UsermodBME68X::addToConfig(JsonObject& root) { DEBUG_PRINT(F(UMOD_DEBUG_NAME "Creating configuration pages content: ")); JsonObject top = root.createNestedObject(FPSTR(UMOD_NAME)); /* general settings */ top[FPSTR(_enabled)] = settings.enabled; top[FPSTR(_nameI2CAdr)] = settings.I2cadress; top[FPSTR(_nameInterval)] = settings.Interval; top[FPSTR(_namePublishChange)] = settings.PublischChange; top[FPSTR(_namePubAc)] = settings.pubAcc; top[FPSTR(_namePubSenState)] = settings.publishSensorState; top[FPSTR(_nameTempScale)] = settings.tempScale; top[FPSTR(_nameTempOffset)] = settings.tempOffset; top[FPSTR(_nameHADisc)] = settings.HomeAssistantDiscovery; top[FPSTR(_namePauseOnActWL)] = settings.pauseOnActiveWled; top[FPSTR(_nameDelCalib)] = flags.DeleteCaibration; /* Digs */ JsonObject sensors_json = top.createNestedObject("Sensors"); sensors_json[FPSTR(_nameTemp)] = settings.decimals.temperature; sensors_json[FPSTR(_nameHum)] = settings.decimals.humidity; sensors_json[FPSTR(_namePress)] = settings.decimals.pressure; sensors_json[FPSTR(_nameGasRes)] = settings.decimals.gasResistance; sensors_json[FPSTR(_nameAHum)] = settings.decimals.absHumidity; sensors_json[FPSTR(_nameDrewP)] = settings.decimals.drewPoint; sensors_json[FPSTR(_nameIaq)] = settings.decimals.iaq; sensors_json[FPSTR(_nameIaqVerb)] = settings.PublishIAQVerbal; sensors_json[FPSTR(_nameStaticIaq)] = settings.decimals.staticIaq; sensors_json[FPSTR(_nameStaticIaqVerb)] = settings.PublishStaticIAQVerbal; sensors_json[FPSTR(_nameCo2)] = settings.decimals.co2; sensors_json[FPSTR(_nameVoc)] = settings.decimals.Voc; sensors_json[FPSTR(_nameGasPer)] = settings.decimals.gasPerc; DEBUG_PRINTLN(F(GOGAB_OK)); } /** * @brief Called by WLED: Add dropdown and additional infos / structure * @see Usermod::appendConfigData() * @see UsermodManager::appendConfigData() */ void UsermodBME68X::appendConfigData() { // snprintf_P(charbuffer, 127, PSTR("addInfo('%s:%s',1,'read interval [seconds]');"), UMOD_NAME, _nameInterval); oappend(charbuffer); // snprintf_P(charbuffer, 127, PSTR("addInfo('%s:%s',1,'only if value changes');"), UMOD_NAME, _namePublishChange); oappend(charbuffer); // snprintf_P(charbuffer, 127, PSTR("addInfo('%s:%s',1,'maximum age of a message in seconds');"), UMOD_NAME, _nameMaxAge); oappend(charbuffer); // snprintf_P(charbuffer, 127, PSTR("addInfo('%s:%s',1,'Gas related values are only published after the gas sensor has been calibrated');"), UMOD_NAME, _namePubAfterCalib); oappend(charbuffer); // snprintf_P(charbuffer, 127, PSTR("addInfo('%s:%s',1,'*) Set to minus to deactivate (all sensors)');"), UMOD_NAME, _nameTemp); oappend(charbuffer); /* Dropdown for Celsius/Fahrenheit*/ oappend(F("dd=addDropdown('")); oappend(UMOD_NAME); oappend(F("','")); oappend(_nameTempScale); oappend(F("');")); oappend(F("addOption(dd,'Celsius',0);")); oappend(F("addOption(dd,'Fahrenheit',1);")); /* i²C Address*/ oappend(F("dd=addDropdown('")); oappend(UMOD_NAME); oappend(F("','")); oappend(_nameI2CAdr); oappend(F("');")); oappend(F("addOption(dd,'0x76',0x76);")); oappend(F("addOption(dd,'0x77',0x77);")); } /** * @brief Called by WLED: Read Usermod Config Settings default settings values could be set here (or below using the 3-argument getJsonValue()) * instead of in the class definition or constructor setting them inside readFromConfig() is slightly more robust, handling the rare but * plausible use case of single value being missing after boot (e.g. if the cfg.json was manually edited and a value was removed) * This is called whenever WLED boots and loads cfg.json, or when the UM config * page is saved. Will properly re-instantiate the SHT class upon type change and * publish HA discovery after enabling. * NOTE: Here are the default settings of the user module * @param JsonObject Pointer * @return bool * @see Usermod::readFromConfig() * @see UsermodManager::readFromConfig() */ bool UsermodBME68X::readFromConfig(JsonObject& root) { DEBUG_PRINT(F(UMOD_DEBUG_NAME "Reading configuration: ")); JsonObject top = root[FPSTR(UMOD_NAME)]; bool configComplete = !top.isNull(); /* general settings */ /* DEFAULTS */ configComplete &= getJsonValue(top[FPSTR(_enabled)], settings.enabled, 1 ); // Usermod enabled per default configComplete &= getJsonValue(top[FPSTR(_nameI2CAdr)], settings.I2cadress, 0x77 ); // Defalut IC2 adress set to 0x77 (some modules are set to 0x76) configComplete &= getJsonValue(top[FPSTR(_nameInterval)], settings.Interval, 1 ); // Executed every second configComplete &= getJsonValue(top[FPSTR(_namePublishChange)], settings.PublischChange, false ); // Publish changed values only configComplete &= getJsonValue(top[FPSTR(_nameTempScale)], settings.tempScale, 0 ); // Temp sale set to Celsius (1=Fahrenheit) configComplete &= getJsonValue(top[FPSTR(_nameTempOffset)], settings.tempOffset, 0 ); // Temp offset is set to 0 (Celsius) configComplete &= getJsonValue(top[FPSTR(_namePubSenState)], settings.publishSensorState, 1 ); // Publish the sensor states configComplete &= getJsonValue(top[FPSTR(_namePubAc)], settings.pubAcc, 1 ); // Publish accuracy values configComplete &= getJsonValue(top[FPSTR(_nameHADisc)], settings.HomeAssistantDiscovery, true ); // Activate HomeAssistant Discovery (this Module will be shown as MQTT device in HA) configComplete &= getJsonValue(top[FPSTR(_namePauseOnActWL)], settings.pauseOnActiveWled, false ); // Pause on active WLED not activated per default configComplete &= getJsonValue(top[FPSTR(_nameDelCalib)], flags.DeleteCaibration, false ); // IF checked the calibration file will be delete when the save button is pressed /* Decimal places */ /* no of digs / -1 means deactivated */ configComplete &= getJsonValue(top["Sensors"][FPSTR(_nameTemp)], settings.decimals.temperature, 1 ); // One decimal places configComplete &= getJsonValue(top["Sensors"][FPSTR(_nameHum)], settings.decimals.humidity, 1 ); configComplete &= getJsonValue(top["Sensors"][FPSTR(_namePress)], settings.decimals.pressure, 0 ); // Zero decimal places configComplete &= getJsonValue(top["Sensors"][FPSTR(_nameGasRes)], settings.decimals.gasResistance, -1 ); // deavtivated configComplete &= getJsonValue(top["Sensors"][FPSTR(_nameDrewP)], settings.decimals.drewPoint, 1 ); configComplete &= getJsonValue(top["Sensors"][FPSTR(_nameAHum)], settings.decimals.absHumidity, 1 ); configComplete &= getJsonValue(top["Sensors"][FPSTR(_nameIaq)], settings.decimals.iaq, 0 ); // Index for Air Quality Number is active configComplete &= getJsonValue(top["Sensors"][FPSTR(_nameIaqVerb)], settings.PublishIAQVerbal, -1 ); // deactivated - Index for Air Quality (IAQ) verbal classification configComplete &= getJsonValue(top["Sensors"][FPSTR(_nameStaticIaq)], settings.decimals.staticIaq, 0 ); // activated - Static IAQ is better than IAQ for devices that are not moved configComplete &= getJsonValue(top["Sensors"][FPSTR(_nameStaticIaqVerb)], settings.PublishStaticIAQVerbal, 0 ); // activated configComplete &= getJsonValue(top["Sensors"][FPSTR(_nameCo2)], settings.decimals.co2, 0 ); configComplete &= getJsonValue(top["Sensors"][FPSTR(_nameVoc)], settings.decimals.Voc, 0 ); configComplete &= getJsonValue(top["Sensors"][FPSTR(_nameGasPer)], settings.decimals.gasPerc, 0 ); DEBUG_PRINTLN(F(GOGAB_OK)); /* Set the selected temperature unit */ if (settings.tempScale) { tempScale = F(_unitFahrenheit); } else { tempScale = F(_unitCelsius); } if (flags.DeleteCaibration) { DEBUG_PRINT(F(UMOD_DEBUG_NAME "Deleting Calibration File")); flags.DeleteCaibration = false; if (WLED_FS.remove(CALIB_FILE_NAME)) { DEBUG_PRINTLN(F(GOGAB_OK)); } else { DEBUG_PRINTLN(F(GOGAB_FAIL)); } } if (settings.Interval < 1) settings.Interval = 1; // Correct interval on need (A number less than 1 is not permitted) iaqSensor.setTemperatureOffset(settings.tempOffset); // Set Temp Offset return configComplete; } /** * @brief Called by WLED: Retunrs the user modul id number * * @return uint16_t User module number */ uint16_t UsermodBME68X::getId() { return USERMOD_ID_BME68X; } /** * @brief Returns the current temperature in the scale which is choosen in settings * @return Temperature value (°C or °F as choosen in settings) */ inline float UsermodBME68X::getTemperature() { return ValuesPtr->temperature; } /** * @brief Returns the current humidity * @return Humididty value (%) */ inline float UsermodBME68X::getHumidity() { return ValuesPtr->humidity; } /** * @brief Returns the current pressure * @return Pressure value (hPa) */ inline float UsermodBME68X::getPressure() { return ValuesPtr->pressure; } /** * @brief Returns the current gas resistance * @return Gas resistance value (kΩ) */ inline float UsermodBME68X::getGasResistance() { return ValuesPtr->gasResistance; } /** * @brief Returns the current absolute humidity * @return Absolute humidity value (g/m³) */ inline float UsermodBME68X::getAbsoluteHumidity() { return ValuesPtr->absHumidity; } /** * @brief Returns the current dew point * @return Dew point (°C or °F as choosen in settings) */ inline float UsermodBME68X::getDewPoint() { return ValuesPtr->drewPoint; } /** * @brief Returns the current iaq (Indoor Air Quallity) * @return Iaq value (0-500) */ inline float UsermodBME68X::getIaq() { return ValuesPtr->iaq; } /** * @brief Returns the current static iaq (Indoor Air Quallity) (NOTE: Static iaq is the better choice than iaq for fixed devices such as the wled module) * @return Static iaq value (float) */ inline float UsermodBME68X::getStaticIaq() { return ValuesPtr->staticIaq; } /** * @brief Returns the current co2 * @return Co2 value (ppm) */ inline float UsermodBME68X::getCo2() { return ValuesPtr->co2; } /** * @brief Returns the current voc (Breath VOC concentration estimate [ppm]) * @return Voc value (ppm) */ inline float UsermodBME68X::getVoc() { return ValuesPtr->Voc; } /** * @brief Returns the current gas percentage * @return Gas percentage value (%) */ inline float UsermodBME68X::getGasPerc() { return ValuesPtr->gasPerc; } /** * @brief Returns the current iaq accuracy (0 = not calibrated, 2 = being calibrated, 3 = calibrated) * @return Iaq accuracy value (0-3) */ inline uint8_t UsermodBME68X::getIaqAccuracy() { return ValuesPtr->iaqAccuracy ; } /** * @brief Returns the current static iaq accuracy accuracy (0 = not calibrated, 2 = being calibrated, 3 = calibrated) * @return Static iaq accuracy value (0-3) */ inline uint8_t UsermodBME68X::getStaticIaqAccuracy() { return ValuesPtr->staticIaqAccuracy; } /** * @brief Returns the current co2 accuracy (0 = not calibrated, 2 = being calibrated, 3 = calibrated) * @return Co2 accuracy value (0-3) */ inline uint8_t UsermodBME68X::getCo2Accuracy() { return ValuesPtr->co2Accuracy; } /** * @brief Returns the current voc accuracy (0 = not calibrated, 2 = being calibrated, 3 = calibrated) * @return Voc accuracy value (0-3) */ inline uint8_t UsermodBME68X::getVocAccuracy() { return ValuesPtr->VocAccuracy; } /** * @brief Returns the current gas percentage accuracy (0 = not calibrated, 2 = being calibrated, 3 = calibrated) * @return Gas percentage accuracy value (0-3) */ inline uint8_t UsermodBME68X::getGasPercAccuracy() { return ValuesPtr->gasPercAccuracy; } /** * @brief Returns the current stab status. * Indicates when the sensor is ready after after switch-on * @return stab status value (0 = switched on / 1 = stabilized) */ inline bool UsermodBME68X::getStabStatus() { return ValuesPtr->stabStatus; } /** * @brief Returns the current run in status. * Indicates if the sensor is undergoing initial stabilization during its first use after production * @return Tun status accuracy value (0 = switched on first time / 1 = stabilized) */ inline bool UsermodBME68X::getRunInStatus() { return ValuesPtr->runInStatus; } /** * @brief Checks whether the library and the sensor are running. */ void UsermodBME68X::checkIaqSensorStatus() { if (iaqSensor.bsecStatus != BSEC_OK) { InfoPageStatusLine = "BSEC Library "; DEBUG_PRINT(UMOD_DEBUG_NAME + InfoPageStatusLine); flags.InitSuccessful = false; if (iaqSensor.bsecStatus < BSEC_OK) { InfoPageStatusLine += " Error Code : " + String(iaqSensor.bsecStatus); DEBUG_PRINTLN(GOGAB_FAIL); } else { InfoPageStatusLine += " Warning Code : " + String(iaqSensor.bsecStatus); DEBUG_PRINTLN(GOGAB_WARN); } } else { InfoPageStatusLine = "Sensor BME68X "; DEBUG_PRINT(UMOD_DEBUG_NAME + InfoPageStatusLine); if (iaqSensor.bme68xStatus != BME68X_OK) { flags.InitSuccessful = false; if (iaqSensor.bme68xStatus < BME68X_OK) { InfoPageStatusLine += "error code: " + String(iaqSensor.bme68xStatus); DEBUG_PRINTLN(GOGAB_FAIL); } else { InfoPageStatusLine += "warning code: " + String(iaqSensor.bme68xStatus); DEBUG_PRINTLN(GOGAB_WARN); } } else { InfoPageStatusLine += F("OK"); DEBUG_PRINTLN(GOGAB_OK); } } } /** * @brief Loads the calibration data from the file system of the device */ void UsermodBME68X::loadState() { if (WLED_FS.exists(CALIB_FILE_NAME)) { DEBUG_PRINT(F(UMOD_DEBUG_NAME "Read the calibration file: ")); File file = WLED_FS.open(CALIB_FILE_NAME, FILE_READ); if (!file) { DEBUG_PRINTLN(GOGAB_FAIL); } else { file.read(bsecState, BSEC_MAX_STATE_BLOB_SIZE); file.close(); DEBUG_PRINTLN(GOGAB_OK); iaqSensor.setState(bsecState); } } else { DEBUG_PRINTLN(F(UMOD_DEBUG_NAME "Calibration file not found.")); } } /** * @brief Saves the calibration data from the file system of the device */ void UsermodBME68X::saveState() { DEBUG_PRINT(F(UMOD_DEBUG_NAME "Write the calibration file ")); File file = WLED_FS.open(CALIB_FILE_NAME, FILE_WRITE); if (!file) { DEBUG_PRINTLN(GOGAB_FAIL); } else { iaqSensor.getState(bsecState); file.write(bsecState, BSEC_MAX_STATE_BLOB_SIZE); file.close(); stateUpdateCounter++; DEBUG_PRINTF("(saved %d times)" GOGAB_OK "\n", stateUpdateCounter); flags.SaveState = false; // Clear save state flag char contbuffer[30]; /* Timestamp */ time_t curr_time; tm* curr_tm; time(&curr_time); curr_tm = localtime(&curr_time); snprintf_P(charbuffer, 127, PSTR("%s/%s"), mqttDeviceTopic, UMOD_NAME "/Calib Last Run"); strftime(contbuffer, 30, "%d %B %Y - %T", curr_tm); if (WLED_MQTT_CONNECTED) mqtt->publish(charbuffer, 0, false, contbuffer); snprintf(contbuffer, 30, "%d", stateUpdateCounter); snprintf_P(charbuffer, 127, PSTR("%s/%s"), mqttDeviceTopic, UMOD_NAME "/Calib Count"); if (WLED_MQTT_CONNECTED) mqtt->publish(charbuffer, 0, false, contbuffer); } } static UsermodBME68X bme68x_v2; REGISTER_USERMOD(bme68x_v2); ================================================ FILE: usermods/BME68X_v2/README.md ================================================ # Usermod BME68X This usermod was developed for a BME680/BME68X sensor. The BME68X is not compatible with the BME280/BMP280 chip. It has its own library. The original 'BSEC Software Library' from Bosch was used to develop the code. The measured values are displayed on the WLED info page.

In addition, the values are published on MQTT if this is active. The topic used for this is: 'wled/[MQTT Client ID]'. The Client ID is set in the WLED MQTT settings.

If you use HomeAssistance discovery, the device tree for HomeAssistance is created. This is published under the topic 'homeassistant/sensor/[MQTT Client ID]' via MQTT.

A device with the following sensors appears in HomeAssistant. Please note that MQTT must be activated in HomeAssistant.

## Features Raw sensor types Sensor Accuracy Scale Range ----------------------------- Temperature +/- 1.0 °C/°F -40 to 85 °C Humidity +/- 3 % 0 to 100 % Pressure +/- 1 hPa 300 to 1100 hPa Gas Resistance Ohm The BSEC Library calculates the following values via the gas resistance Sensor Accuracy Scale Range ----------------------------- IAQ value between 0 and 500 Static IAQ same as IAQ but for permanently installed devices CO2 PPM VOC PPM Gas-Percentage % In addition the usermod calculates Sensor Accuracy Scale Range ----------------------------- Absolute humidity g/m³ Dew point °C/°F ### IAQ (Indoor Air Quality) The IAQ is divided into the following value groups.

For more detailed information, please consult the enclosed Bosch product description (BME680.pdf). ## Calibration of the device The gas sensor of the BME68X must be calibrated. This differs from the BME280, which does not require any calibration. There is a range of additional information for this, which the driver also provides. These values can be found in HomeAssistant under Diagnostics. - **STABILIZATION_STATUS**: Gas sensor stabilization status [boolean] Indicates initial stabilization status of the gas sensor element: stabilization is ongoing (0) or stabilization is finished (1). - **RUN_IN_STATUS**: Gas sensor run-in status [boolean] Indicates power-on stabilization status of the gas sensor element: stabilization is ongoing (0) or stabilization is finished (1) Furthermore, all GAS based values have their own accuracy value. These have the following meaning: - **Accuracy = 0** means the sensor is being stabilized (this can take a while on the first run) - **Accuracy = 1** means that the previous measured values show too few differences and cannot be used for calibration. If the sensor is at accuracy 1 for too long, you must ensure that the ambient air is chaning. Opening the windows is fine. Or sometimes it is sufficient to breathe on the sensor for approx. 5 minutes. - **Accuracy = 2** means the sensor is currently calibrating. - **Accuracy = 3** means that the sensor has been successfully calibrated. Once accuracy 3 is reached, the calibration data is automatically written to the file system. This calibration data will be used again at the next start and will speed up the calibration. The IAQ index is therefore only meaningful if IAQ Accuracy = 3. In addition to the value for IAQ, BSEC also provides us with CO2 and VOC equivalent values. When using the sensor, the calibration value should also always be read out and displayed or transmitted. Reasonably reliable values are therefore only achieved when accuracy displays the value 3. ## Settings The settings of the usermods are set in the usermod section of wled.

The possible settings are - **Enable:** Enables / disables the usermod - **I2C address:** I2C address of the sensor. You can choose between 0X77 & 0X76. The default is 0x77. - **Interval:** Specifies the interval of seconds at which the usermod should be executed. The default is every second. - **Pub Chages Only:** If this item is active, the values are only published if they have changed since the last publication. - **Pub Accuracy:** The Accuracy values associated with the gas values are also published. - **Pub Calib State:** If this item is active, STABILIZATION_STATUS& RUN_IN_STATUS are also published. - **Temp Scale:** Here you can choose between °C and °F. - **Temp Offset:** The temperature offset is always set in °C. It must be converted for Fahrenheit. - **HA Discovery:** If this item is active, the HomeAssistant sensor tree is created. - **Pause While WLED Active:** If WLED has many LEDs to calculate, the computing power may no longer be sufficient to calculate the LEDs and read the sensor data. The LEDs then hang for a few microseconds, which can be seen. If this point is active, no sensor data is fetched as long as WLED is running. - **Del Calibration Hist:** If a check mark is set here, the calibration file saved in the file system is deleted when the settings are saved. ### Sensors Applies to all sensors. The number of decimal places is set here. If the sensor is set to -1, it will no longer be published. In addition, the IAQ values can be activated here in verbal form. It is recommended to use the Static IAQ for the IAQ values. This is recommended by Bosch for statically placed devices. ## Output Data is published over MQTT - make sure you've enabled the MQTT sync interface. In addition to outputting via MQTT, you can read the values from the Info Screen on the dashboard page of the device's web interface. Methods also exist to read the read/calculated values from other WLED modules through code. - getTemperature(); The scale °C/°F is depended to the settings - getHumidity(); - getPressure(); - getGasResistance(); - getAbsoluteHumidity(); - getDewPoint(); The scale °C/°F is depended to the settings - getIaq(); - getStaticIaq(); - getCo2(); - getVoc(); - getGasPerc(); - getIaqAccuracy(); - getStaticIaqAccuracy(); - getCo2Accuracy(); - getVocAccuracy(); - getGasPercAccuracy(); - getStabStatus(); - getRunInStatus(); ## Compilation To enable, compile with `BME68X` in `custom_usermods` (e.g. in `platformio_override.ini`) Example: ```[env:esp32_mySpecial] extends = env:esp32dev custom_usermods = ${env:esp32dev.custom_usermods} BME68X ``` ## Revision History ### Version 1.0.0 - First version of the BME68X_v user module ### Version 1.0.1 - Rebased to WELD Version 0.15 - Reworked some default settings - A problem with the default settings has been fixed ### Version 1.0.2 * Rebased to WELD Version 0.16 * Fixed: Solved compilation problems related to some macro naming interferences. ## Known problems - MQTT goes online at device start. Shortly afterwards it goes offline and takes quite a while until it goes online again. The problem does not come from this user module, but from the WLED core. - If you save the settings often, WLED can get stuck. - If many LEDS are connected to WLED, reading the sensor can cause a small but noticeable hang. The "Pause While WLED Active" option was introduced as a workaround.
Gabriel Sieben (gsieben@geogab.net) ================================================ FILE: usermods/BME68X_v2/library.json ================================================ { "name": "BME68X", "build": { "libArchive": false }, "dependencies": { "boschsensortec/BSEC Software Library":"^1.8.1492" } } ================================================ FILE: usermods/Battery/Battery.cpp ================================================ #include "wled.h" #include "battery_defaults.h" #include "UMBattery.h" #include "types/UnkownUMBattery.h" #include "types/LionUMBattery.h" #include "types/LipoUMBattery.h" /* * Usermod by Maximilian Mewes * E-mail: mewes.maximilian@gmx.de * Created at: 25.12.2022 * If you have any questions, please feel free to contact me. */ class UsermodBattery : public Usermod { private: // battery pin can be defined in my_config.h int8_t batteryPin = USERMOD_BATTERY_MEASUREMENT_PIN; UMBattery* bat = new UnkownUMBattery(); batteryConfig cfg; // Initial delay before first reading to allow voltage stabilization unsigned long initialDelay = USERMOD_BATTERY_INITIAL_DELAY; bool initialDelayComplete = false; bool isFirstVoltageReading = true; // how often to read the battery voltage unsigned long readingInterval = USERMOD_BATTERY_MEASUREMENT_INTERVAL; unsigned long nextReadTime = 0; unsigned long lastReadTime = 0; // between 0 and 1, to control strength of voltage smoothing filter float alpha = USERMOD_BATTERY_AVERAGING_ALPHA; // auto shutdown/shutoff/master off feature bool autoOffEnabled = USERMOD_BATTERY_AUTO_OFF_ENABLED; uint8_t autoOffThreshold = USERMOD_BATTERY_AUTO_OFF_THRESHOLD; // low power indicator feature bool lowPowerIndicatorEnabled = USERMOD_BATTERY_LOW_POWER_INDICATOR_ENABLED; uint8_t lowPowerIndicatorPreset = USERMOD_BATTERY_LOW_POWER_INDICATOR_PRESET; uint8_t lowPowerIndicatorThreshold = USERMOD_BATTERY_LOW_POWER_INDICATOR_THRESHOLD; uint8_t lowPowerIndicatorReactivationThreshold = lowPowerIndicatorThreshold+10; uint8_t lowPowerIndicatorDuration = USERMOD_BATTERY_LOW_POWER_INDICATOR_DURATION; bool lowPowerIndicationDone = false; unsigned long lowPowerActivationTime = 0; // used temporary during active time uint8_t lastPreset = 0; // bool initDone = false; bool initializing = true; bool HomeAssistantDiscovery = false; // strings to reduce flash memory usage (used more than twice) static const char _name[]; static const char _readInterval[]; static const char _enabled[]; static const char _threshold[]; static const char _preset[]; static const char _duration[]; static const char _init[]; static const char _haDiscovery[]; /** * Helper for rounding floating point values */ float dot2round(float x) { float nx = (int)(x * 100 + .5); return (float)(nx / 100); } /** * Helper for converting a string to lowercase */ String stringToLower(String str) { for(int i = 0; i < str.length(); i++) if(str[i] >= 'A' && str[i] <= 'Z') str[i] += 32; return str; } /** * Turn off all leds */ void turnOff() { bri = 0; stateUpdated(CALL_MODE_DIRECT_CHANGE); } /** * Indicate low power by activating a configured preset for a given time and then switching back to the preset that was selected previously */ void lowPowerIndicator() { if (!lowPowerIndicatorEnabled) return; if (batteryPin < 0) return; // no measurement if (lowPowerIndicationDone && lowPowerIndicatorReactivationThreshold <= bat->getLevel()) lowPowerIndicationDone = false; if (lowPowerIndicatorThreshold <= bat->getLevel()) return; if (lowPowerIndicationDone) return; if (lowPowerActivationTime <= 1) { lowPowerActivationTime = millis(); lastPreset = currentPreset; applyPreset(lowPowerIndicatorPreset); } if (lowPowerActivationTime+(lowPowerIndicatorDuration*1000) <= millis()) { lowPowerIndicationDone = true; lowPowerActivationTime = 0; applyPreset(lastPreset); } } /** * read the battery voltage in different ways depending on the architecture */ float readVoltage() { #ifdef ARDUINO_ARCH_ESP32 // use calibrated millivolts analogread on esp32 (150 mV ~ 2450 mV default attentuation) and divide by 1000 to get from milivolts to volts and multiply by voltage multiplier and apply calibration value return (analogReadMilliVolts(batteryPin) / 1000.0f) * bat->getVoltageMultiplier() + bat->getCalibration(); #else // use analog read on esp8266 ( 0V ~ 1V no attenuation options) and divide by ADC precision 1023 and multiply by voltage multiplier and apply calibration value return (analogRead(batteryPin) / 1023.0f) * bat->getVoltageMultiplier() + bat->getCalibration(); #endif } #ifndef WLED_DISABLE_MQTT void addMqttSensor(const String &name, const String &type, const String &topic, const String &deviceClass, const String &unitOfMeasurement = "", const bool &isDiagnostic = false) { StaticJsonDocument<600> doc; char uid[128], json_str[1024], buf[128]; doc[F("name")] = name; doc[F("stat_t")] = topic; sprintf_P(uid, PSTR("%s_%s_%s"), escapedMac.c_str(), stringToLower(name).c_str(), type); doc[F("uniq_id")] = uid; doc[F("dev_cla")] = deviceClass; doc[F("exp_aft")] = 1800; if(type == "binary_sensor") { doc[F("pl_on")] = "on"; doc[F("pl_off")] = "off"; } if(unitOfMeasurement != "") doc[F("unit_of_measurement")] = unitOfMeasurement; if(isDiagnostic) doc[F("entity_category")] = "diagnostic"; JsonObject device = doc.createNestedObject(F("device")); // attach the sensor to the same device device[F("name")] = serverDescription; device[F("ids")] = String(F("wled-sensor-")) + mqttClientID; device[F("mf")] = F(WLED_BRAND); device[F("mdl")] = F(WLED_PRODUCT_NAME); device[F("sw")] = versionString; sprintf_P(buf, PSTR("homeassistant/%s/%s/%s/config"), type, mqttClientID, uid); DEBUG_PRINTLN(buf); size_t payload_size = serializeJson(doc, json_str); DEBUG_PRINTLN(json_str); mqtt->publish(buf, 0, true, json_str, payload_size); } void publishMqtt(const char* topic, const char* state) { if (WLED_MQTT_CONNECTED) { char buf[128]; snprintf_P(buf, 127, PSTR("%s/%s"), mqttDeviceTopic, topic); mqtt->publish(buf, 0, false, state); } } #endif public: //Functions called by WLED /** * 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() { // plug in the right battery type if(cfg.type == (batteryType)lipo) { bat = new LipoUMBattery(); } else if(cfg.type == (batteryType)lion) { bat = new LionUMBattery(); } // update the choosen battery type with configured values bat->update(cfg); #ifdef ARDUINO_ARCH_ESP32 bool success = false; DEBUG_PRINTLN(F("Allocating battery pin...")); if (batteryPin >= 0 && digitalPinToAnalogChannel(batteryPin) >= 0) if (PinManager::allocatePin(batteryPin, false, PinOwner::UM_Battery)) { DEBUG_PRINTLN(F("Battery pin allocation succeeded.")); success = true; } if (!success) { DEBUG_PRINTLN(F("Battery pin allocation failed.")); batteryPin = -1; // allocation failed } else { pinMode(batteryPin, INPUT); } #else //ESP8266 boards have only one analog input pin A0 pinMode(batteryPin, INPUT); #endif // First voltage reading is delayed to allow voltage stabilization after powering up nextReadTime = millis() + initialDelay; lastReadTime = millis(); initDone = true; } /** * 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. * */ void loop() { if(strip.isUpdating()) return; lowPowerIndicator(); // Handling the initial delay if (!initialDelayComplete && millis() < nextReadTime) return; // Continue to return until the initial delay is over // Once the initial delay is over, set it as complete if (!initialDelayComplete) { initialDelayComplete = true; // Set the regular interval after initial delay nextReadTime = millis() + readingInterval; } // Make the first voltage reading after the initial delay has elapsed if (isFirstVoltageReading) { bat->setVoltage(readVoltage()); isFirstVoltageReading = false; } // check the battery level every USERMOD_BATTERY_MEASUREMENT_INTERVAL (ms) if (millis() < nextReadTime) return; nextReadTime = millis() + readingInterval; lastReadTime = millis(); if (batteryPin < 0) return; // nothing to read initializing = false; float rawValue = readVoltage(); // filter with exponential smoothing because ADC in esp32 is fluctuating too much for a good single readout float filteredVoltage = bat->getVoltage() + alpha * (rawValue - bat->getVoltage()); bat->setVoltage(filteredVoltage); // translate battery voltage into percentage bat->calculateAndSetLevel(filteredVoltage); // Auto off -- Master power off if (autoOffEnabled && (autoOffThreshold >= bat->getLevel())) turnOff(); #ifndef WLED_DISABLE_MQTT publishMqtt("battery", String(bat->getLevel(), 0).c_str()); publishMqtt("voltage", String(bat->getVoltage()).c_str()); #endif } /** * 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) { JsonObject user = root["u"]; if (user.isNull()) user = root.createNestedObject("u"); if (batteryPin < 0) { JsonArray infoVoltage = user.createNestedArray(F("Battery voltage")); infoVoltage.add(F("n/a")); infoVoltage.add(F(" invalid GPIO")); return; // no GPIO - nothing to report } // info modal display names JsonArray infoPercentage = user.createNestedArray(F("Battery level")); JsonArray infoVoltage = user.createNestedArray(F("Battery voltage")); JsonArray infoNextUpdate = user.createNestedArray(F("Next update")); infoNextUpdate.add((nextReadTime - millis()) / 1000); infoNextUpdate.add(F(" sec")); if (initializing) { infoPercentage.add(FPSTR(_init)); infoVoltage.add(FPSTR(_init)); return; } if (bat->getLevel() < 0) { infoPercentage.add(F("invalid")); } else { infoPercentage.add(bat->getLevel()); } infoPercentage.add(F(" %")); if (bat->getVoltage() < 0) { infoVoltage.add(F("invalid")); } else { infoVoltage.add(dot2round(bat->getVoltage())); } infoVoltage.add(F(" V")); } void addBatteryToJsonObject(JsonObject& battery, bool forJsonState) { if(forJsonState) { battery[F("type")] = cfg.type; } else {battery[F("type")] = (String)cfg.type; } // has to be a String otherwise it won't get converted to a Dropdown battery[F("min-voltage")] = bat->getMinVoltage(); battery[F("max-voltage")] = bat->getMaxVoltage(); battery[F("calibration")] = bat->getCalibration(); battery[F("voltage-multiplier")] = bat->getVoltageMultiplier(); battery[FPSTR(_readInterval)] = readingInterval; battery[FPSTR(_haDiscovery)] = HomeAssistantDiscovery; JsonObject ao = battery.createNestedObject(F("auto-off")); // auto off section ao[FPSTR(_enabled)] = autoOffEnabled; ao[FPSTR(_threshold)] = autoOffThreshold; JsonObject lp = battery.createNestedObject(F("indicator")); // low power section lp[FPSTR(_enabled)] = lowPowerIndicatorEnabled; lp[FPSTR(_preset)] = lowPowerIndicatorPreset; // dropdown trickery (String)lowPowerIndicatorPreset; lp[FPSTR(_threshold)] = lowPowerIndicatorThreshold; lp[FPSTR(_duration)] = lowPowerIndicatorDuration; } void getUsermodConfigFromJsonObject(JsonObject& battery) { getJsonValue(battery[F("type")], cfg.type); getJsonValue(battery[F("min-voltage")], cfg.minVoltage); getJsonValue(battery[F("max-voltage")], cfg.maxVoltage); getJsonValue(battery[F("calibration")], cfg.calibration); getJsonValue(battery[F("voltage-multiplier")], cfg.voltageMultiplier); setReadingInterval(battery[FPSTR(_readInterval)] | readingInterval); setHomeAssistantDiscovery(battery[FPSTR(_haDiscovery)] | HomeAssistantDiscovery); JsonObject ao = battery[F("auto-off")]; setAutoOffEnabled(ao[FPSTR(_enabled)] | autoOffEnabled); setAutoOffThreshold(ao[FPSTR(_threshold)] | autoOffThreshold); JsonObject lp = battery[F("indicator")]; setLowPowerIndicatorEnabled(lp[FPSTR(_enabled)] | lowPowerIndicatorEnabled); setLowPowerIndicatorPreset(lp[FPSTR(_preset)] | lowPowerIndicatorPreset); setLowPowerIndicatorThreshold(lp[FPSTR(_threshold)] | lowPowerIndicatorThreshold); lowPowerIndicatorReactivationThreshold = lowPowerIndicatorThreshold+10; setLowPowerIndicatorDuration(lp[FPSTR(_duration)] | lowPowerIndicatorDuration); if(initDone) bat->update(cfg); } /** * 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) { JsonObject battery = root.createNestedObject(FPSTR(_name)); if (battery.isNull()) battery = root.createNestedObject(FPSTR(_name)); addBatteryToJsonObject(battery, true); DEBUG_PRINTLN(F("Battery state exposed in JSON API.")); } /** * 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) { if (!initDone) return; // prevent crash on boot applyPreset() JsonObject battery = root[FPSTR(_name)]; if (!battery.isNull()) { getUsermodConfigFromJsonObject(battery); DEBUG_PRINTLN(F("Battery state read from JSON API.")); } } */ /** * 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 make your settings editable through the Usermod Settings page automatically. * * Usermod Settings Overview: * - Numeric values are treated as floats in the browser. * - If the numeric value entered into the browser contains a decimal point, it will be parsed as a C float * before being returned to the Usermod. The float data type has only 6-7 decimal digits of precision, and * doubles are not supported, numbers will be rounded to the nearest float value when being parsed. * The range accepted by the input field is +/- 1.175494351e-38 to +/- 3.402823466e+38. * - If the numeric value entered into the browser doesn't contain a decimal point, it will be parsed as a * C int32_t (range: -2147483648 to 2147483647) before being returned to the usermod. * Overflows or underflows are truncated to the max/min value for an int32_t, and again truncated to the type * used in the Usermod when reading the value from ArduinoJson. * - Pin values can be treated differently from an integer value by using the key name "pin" * - "pin" can contain a single or array of integer values * - On the Usermod Settings page there is simple checking for pin conflicts and warnings for special pins * - Red color indicates a conflict. Yellow color indicates a pin with a warning (e.g. an input-only pin) * - Tip: use int8_t to store the pin value in the Usermod, so a -1 value (pin not set) can be used * * See usermod_v2_auto_save.h for an example that saves Flash space by reusing ArduinoJson key name strings * * If you need a dedicated settings page with custom layout for your Usermod, that takes a lot more work. * You will have to add the setting to the HTML, xml.cpp and set.cpp manually. * See the WLED Soundreactive fork (code and wiki) for reference. https://github.com/atuline/WLED * * I highly recommend checking out the basics of ArduinoJson serialization and deserialization in order to use custom settings! */ void addToConfig(JsonObject& root) { JsonObject battery = root.createNestedObject(FPSTR(_name)); if (battery.isNull()) { battery = root.createNestedObject(FPSTR(_name)); } #ifdef ARDUINO_ARCH_ESP32 battery[F("pin")] = batteryPin; #endif addBatteryToJsonObject(battery, false); // read voltage in case calibration or voltage multiplier changed to see immediate effect bat->setVoltage(readVoltage()); DEBUG_PRINTLN(F("Battery config saved.")); } void appendConfigData() { // Total: 462 Bytes oappend(F("td=addDropdown('Battery','type');")); // 34 Bytes oappend(F("addOption(td,'Unkown','0');")); // 28 Bytes oappend(F("addOption(td,'LiPo','1');")); // 26 Bytes oappend(F("addOption(td,'LiOn','2');")); // 26 Bytes oappend(F("addInfo('Battery:type',1,'requires reboot');")); // 81 Bytes oappend(F("addInfo('Battery:min-voltage',1,'v');")); // 38 Bytes oappend(F("addInfo('Battery:max-voltage',1,'v');")); // 38 Bytes oappend(F("addInfo('Battery:interval',1,'ms');")); // 36 Bytes oappend(F("addInfo('Battery:HA-discovery',1,'');")); // 38 Bytes oappend(F("addInfo('Battery:auto-off:threshold',1,'%');")); // 45 Bytes oappend(F("addInfo('Battery:indicator:threshold',1,'%');")); // 46 Bytes oappend(F("addInfo('Battery:indicator:duration',1,'s');")); // 45 Bytes // this option list would exeed the oappend() buffer // a list of all presets to select one from // oappend(F("bd=addDropdown('Battery:low-power-indicator', 'preset');")); // the loop generates: oappend(F("addOption(bd, 'preset name', preset id);")); // for(int8_t i=1; i < 42; i++) { // oappend(F("addOption(bd, 'Preset#")); // oappendi(i); // oappend(F("',")); // oappendi(i); // oappend(F(");")); // } } /** * 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 immediately after boot, or after saving on the Usermod Settings page) * * 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 :) * * Return true in case the config values returned from Usermod Settings were complete, or false if you'd like WLED to save your defaults to disk (so any missing values are editable in Usermod Settings) * * getJsonValue() returns false if the value is missing, or copies the value into the variable provided and returns true if the value is present * The configComplete variable is true only if the "exampleUsermod" object and all values are present. If any values are missing, WLED will know to call addToConfig() to save them * * This function is guaranteed to be called on boot, but could also be called every time settings are updated */ bool readFromConfig(JsonObject& root) { #ifdef ARDUINO_ARCH_ESP32 int8_t newBatteryPin = batteryPin; #endif JsonObject battery = root[FPSTR(_name)]; if (battery.isNull()) { DEBUG_PRINT(FPSTR(_name)); DEBUG_PRINTLN(F(": No config found. (Using defaults.)")); return false; } #ifdef ARDUINO_ARCH_ESP32 newBatteryPin = battery[F("pin")] | newBatteryPin; #endif setMinBatteryVoltage(battery[F("min-voltage")] | bat->getMinVoltage()); setMaxBatteryVoltage(battery[F("max-voltage")] | bat->getMaxVoltage()); setCalibration(battery[F("calibration")] | bat->getCalibration()); setVoltageMultiplier(battery[F("voltage-multiplier")] | bat->getVoltageMultiplier()); setReadingInterval(battery[FPSTR(_readInterval)] | readingInterval); setHomeAssistantDiscovery(battery[FPSTR(_haDiscovery)] | HomeAssistantDiscovery); getUsermodConfigFromJsonObject(battery); #ifdef ARDUINO_ARCH_ESP32 if (!initDone) { // first run: reading from cfg.json batteryPin = newBatteryPin; DEBUG_PRINTLN(F(" config loaded.")); } else { DEBUG_PRINTLN(F(" config (re)loaded.")); // changing parameters from settings page if (newBatteryPin != batteryPin) { // deallocate pin PinManager::deallocatePin(batteryPin, PinOwner::UM_Battery); batteryPin = newBatteryPin; // initialise setup(); } } #endif return !battery[FPSTR(_readInterval)].isNull(); } #ifndef WLED_DISABLE_MQTT void onMqttConnect(bool sessionPresent) { // Home Assistant Autodiscovery if (!HomeAssistantDiscovery) return; // battery percentage char mqttBatteryTopic[128]; snprintf_P(mqttBatteryTopic, 127, PSTR("%s/battery"), mqttDeviceTopic); this->addMqttSensor(F("Battery"), "sensor", mqttBatteryTopic, "battery", "%", true); // voltage char mqttVoltageTopic[128]; snprintf_P(mqttVoltageTopic, 127, PSTR("%s/voltage"), mqttDeviceTopic); this->addMqttSensor(F("Voltage"), "sensor", mqttVoltageTopic, "voltage", "V", true); } #endif /* * * Getter and Setter. Just in case some other usermod wants to interact with this in the future * */ /** * 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_BATTERY; } /** * get currently active battery type */ batteryType getBatteryType() { return cfg.type; } /** * */ unsigned long getReadingInterval() { return readingInterval; } /** * minimum repetition is 3000ms (3s) */ void setReadingInterval(unsigned long newReadingInterval) { readingInterval = max((unsigned long)3000, newReadingInterval); } /** * Get lowest configured battery voltage */ float getMinBatteryVoltage() { return bat->getMinVoltage(); } /** * Set lowest battery voltage * can't be below 0 volt */ void setMinBatteryVoltage(float voltage) { bat->setMinVoltage(voltage); } /** * Get highest configured battery voltage */ float getMaxBatteryVoltage() { return bat->getMaxVoltage(); } /** * Set highest battery voltage * can't be below minBatteryVoltage */ void setMaxBatteryVoltage(float voltage) { bat->setMaxVoltage(voltage); } /** * Get the calculated voltage * formula: (adc pin value / adc precision * max voltage) + calibration */ float getVoltage() { return bat->getVoltage(); } /** * Get the mapped battery level (0 - 100) based on voltage * important: voltage can drop when a load is applied, so its only an estimate */ int8_t getBatteryLevel() { return bat->getLevel(); } /** * Get the configured calibration value * a offset value to fine-tune the calculated voltage. */ float getCalibration() { return bat->getCalibration(); } /** * Set the voltage calibration offset value * a offset value to fine-tune the calculated voltage. */ void setCalibration(float offset) { bat->setCalibration(offset); } /** * Set the voltage multiplier value * A multiplier that may need adjusting for different voltage divider setups */ void setVoltageMultiplier(float multiplier) { bat->setVoltageMultiplier(multiplier); } /* * Get the voltage multiplier value * A multiplier that may need adjusting for different voltage divider setups */ float getVoltageMultiplier() { return bat->getVoltageMultiplier(); } /** * Get auto-off feature enabled status * is auto-off enabled, true/false */ bool getAutoOffEnabled() { return autoOffEnabled; } /** * Set auto-off feature status */ void setAutoOffEnabled(bool enabled) { autoOffEnabled = enabled; } /** * Get auto-off threshold in percent (0-100) */ int8_t getAutoOffThreshold() { return autoOffThreshold; } /** * Set auto-off threshold in percent (0-100) */ void setAutoOffThreshold(int8_t threshold) { autoOffThreshold = min((int8_t)100, max((int8_t)0, threshold)); // when low power indicator is enabled the auto-off threshold cannot be above indicator threshold autoOffThreshold = lowPowerIndicatorEnabled /*&& autoOffEnabled*/ ? min(lowPowerIndicatorThreshold-1, (int)autoOffThreshold) : autoOffThreshold; } /** * Get low-power-indicator feature enabled status * is the low-power-indicator enabled, true/false */ bool getLowPowerIndicatorEnabled() { return lowPowerIndicatorEnabled; } /** * Set low-power-indicator feature status */ void setLowPowerIndicatorEnabled(bool enabled) { lowPowerIndicatorEnabled = enabled; } /** * Get low-power-indicator preset to activate when low power is detected */ int8_t getLowPowerIndicatorPreset() { return lowPowerIndicatorPreset; } /** * Set low-power-indicator preset to activate when low power is detected */ void setLowPowerIndicatorPreset(int8_t presetId) { // String tmp = ""; For what ever reason this doesn't work :( // lowPowerIndicatorPreset = getPresetName(presetId, tmp) ? presetId : lowPowerIndicatorPreset; lowPowerIndicatorPreset = presetId; } /* * Get low-power-indicator threshold in percent (0-100) */ int8_t getLowPowerIndicatorThreshold() { return lowPowerIndicatorThreshold; } /** * Set low-power-indicator threshold in percent (0-100) */ void setLowPowerIndicatorThreshold(int8_t threshold) { lowPowerIndicatorThreshold = threshold; // when auto-off is enabled the indicator threshold cannot be below auto-off threshold lowPowerIndicatorThreshold = autoOffEnabled /*&& lowPowerIndicatorEnabled*/ ? max(autoOffThreshold+1, (int)lowPowerIndicatorThreshold) : max(5, (int)lowPowerIndicatorThreshold); } /** * Get low-power-indicator duration in seconds */ int8_t getLowPowerIndicatorDuration() { return lowPowerIndicatorDuration; } /** * Set low-power-indicator duration in seconds */ void setLowPowerIndicatorDuration(int8_t duration) { lowPowerIndicatorDuration = duration; } /** * Get low-power-indicator status when the indication is done this returns true */ bool getLowPowerIndicatorDone() { return lowPowerIndicationDone; } /** * Set Home Assistant auto discovery */ void setHomeAssistantDiscovery(bool enable) { HomeAssistantDiscovery = enable; } /** * Get Home Assistant auto discovery */ bool getHomeAssistantDiscovery() { return HomeAssistantDiscovery; } }; // strings to reduce flash memory usage (used more than twice) const char UsermodBattery::_name[] PROGMEM = "Battery"; const char UsermodBattery::_readInterval[] PROGMEM = "interval"; const char UsermodBattery::_enabled[] PROGMEM = "enabled"; const char UsermodBattery::_threshold[] PROGMEM = "threshold"; const char UsermodBattery::_preset[] PROGMEM = "preset"; const char UsermodBattery::_duration[] PROGMEM = "duration"; const char UsermodBattery::_init[] PROGMEM = "init"; const char UsermodBattery::_haDiscovery[] PROGMEM = "HA-discovery"; static UsermodBattery battery; REGISTER_USERMOD(battery); ================================================ FILE: usermods/Battery/UMBattery.h ================================================ #ifndef UMBBattery_h #define UMBBattery_h #include "battery_defaults.h" /** * Battery base class * all other battery classes should inherit from this */ class UMBattery { private: protected: float minVoltage; float maxVoltage; float voltage; int8_t level = 100; float calibration; // offset or calibration value to fine tune the calculated voltage float voltageMultiplier; // ratio for the voltage divider float linearMapping(float v, float min, float max, float oMin = 0.0f, float oMax = 100.0f) { return (v-min) * (oMax-oMin) / (max-min) + oMin; } public: UMBattery() { this->setVoltageMultiplier(USERMOD_BATTERY_VOLTAGE_MULTIPLIER); this->setCalibration(USERMOD_BATTERY_CALIBRATION); } virtual void update(batteryConfig cfg) { if(cfg.minVoltage) this->setMinVoltage(cfg.minVoltage); if(cfg.maxVoltage) this->setMaxVoltage(cfg.maxVoltage); if(cfg.level) this->setLevel(cfg.level); if(cfg.calibration) this->setCalibration(cfg.calibration); if(cfg.voltageMultiplier) this->setVoltageMultiplier(cfg.voltageMultiplier); } /** * Corresponding battery curves * calculates the level in % (0-100) with given voltage and possible voltage range */ virtual float mapVoltage(float v, float min, float max) = 0; // { // example implementation, linear mapping // return (v-min) * 100 / (max-min); // }; virtual void calculateAndSetLevel(float voltage) = 0; /* * * Getter and Setter * */ /* * Get lowest configured battery voltage */ virtual float getMinVoltage() { return this->minVoltage; } /* * Set lowest battery voltage * can't be below 0 volt */ virtual void setMinVoltage(float voltage) { this->minVoltage = max(0.0f, voltage); } /* * Get highest configured battery voltage */ virtual float getMaxVoltage() { return this->maxVoltage; } /* * Set highest battery voltage * can't be below minVoltage */ virtual void setMaxVoltage(float voltage) { this->maxVoltage = max(getMinVoltage()+.5f, voltage); } float getVoltage() { return this->voltage; } /** * check if voltage is within specified voltage range, allow 10% over/under voltage */ void setVoltage(float voltage) { // this->voltage = ( (voltage < this->getMinVoltage() * 0.85f) || (voltage > this->getMaxVoltage() * 1.1f) ) // ? -1.0f // : voltage; this->voltage = voltage; } float getLevel() { return this->level; } void setLevel(float level) { this->level = constrain(level, 0.0f, 110.0f); } /* * Get the configured calibration value * a offset value to fine-tune the calculated voltage. */ virtual float getCalibration() { return calibration; } /* * Set the voltage calibration offset value * a offset value to fine-tune the calculated voltage. */ virtual void setCalibration(float offset) { calibration = offset; } /* * Get the configured calibration value * a value to set the voltage divider ratio */ virtual float getVoltageMultiplier() { return voltageMultiplier; } /* * Set the voltage multiplier value * a value to set the voltage divider ratio. */ virtual void setVoltageMultiplier(float multiplier) { voltageMultiplier = multiplier; } }; #endif ================================================ FILE: usermods/Battery/battery_defaults.h ================================================ #ifndef UMBDefaults_h #define UMBDefaults_h #include "wled.h" // pin defaults // for the esp32 it is best to use the ADC1: GPIO32 - GPIO39 // https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-reference/peripherals/adc.html #ifndef USERMOD_BATTERY_MEASUREMENT_PIN #ifdef ARDUINO_ARCH_ESP32 #define USERMOD_BATTERY_MEASUREMENT_PIN 35 #else //ESP8266 boards #define USERMOD_BATTERY_MEASUREMENT_PIN A0 #endif #endif // The initial delay before the first battery voltage reading after power-on. // This allows the voltage to stabilize before readings are taken, improving accuracy of initial reading. #ifndef USERMOD_BATTERY_INITIAL_DELAY #define USERMOD_BATTERY_INITIAL_DELAY 10000 // (milliseconds) #endif // the frequency to check the battery, 30 sec #ifndef USERMOD_BATTERY_MEASUREMENT_INTERVAL #define USERMOD_BATTERY_MEASUREMENT_INTERVAL 30000 #endif /* Default Battery Type * 0 = unkown * 1 = Lipo * 2 = Lion */ #ifndef USERMOD_BATTERY_DEFAULT_TYPE #define USERMOD_BATTERY_DEFAULT_TYPE 0 #endif /* * * Unkown 'Battery' defaults * */ #ifndef USERMOD_BATTERY_UNKOWN_MIN_VOLTAGE // Extra save defaults #define USERMOD_BATTERY_UNKOWN_MIN_VOLTAGE 3.3f #endif #ifndef USERMOD_BATTERY_UNKOWN_MAX_VOLTAGE #define USERMOD_BATTERY_UNKOWN_MAX_VOLTAGE 4.2f #endif /* * * Lithium polymer (Li-Po) defaults * */ #ifndef USERMOD_BATTERY_LIPO_MIN_VOLTAGE // LiPo "1S" Batteries should not be dischared below 3V !! #define USERMOD_BATTERY_LIPO_MIN_VOLTAGE 3.2f #endif #ifndef USERMOD_BATTERY_LIPO_MAX_VOLTAGE #define USERMOD_BATTERY_LIPO_MAX_VOLTAGE 4.2f #endif /* * * Lithium-ion (Li-Ion) defaults * */ #ifndef USERMOD_BATTERY_LION_MIN_VOLTAGE // default for 18650 battery #define USERMOD_BATTERY_LION_MIN_VOLTAGE 2.6f #endif #ifndef USERMOD_BATTERY_LION_MAX_VOLTAGE #define USERMOD_BATTERY_LION_MAX_VOLTAGE 4.2f #endif // the default ratio for the voltage divider #ifndef USERMOD_BATTERY_VOLTAGE_MULTIPLIER #ifdef ARDUINO_ARCH_ESP32 #define USERMOD_BATTERY_VOLTAGE_MULTIPLIER 2.0f #else //ESP8266 boards #define USERMOD_BATTERY_VOLTAGE_MULTIPLIER 4.2f #endif #endif #ifndef USERMOD_BATTERY_AVERAGING_ALPHA #define USERMOD_BATTERY_AVERAGING_ALPHA 0.1f #endif // offset or calibration value to fine tune the calculated voltage #ifndef USERMOD_BATTERY_CALIBRATION #define USERMOD_BATTERY_CALIBRATION 0 #endif // auto-off feature #ifndef USERMOD_BATTERY_AUTO_OFF_ENABLED #define USERMOD_BATTERY_AUTO_OFF_ENABLED true #endif #ifndef USERMOD_BATTERY_AUTO_OFF_THRESHOLD #define USERMOD_BATTERY_AUTO_OFF_THRESHOLD 10 #endif // low power indication feature #ifndef USERMOD_BATTERY_LOW_POWER_INDICATOR_ENABLED #define USERMOD_BATTERY_LOW_POWER_INDICATOR_ENABLED true #endif #ifndef USERMOD_BATTERY_LOW_POWER_INDICATOR_PRESET #define USERMOD_BATTERY_LOW_POWER_INDICATOR_PRESET 0 #endif #ifndef USERMOD_BATTERY_LOW_POWER_INDICATOR_THRESHOLD #define USERMOD_BATTERY_LOW_POWER_INDICATOR_THRESHOLD 20 #endif #ifndef USERMOD_BATTERY_LOW_POWER_INDICATOR_DURATION #define USERMOD_BATTERY_LOW_POWER_INDICATOR_DURATION 5 #endif // battery types typedef enum { unknown=0, lipo=1, lion=2 } batteryType; // used for initial configuration after boot typedef struct bconfig_t { batteryType type; float minVoltage; float maxVoltage; float voltage; // current voltage int8_t level; // current level float calibration; // offset or calibration value to fine tune the calculated voltage float voltageMultiplier; } batteryConfig; #endif ================================================ FILE: usermods/Battery/library.json ================================================ { "name": "Battery", "build": { "libArchive": false } } ================================================ FILE: usermods/Battery/readme.md ================================================

# Welcome to the battery usermod! 🔋 Enables battery level monitoring of your project.


## ⚙️ Features - 💯 Displays current battery voltage - 🚥 Displays battery level - 🚫 Auto-off with configurable threshold - 🚨 Low power indicator with many configuration possibilities

## 🎈 Installation In `platformio_override.ini` (or `platformio.ini`)
Under: `custom_usermods =`, add the line: `Battery`

[Example: platformio_override.ini](assets/installation_platformio_override_ini.png) |

## 🔌 Example wiring - (see [Useful Links](#useful-links)).

ESP8266
With a 100k Ohm resistor, connect the positive
side of the battery to pin `A0`.

ESP32 (+S2, S3, C3 etc...)
Use a voltage divider (two resistors of equal value).
Connect to ADC1 (GPIO32 - GPIO39). GPIO35 is Default.



## Define Your Options | Name | Unit | Description | | ----------------------------------------------- | ----------- |-------------------------------------------------------------------------------------- | | `USERMOD_BATTERY` | | Define this (in `my_config.h`) to have this usermod included wled00\usermods_list.cpp | | `USERMOD_BATTERY_MEASUREMENT_PIN` | | Defaults to A0 on ESP8266 and GPIO35 on ESP32 | | `USERMOD_BATTERY_MEASUREMENT_INTERVAL` | ms | Battery check interval. defaults to 30 seconds | | `USERMOD_BATTERY_INITIAL_DELAY` | ms | Delay before initial reading. defaults to 10 seconds to allow voltage stabilization | | `USERMOD_BATTERY_{TYPE}_MIN_VOLTAGE` | v | Minimum battery voltage. default is 2.6 (18650 battery standard) | | `USERMOD_BATTERY_{TYPE}_MAX_VOLTAGE` | v | Maximum battery voltage. default is 4.2 (18650 battery standard) | | `USERMOD_BATTERY_{TYPE}_TOTAL_CAPACITY` | mAh | The capacity of all cells in parallel summed up | | `USERMOD_BATTERY_{TYPE}_CALIBRATION` | | Offset / calibration number, fine tune the measured voltage by the microcontroller | | Auto-Off | --- | --- | | `USERMOD_BATTERY_AUTO_OFF_ENABLED` | true/false | Enables auto-off | | `USERMOD_BATTERY_AUTO_OFF_THRESHOLD` | % (0-100) | When this threshold is reached master power turns off | | Low-Power-Indicator | --- | --- | | `USERMOD_BATTERY_LOW_POWER_INDICATOR_ENABLED` | true/false | Enables low power indication | | `USERMOD_BATTERY_LOW_POWER_INDICATOR_PRESET` | preset id | When low power is detected then use this preset to indicate low power | | `USERMOD_BATTERY_LOW_POWER_INDICATOR_THRESHOLD` | % (0-100) | When this threshold is reached low power gets indicated | | `USERMOD_BATTERY_LOW_POWER_INDICATOR_DURATION` | seconds | For this long the configured preset is played | All parameters can be configured at runtime via the Usermods settings page.
**NOTICE:** Each Battery type can be pre-configured individualy (in `my_config.h`) | Name | Alias | `my_config.h` example | | --------------- | ------------- | ------------------------------------- | | Lithium Polymer | lipo (Li-Po) | `USERMOD_BATTERY_lipo_MIN_VOLTAGE` | | Lithium Ionen | lion (Li-Ion) | `USERMOD_BATTERY_lion_TOTAL_CAPACITY` |

## 🔧 Calibration The calibration number is a value that is added to the final computed voltage after it has been scaled by the voltage multiplier. It fine-tunes the voltage reading so that it more closely matches the actual battery voltage, compensating for inaccuracies inherent in the voltage divider resistors or the ESP's ADC measurements. Set calibration either in the Usermods settings page or at compile time in `my_config.h` or `platformio_override.ini`. It can be either a positive or negative number.

## ⚠️ Important Make sure you know your battery specifications! All batteries are **NOT** the same! Example: | Your battery specification table | | Options you can define | | --------------------------------- | --------------- | ----------------------------- | | Capacity | 3500mAh 12.5Wh | | | Minimum capacity | 3350mAh 11.9Wh | | | Rated voltage | 3.6V - 3.7V | | | **Charging end voltage** | **4.2V ± 0.05** | `USERMOD_BATTERY_MAX_VOLTAGE` | | **Discharge voltage** | **2.5V** | `USERMOD_BATTERY_MIN_VOLTAGE` | | Max. discharge current (constant) | 10A (10000mA) | | | max. charging current | 1.7A (1700mA) | | | ... | ... | ... | | .. | .. | .. | Specification from: [Molicel INR18650-M35A, 3500mAh 10A Lithium-ion battery, 3.6V - 3.7V](https://www.akkuteile.de/lithium-ionen-akkus/18650/molicel/molicel-inr18650-m35a-3500mah-10a-lithium-ionen-akku-3-6v-3-7v_100833)

## 🌐 Useful Links - https://lazyzero.de/elektronik/esp8266/wemos_d1_mini_a0/start - https://arduinodiy.wordpress.com/2016/12/25/monitoring-lipo-battery-voltage-with-wemos-d1-minibattery-shield-and-thingspeak/

## 📝 Change Log 2024-08-19 - Improved MQTT support - Added battery percentage & battery voltage as MQTT topic 2024-05-11 - Documentation updated 2024-04-30 - Integrate factory pattern to make it easier to add other / custom battery types - Update readme - Improved initial reading accuracy by delaying initial measurement to allow voltage to stabilize at power-on 2023-01-04 - Basic support for LiPo rechargeable batteries (`-D USERMOD_BATTERY_USE_LIPO`) - Improved support for ESP32 (read calibrated voltage) - Corrected config saving (measurement pin, and battery min/max were lost) - Various bugfixes 2022-12-25 - Added "auto-off" feature - Added "low-power-indication" feature - Added "calibration/offset" field to configuration page - Added getter and setter, so that user usermods could interact with this one - Update readme (added new options, made it markdownlint compliant) 2021-09-02 - Added "Battery voltage" to info - Added circuit diagram to readme - Added MQTT support, sending battery voltage - Minor fixes 2021-08-15 - Changed `USERMOD_BATTERY_MIN_VOLTAGE` to 2.6 volt as default for 18650 batteries - Updated readme, added specification table 2021-08-10 - Created ================================================ FILE: usermods/Battery/types/LionUMBattery.h ================================================ #ifndef UMBLion_h #define UMBLion_h #include "../battery_defaults.h" #include "../UMBattery.h" /** * LiOn Battery * */ class LionUMBattery : public UMBattery { private: public: LionUMBattery() : UMBattery() { this->setMinVoltage(USERMOD_BATTERY_LION_MIN_VOLTAGE); this->setMaxVoltage(USERMOD_BATTERY_LION_MAX_VOLTAGE); } float mapVoltage(float v, float min, float max) override { return this->linearMapping(v, min, max); // basic mapping }; void calculateAndSetLevel(float voltage) override { this->setLevel(this->mapVoltage(voltage, this->getMinVoltage(), this->getMaxVoltage())); }; virtual void setMaxVoltage(float voltage) override { this->maxVoltage = max(getMinVoltage()+1.0f, voltage); } }; #endif ================================================ FILE: usermods/Battery/types/LipoUMBattery.h ================================================ #ifndef UMBLipo_h #define UMBLipo_h #include "../battery_defaults.h" #include "../UMBattery.h" /** * LiPo Battery * */ class LipoUMBattery : public UMBattery { private: public: LipoUMBattery() : UMBattery() { this->setMinVoltage(USERMOD_BATTERY_LIPO_MIN_VOLTAGE); this->setMaxVoltage(USERMOD_BATTERY_LIPO_MAX_VOLTAGE); } /** * LiPo batteries have a differnt discharge curve, see * https://blog.ampow.com/lipo-voltage-chart/ */ float mapVoltage(float v, float min, float max) override { float lvl = 0.0f; lvl = this->linearMapping(v, min, max); // basic mapping if (lvl < 40.0f) lvl = this->linearMapping(lvl, 0, 40, 0, 12); // last 45% -> drops very quickly else { if (lvl < 90.0f) lvl = this->linearMapping(lvl, 40, 90, 12, 95); // 90% ... 40% -> almost linear drop else // level > 90% lvl = this->linearMapping(lvl, 90, 105, 95, 100); // highest 15% -> drop slowly } return lvl; }; void calculateAndSetLevel(float voltage) override { this->setLevel(this->mapVoltage(voltage, this->getMinVoltage(), this->getMaxVoltage())); }; virtual void setMaxVoltage(float voltage) override { this->maxVoltage = max(getMinVoltage()+0.7f, voltage); } }; #endif ================================================ FILE: usermods/Battery/types/UnkownUMBattery.h ================================================ #ifndef UMBUnkown_h #define UMBUnkown_h #include "../battery_defaults.h" #include "../UMBattery.h" /** * Unkown / Default Battery * */ class UnkownUMBattery : public UMBattery { private: public: UnkownUMBattery() : UMBattery() { this->setMinVoltage(USERMOD_BATTERY_UNKOWN_MIN_VOLTAGE); this->setMaxVoltage(USERMOD_BATTERY_UNKOWN_MAX_VOLTAGE); } void update(batteryConfig cfg) { if(cfg.minVoltage) this->setMinVoltage(cfg.minVoltage); else this->setMinVoltage(USERMOD_BATTERY_UNKOWN_MIN_VOLTAGE); if(cfg.maxVoltage) this->setMaxVoltage(cfg.maxVoltage); else this->setMaxVoltage(USERMOD_BATTERY_UNKOWN_MAX_VOLTAGE); } float mapVoltage(float v, float min, float max) override { return this->linearMapping(v, min, max); // basic mapping }; void calculateAndSetLevel(float voltage) override { this->setLevel(this->mapVoltage(voltage, this->getMinVoltage(), this->getMaxVoltage())); }; }; #endif ================================================ FILE: usermods/Cronixie/Cronixie.cpp ================================================ #include "wled.h" class UsermodCronixie : public Usermod { private: unsigned long lastTime = 0; char cronixieDisplay[7] = "HHMMSS"; byte _digitOut[6] = {10,10,10,10,10,10}; byte dP[6] = {255, 255, 255, 255, 255, 255}; // set your config variables to their boot default value (this can also be done in readFromConfig() or a constructor if you prefer) bool backlight = true; public: void initCronixie() { if (dP[0] == 255) // if dP[0] is 255, cronixie is not yet init'ed { setCronixie(); strip.getSegment(0).grouping = 10; // 10 LEDs per digit } } void setup() { } void loop() { if (!toki.isTick()) return; initCronixie(); _overlayCronixie(); strip.trigger(); } byte getSameCodeLength(char code, int index, char const cronixieDisplay[]) { byte counter = 0; for (int i = index+1; i < 6; i++) { if (cronixieDisplay[i] == code) { counter++; } else { return counter; } } return counter; } void setCronixie() { /* * digit purpose index * 0-9 | 0-9 (incl. random) * 10 | blank * 11 | blank, bg off * 12 | test upw. * 13 | test dnw. * 14 | binary AM/PM * 15 | BB upper +50 for no trailing 0 * 16 | BBB * 17 | BBBB * 18 | BBBBB * 19 | BBBBBB * 20 | H * 21 | HH * 22 | HHH * 23 | HHHH * 24 | M * 25 | MM * 26 | MMM * 27 | MMMM * 28 | MMMMM * 29 | MMMMMM * 30 | S * 31 | SS * 32 | SSS * 33 | SSSS * 34 | SSSSS * 35 | SSSSSS * 36 | Y * 37 | YY * 38 | YYYY * 39 | I * 40 | II * 41 | W * 42 | WW * 43 | D * 44 | DD * 45 | DDD * 46 | V * 47 | VV * 48 | VVV * 49 | VVVV * 50 | VVVVV * 51 | VVVVVV * 52 | v * 53 | vv * 54 | vvv * 55 | vvvv * 56 | vvvvv * 57 | vvvvvv */ //H HourLower | HH - Hour 24. | AH - Hour 12. | HHH Hour of Month | HHHH Hour of Year //M MinuteUpper | MM Minute of Hour | MMM Minute of 12h | MMMM Minute of Day | MMMMM Minute of Month | MMMMMM Minute of Year //S SecondUpper | SS Second of Minute | SSS Second of 10 Minute | SSSS Second of Hour | SSSSS Second of Day | SSSSSS Second of Week //B AM/PM | BB 0-6/6-12/12-18/18-24 | BBB 0-3... | BBBB 0-1.5... | BBBBB 0-1 | BBBBBB 0-0.5 //Y YearLower | YY - Year LU | YYYY - Std. //I MonthLower | II - Month of Year //W Week of Month | WW Week of Year //D Day of Week | DD Day Of Month | DDD Day Of Year DEBUG_PRINT(F("cset ")); DEBUG_PRINTLN(cronixieDisplay); for (int i = 0; i < 6; i++) { dP[i] = 10; switch (cronixieDisplay[i]) { case '_': dP[i] = 10; break; case '-': dP[i] = 11; break; case 'r': dP[i] = random(1,7); break; //random btw. 1-6 case 'R': dP[i] = random(0,10); break; //random btw. 0-9 //case 't': break; //Test upw. //case 'T': break; //Test dnw. case 'b': dP[i] = 14 + getSameCodeLength('b',i,cronixieDisplay); i = i+dP[i]-14; break; case 'B': dP[i] = 14 + getSameCodeLength('B',i,cronixieDisplay); i = i+dP[i]-14; break; case 'h': dP[i] = 70 + getSameCodeLength('h',i,cronixieDisplay); i = i+dP[i]-70; break; case 'H': dP[i] = 20 + getSameCodeLength('H',i,cronixieDisplay); i = i+dP[i]-20; break; case 'A': dP[i] = 108; i++; break; case 'a': dP[i] = 58; i++; break; case 'm': dP[i] = 74 + getSameCodeLength('m',i,cronixieDisplay); i = i+dP[i]-74; break; case 'M': dP[i] = 24 + getSameCodeLength('M',i,cronixieDisplay); i = i+dP[i]-24; break; case 's': dP[i] = 80 + getSameCodeLength('s',i,cronixieDisplay); i = i+dP[i]-80; break; //refresh more often bc. of secs case 'S': dP[i] = 30 + getSameCodeLength('S',i,cronixieDisplay); i = i+dP[i]-30; break; case 'Y': dP[i] = 36 + getSameCodeLength('Y',i,cronixieDisplay); i = i+dP[i]-36; break; case 'y': dP[i] = 86 + getSameCodeLength('y',i,cronixieDisplay); i = i+dP[i]-86; break; case 'I': dP[i] = 39 + getSameCodeLength('I',i,cronixieDisplay); i = i+dP[i]-39; break; //Month. Don't ask me why month and minute both start with M. case 'i': dP[i] = 89 + getSameCodeLength('i',i,cronixieDisplay); i = i+dP[i]-89; break; //case 'W': break; //case 'w': break; case 'D': dP[i] = 43 + getSameCodeLength('D',i,cronixieDisplay); i = i+dP[i]-43; break; case 'd': dP[i] = 93 + getSameCodeLength('d',i,cronixieDisplay); i = i+dP[i]-93; break; case '0': dP[i] = 0; break; case '1': dP[i] = 1; break; case '2': dP[i] = 2; break; case '3': dP[i] = 3; break; case '4': dP[i] = 4; break; case '5': dP[i] = 5; break; case '6': dP[i] = 6; break; case '7': dP[i] = 7; break; case '8': dP[i] = 8; break; case '9': dP[i] = 9; break; //case 'V': break; //user var0 //case 'v': break; //user var1 } } DEBUG_PRINT(F("result ")); for (int i = 0; i < 5; i++) { DEBUG_PRINT((int)dP[i]); DEBUG_PRINT(" "); } DEBUG_PRINTLN((int)dP[5]); _overlayCronixie(); // refresh } void _overlayCronixie() { byte h = hour(localTime); byte h0 = h; byte m = minute(localTime); byte s = second(localTime); byte d = day(localTime); byte mi = month(localTime); int y = year(localTime); //this has to be changed in time for 22nd century y -= 2000; if (y<0) y += 30; //makes countdown work if (useAMPM && !countdownMode) { if (h>12) h-=12; else if (h==0) h+=12; } for (int i = 0; i < 6; i++) { if (dP[i] < 12) _digitOut[i] = dP[i]; else { if (dP[i] < 65) { switch(dP[i]) { case 21: _digitOut[i] = h/10; _digitOut[i+1] = h- _digitOut[i]*10; i++; break; //HH case 25: _digitOut[i] = m/10; _digitOut[i+1] = m- _digitOut[i]*10; i++; break; //MM case 31: _digitOut[i] = s/10; _digitOut[i+1] = s- _digitOut[i]*10; i++; break; //SS case 20: _digitOut[i] = h- (h/10)*10; break; //H case 24: _digitOut[i] = m/10; break; //M case 30: _digitOut[i] = s/10; break; //S case 43: _digitOut[i] = weekday(localTime); _digitOut[i]--; if (_digitOut[i]<1) _digitOut[i]= 7; break; //D case 44: _digitOut[i] = d/10; _digitOut[i+1] = d- _digitOut[i]*10; i++; break; //DD case 40: _digitOut[i] = mi/10; _digitOut[i+1] = mi- _digitOut[i]*10; i++; break; //II case 37: _digitOut[i] = y/10; _digitOut[i+1] = y- _digitOut[i]*10; i++; break; //YY case 39: _digitOut[i] = 2; _digitOut[i+1] = 0; _digitOut[i+2] = y/10; _digitOut[i+3] = y- _digitOut[i+2]*10; i+=3; break; //YYYY //case 16: _digitOut[i+2] = ((h0/3)&1)?1:0; i++; //BBB (BBBB NI) //case 15: _digitOut[i+1] = (h0>17 || (h0>5 && h0<12))?1:0; i++; //BB case 14: _digitOut[i] = (h0>11)?1:0; break; //B } } else { switch(dP[i]) { case 71: _digitOut[i] = h/10; _digitOut[i+1] = h- _digitOut[i]*10; if(_digitOut[i] == 0) _digitOut[i]=10; i++; break; //hh case 75: _digitOut[i] = m/10; _digitOut[i+1] = m- _digitOut[i]*10; if(_digitOut[i] == 0) _digitOut[i]=10; i++; break; //mm case 81: _digitOut[i] = s/10; _digitOut[i+1] = s- _digitOut[i]*10; if(_digitOut[i] == 0) _digitOut[i]=10; i++; break; //ss //case 66: _digitOut[i+2] = ((h0/3)&1)?1:10; i++; //bbb (bbbb NI) //case 65: _digitOut[i+1] = (h0>17 || (h0>5 && h0<12))?1:10; i++; //bb case 64: _digitOut[i] = (h0>11)?1:10; break; //b case 93: _digitOut[i] = weekday(localTime); _digitOut[i]--; if (_digitOut[i]<1) _digitOut[i]= 7; break; //d case 94: _digitOut[i] = d/10; _digitOut[i+1] = d- _digitOut[i]*10; if(_digitOut[i] == 0) _digitOut[i]=10; i++; break; //dd case 90: _digitOut[i] = mi/10; _digitOut[i+1] = mi- _digitOut[i]*10; if(_digitOut[i] == 0) _digitOut[i]=10; i++; break; //ii case 87: _digitOut[i] = y/10; _digitOut[i+1] = y- _digitOut[i]*10; i++; break; //yy case 89: _digitOut[i] = 2; _digitOut[i+1] = 0; _digitOut[i+2] = y/10; _digitOut[i+3] = y- _digitOut[i+2]*10; i+=3; break; //yyyy } } } } } void handleOverlayDraw() { byte offsets[] = {5, 0, 6, 1, 7, 2, 8, 3, 9, 4}; for (uint16_t i = 0; i < 6; i++) { byte o = 10*i; byte excl = 10; if(_digitOut[i] < 10) excl = offsets[_digitOut[i]]; excl += o; if (backlight && _digitOut[i] <11) { uint32_t col = strip.getSegment(0).colors[1]; for (uint16_t j=o; j< o+10; j++) { if (j != excl) strip.setPixelColor(j, col); } } else { for (uint16_t j=o; j< o+10; j++) { if (j != excl) strip.setPixelColor(j, 0); } } } } void addToJsonState(JsonObject& root) { root["nx"] = cronixieDisplay; } void readFromJsonState(JsonObject& root) { if (root["nx"].is()) { strncpy(cronixieDisplay, root["nx"], 6); setCronixie(); } } void addToConfig(JsonObject& root) { JsonObject top = root.createNestedObject(F("Cronixie")); top["backlight"] = backlight; } bool readFromConfig(JsonObject& root) { // default settings values could be set here (or below using the 3-argument getJsonValue()) instead of in the class definition or constructor // setting them inside readFromConfig() is slightly more robust, handling the rare but plausible use case of single value being missing after boot (e.g. if the cfg.json was manually edited and a value was removed) JsonObject top = root[F("Cronixie")]; bool configComplete = !top.isNull(); configComplete &= getJsonValue(top["backlight"], backlight); return configComplete; } uint16_t getId() { return USERMOD_ID_CRONIXIE; } }; static UsermodCronixie cronixie; REGISTER_USERMOD(cronixie); ================================================ FILE: usermods/Cronixie/library.json ================================================ { "name": "Cronixie", "build": { "libArchive": false } } ================================================ FILE: usermods/Cronixie/readme.md ================================================ # Cronixie clock usermod This usermod supports driving the Cronixie M and L clock kits by Diamex. ## Installation Compile and upload after adding `Cronixie` to `custom_usermods` of your PlatformIO environment. Make sure the Auto Brightness Limiter is enabled at 420mA (!) and configure 60 WS281x LEDs. ================================================ FILE: usermods/DHT/DHT.cpp ================================================ #include "wled.h" #ifdef WLED_DISABLE_MQTT #error "This user mod requires MQTT to be enabled." #endif #include // USERMOD_DHT_DHTTYPE: // 11 // DHT 11 // 21 // DHT 21 // 22 // DHT 22 (AM2302), AM2321 *** default #ifndef USERMOD_DHT_DHTTYPE #define USERMOD_DHT_DHTTYPE 22 #endif #if USERMOD_DHT_DHTTYPE == 11 #define DHTTYPE DHT_TYPE_11 #elif USERMOD_DHT_DHTTYPE == 21 #define DHTTYPE DHT_TYPE_21 #elif USERMOD_DHT_DHTTYPE == 22 #define DHTTYPE DHT_TYPE_22 #endif // Connect pin 1 (on the left) of the sensor to +5V // NOTE: If using a board with 3.3V logic like an Arduino Due connect pin 1 // to 3.3V instead of 5V! // Connect pin 2 of the sensor to whatever your DHTPIN is // NOTE: Pin defaults below are for QuinLed Dig-Uno's Q2 on the board // Connect pin 4 (on the right) of the sensor to GROUND // NOTE: If using a bare sensor (AM*), Connect a 10K resistor from pin 2 // (data) to pin 1 (power) of the sensor. DHT* boards have the pullup already #ifdef USERMOD_DHT_PIN #define DHTPIN USERMOD_DHT_PIN #else #ifdef ARDUINO_ARCH_ESP32 #define DHTPIN 21 #else //ESP8266 boards #define DHTPIN 4 #endif #endif // the frequency to check sensor, 1 minute #ifndef USERMOD_DHT_MEASUREMENT_INTERVAL #define USERMOD_DHT_MEASUREMENT_INTERVAL 60000 #endif // how many seconds after boot to take first measurement, 90 seconds // 90 gives enough time to OTA update firmware if this crashes #ifndef USERMOD_DHT_FIRST_MEASUREMENT_AT #define USERMOD_DHT_FIRST_MEASUREMENT_AT 90000 #endif // from COOLDOWN_TIME in dht_nonblocking.cpp #define DHT_TIMEOUT_TIME 10000 DHT_nonblocking dht_sensor(DHTPIN, DHTTYPE); class UsermodDHT : public Usermod { private: unsigned long nextReadTime = 0; unsigned long lastReadTime = 0; float humidity, temperature = 0; bool initializing = true; bool disabled = false; #ifdef USERMOD_DHT_MQTT char dhtMqttTopic[64]; size_t dhtMqttTopicLen; #endif #ifdef USERMOD_DHT_STATS unsigned long nextResetStatsTime = 0; uint16_t updates = 0; uint16_t clean_updates = 0; uint16_t errors = 0; unsigned long maxDelay = 0; unsigned long currentIteration = 0; unsigned long maxIteration = 0; #endif public: void setup() { nextReadTime = millis() + USERMOD_DHT_FIRST_MEASUREMENT_AT; lastReadTime = millis(); #ifdef USERMOD_DHT_MQTT sprintf(dhtMqttTopic, "%s/dht", mqttDeviceTopic); dhtMqttTopicLen = strlen(dhtMqttTopic); #endif #ifdef USERMOD_DHT_STATS nextResetStatsTime = millis() + 60*60*1000; #endif } void loop() { if (disabled) { return; } if (millis() < nextReadTime) { return; } #ifdef USERMOD_DHT_STATS if (millis() >= nextResetStatsTime) { nextResetStatsTime += 60*60*1000; errors = 0; updates = 0; clean_updates = 0; } unsigned long dcalc = millis(); if (currentIteration == 0) { currentIteration = millis(); } #endif float tempC; if (dht_sensor.measure(&tempC, &humidity)) { #ifdef USERMOD_DHT_CELSIUS temperature = tempC; #else temperature = tempC * 9 / 5 + 32; #endif #ifdef USERMOD_DHT_MQTT // 10^n where n is number of decimal places to display in mqtt message. Please adjust buff size together with this constant #define FLOAT_PREC 100 if (WLED_MQTT_CONNECTED) { char buff[10]; strcpy(dhtMqttTopic + dhtMqttTopicLen, "/temperature"); sprintf(buff, "%d.%d", (int)temperature, ((int)(temperature * FLOAT_PREC)) % FLOAT_PREC); mqtt->publish(dhtMqttTopic, 0, false, buff); sprintf(buff, "%d.%d", (int)humidity, ((int)(humidity * FLOAT_PREC)) % FLOAT_PREC); strcpy(dhtMqttTopic + dhtMqttTopicLen, "/humidity"); mqtt->publish(dhtMqttTopic, 0, false, buff); dhtMqttTopic[dhtMqttTopicLen] = '\0'; } #undef FLOAT_PREC #endif nextReadTime = millis() + USERMOD_DHT_MEASUREMENT_INTERVAL; lastReadTime = millis(); initializing = false; #ifdef USERMOD_DHT_STATS unsigned long icalc = millis() - currentIteration; if (icalc > maxIteration) { maxIteration = icalc; } if (icalc > DHT_TIMEOUT_TIME) { errors += icalc/DHT_TIMEOUT_TIME; } else { clean_updates += 1; } updates += 1; currentIteration = 0; #endif } #ifdef USERMOD_DHT_STATS dcalc = millis() - dcalc; if (dcalc > maxDelay) { maxDelay = dcalc; } #endif if (((millis() - lastReadTime) > 10*USERMOD_DHT_MEASUREMENT_INTERVAL)) { disabled = true; } } void addToJsonInfo(JsonObject& root) { if (disabled) { return; } JsonObject user = root["u"]; if (user.isNull()) user = root.createNestedObject("u"); JsonArray temp = user.createNestedArray("Temperature"); JsonArray hum = user.createNestedArray("Humidity"); #ifdef USERMOD_DHT_STATS JsonArray next = user.createNestedArray("next"); if (nextReadTime >= millis()) { next.add((nextReadTime - millis()) / 1000); next.add(" sec until read"); } else { next.add((millis() - nextReadTime) / 1000); next.add(" sec active reading"); } JsonArray last = user.createNestedArray("last"); last.add((millis() - lastReadTime) / 60000); last.add(" min since read"); JsonArray err = user.createNestedArray("errors"); err.add(errors); err.add(" Errors"); JsonArray upd = user.createNestedArray("updates"); upd.add(updates); upd.add(" Updates"); JsonArray cupd = user.createNestedArray("cleanUpdates"); cupd.add(clean_updates); cupd.add(" Updates"); JsonArray iter = user.createNestedArray("maxIter"); iter.add(maxIteration); iter.add(" ms"); JsonArray delay = user.createNestedArray("maxDelay"); delay.add(maxDelay); delay.add(" ms"); #endif if (initializing) { // if we haven't read the sensor yet, let the user know // that we are still waiting for the first measurement temp.add((nextReadTime - millis()) / 1000); temp.add(" sec until read"); hum.add((nextReadTime - millis()) / 1000); hum.add(" sec until read"); return; } hum.add(humidity); hum.add("%"); temp.add(temperature); #ifdef USERMOD_DHT_CELSIUS temp.add("°C"); #else temp.add("°F"); #endif } uint16_t getId() { return USERMOD_ID_DHT; } }; static UsermodDHT dht; REGISTER_USERMOD(dht); ================================================ FILE: usermods/DHT/library.json ================================================ { "name": "DHT", "build": { "libArchive": false}, "dependencies": { "DHT_nonblocking":"https://github.com/alwynallan/DHT_nonblocking" } } ================================================ FILE: usermods/DHT/readme.md ================================================ # DHT Temperature/Humidity sensor usermod This usermod will read from an attached DHT22 or DHT11 humidity and temperature sensor. The sensor readings are displayed in the Info section of the web UI (and optionally sent to an MQTT broker). If sensor is not detected after 10 update intervals, the usermod will be disabled. If enabled, measured temperature and humidity will be published to the following MQTT topics * `{devceTopic}/dht/temperature` * `{devceTopic}/dht/humidity` ## Installation Copy the example `platformio_override.ini` to the root directory. This file should be placed in the same directory as `platformio.ini`. ### Define Your Options * `USERMOD_DHT_DHTTYPE` - DHT model: 11, 21, 22 for DHT11, DHT21, or DHT22, defaults to 22/DHT22 * `USERMOD_DHT_PIN` - pin to which DTH is connected, defaults to Q2 pin on QuinLed Dig-Uno's board * `USERMOD_DHT_CELSIUS` - define this to report temperatures in degrees Celsius, otherwise Fahrenheit will be reported * `USERMOD_DHT_MEASUREMENT_INTERVAL` - the number of milliseconds between measurements, defaults to 60000 ms * `USERMOD_DHT_FIRST_MEASUREMENT_AT` - the number of milliseconds after boot to take first measurement, defaults to 90000 ms * `USERMOD_DHT_MQTT` - publish measurements to an MQTT broker * `USERMOD_DHT_STATS` - For debug, report delay stats ## Project link * [QuinLED-Dig-Uno](https://quinled.info/2018/09/15/quinled-dig-uno/) - Project link ### PlatformIO requirements If you are using `platformio_override.ini`, you should be able to refresh the task list and see your custom task, for example `env:d1_mini_usermod_dht_C`. If not, you can add the libraries and dependencies into `platformio.ini` as you see fit. ## Change Log 2022-10-15 * Add ability to publish sensor readings to an MQTT broker * fix compilation error for sample [env:d1_mini_usermod_dht_C] task 2020-02-04 * Change default QuinLed pin to Q2 * Instead of trying to keep updates at constant cadence, space out readings by measurement interval. Hopefully, this helps eliminate occasional bursts of readings with errors * Add some more (optional) stats 2020-02-03 * Due to poor readouts on ESP32 with previous DHT library, rewrote to use https://github.com/alwynallan/DHT_nonblocking * The new library serializes/delays up to 5ms for the sensor readout 2020-02-02 * Created ================================================ FILE: usermods/EXAMPLE/library.json ================================================ { "name": "EXAMPLE", "build": { "libArchive": false }, "dependencies": {} } ================================================ FILE: usermods/EXAMPLE/readme.md ================================================ # Usermods API v2 example usermod In this usermod file you can find the documentation on how to take advantage of the new version 2 usermods! ## Installation Add `EXAMPLE` to `custom_usermods` in your PlatformIO environment and compile! _(You shouldn't need to actually install this, it does nothing useful)_ ================================================ FILE: usermods/EXAMPLE/usermod_v2_example.cpp ================================================ #include "wled.h" /* * Usermods allow you to add own functionality to WLED more easily * See: https://github.com/wled-dev/WLED/wiki/Add-own-functionality * * This is an example for a v2 usermod. * v2 usermods are class inheritance based and can (but don't have to) implement more functions, each of them is shown in this example. * Multiple v2 usermods can be added to one compilation easily. * * Creating a usermod: * This file serves as an example. If you want to create a usermod, it is recommended to use usermod_v2_empty.h from the usermods folder as a template. * Please remember to rename the class and file to a descriptive name. * You may also use multiple .h and .cpp files. * * Using a usermod: * 1. Copy the usermod into the sketch folder (same folder as wled00.ino) * 2. Register the usermod by adding #include "usermod_filename.h" in the top and registerUsermod(new MyUsermodClass()) in the bottom of usermods_list.cpp */ //class name. Use something descriptive and leave the ": public Usermod" part :) class MyExampleUsermod : public Usermod { private: // Private class members. You can declare variables and functions only accessible to your usermod here bool enabled = false; bool initDone = false; unsigned long lastTime = 0; // set your config variables to their boot default value (this can also be done in readFromConfig() or a constructor if you prefer) bool testBool = false; unsigned long testULong = 42424242; float testFloat = 42.42; String testString = "Forty-Two"; // These config variables have defaults set inside readFromConfig() int testInt; long testLong; int8_t testPins[2]; // string that are used multiple time (this will save some flash memory) static const char _name[]; static const char _enabled[]; // any private methods should go here (non-inline method should be defined out of class) void publishMqtt(const char* state, bool retain = false); // example for publishing MQTT message public: // non WLED related methods, may be used for data exchange between usermods (non-inline methods should be defined out of class) /** * Enable/Disable the usermod */ inline void enable(bool enable) { enabled = enable; } /** * Get usermod enabled/disabled state */ inline bool isEnabled() { return enabled; } // in such case add the following to another usermod: // in private vars: // #ifdef USERMOD_EXAMPLE // MyExampleUsermod* UM; // #endif // in setup() // #ifdef USERMOD_EXAMPLE // UM = (MyExampleUsermod*) UsermodManager::lookup(USERMOD_ID_EXAMPLE); // #endif // somewhere in loop() or other member method // #ifdef USERMOD_EXAMPLE // if (UM != nullptr) isExampleEnabled = UM->isEnabled(); // if (!isExampleEnabled) UM->enable(true); // #endif // methods called by WLED (can be inlined as they are called only once but if you call them explicitly define them out of class) /* * setup() is called once at boot. WiFi is not yet connected at this point. * readFromConfig() is called prior to setup() * You can use it to initialize variables, sensors or similar. */ void setup() override { // do your set-up here //Serial.println("Hello from my usermod!"); initDone = true; } /* * connected() is called every time the WiFi is (re)connected * Use it to initialize network interfaces */ void connected() override { //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() override { // if usermod is disabled or called during strip updating just exit // NOTE: on very long strips strip.isUpdating() may always return true so update accordingly if (!enabled || strip.isUpdating()) return; // do your magic here if (millis() - lastTime > 1000) { //Serial.println("I'm alive!"); lastTime = millis(); } } /* * 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) override { // if "u" object does not exist yet wee need to create it JsonObject user = root["u"]; if (user.isNull()) user = root.createNestedObject("u"); //this code adds "u":{"ExampleUsermod":[20," lux"]} to the info object //int reading = 20; //JsonArray lightArr = user.createNestedArray(FPSTR(_name))); //name //lightArr.add(reading); //value //lightArr.add(F(" lux")); //unit // if you are implementing a sensor usermod, you may publish sensor data //JsonObject sensor = root[F("sensor")]; //if (sensor.isNull()) sensor = root.createNestedObject(F("sensor")); //temp = sensor.createNestedArray(F("light")); //temp.add(reading); //temp.add(F("lux")); } /* * 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) override { if (!initDone || !enabled) return; // prevent crash on boot applyPreset() JsonObject usermod = root[FPSTR(_name)]; if (usermod.isNull()) usermod = root.createNestedObject(FPSTR(_name)); //usermod["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) override { if (!initDone) return; // prevent crash on boot applyPreset() JsonObject usermod = root[FPSTR(_name)]; if (!usermod.isNull()) { // expect JSON usermod data in usermod name object: {"ExampleUsermod:{"user0":10}"} userVar0 = usermod["user0"] | userVar0; //if "user0" key exists in JSON, update, else keep old value } // you can as well check WLED state JSON keys //if (root["bri"] == 255) Serial.println(F("Don't burn down your garage!")); } /* * 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 make your settings editable through the Usermod Settings page automatically. * * Usermod Settings Overview: * - Numeric values are treated as floats in the browser. * - If the numeric value entered into the browser contains a decimal point, it will be parsed as a C float * before being returned to the Usermod. The float data type has only 6-7 decimal digits of precision, and * doubles are not supported, numbers will be rounded to the nearest float value when being parsed. * The range accepted by the input field is +/- 1.175494351e-38 to +/- 3.402823466e+38. * - If the numeric value entered into the browser doesn't contain a decimal point, it will be parsed as a * C int32_t (range: -2147483648 to 2147483647) before being returned to the usermod. * Overflows or underflows are truncated to the max/min value for an int32_t, and again truncated to the type * used in the Usermod when reading the value from ArduinoJson. * - Pin values can be treated differently from an integer value by using the key name "pin" * - "pin" can contain a single or array of integer values * - On the Usermod Settings page there is simple checking for pin conflicts and warnings for special pins * - Red color indicates a conflict. Yellow color indicates a pin with a warning (e.g. an input-only pin) * - Tip: use int8_t to store the pin value in the Usermod, so a -1 value (pin not set) can be used * * See usermod_v2_auto_save.h for an example that saves Flash space by reusing ArduinoJson key name strings * * If you need a dedicated settings page with custom layout for your Usermod, that takes a lot more work. * You will have to add the setting to the HTML, xml.cpp and set.cpp manually. * See the WLED Soundreactive fork (code and wiki) for reference. https://github.com/atuline/WLED * * I highly recommend checking out the basics of ArduinoJson serialization and deserialization in order to use custom settings! */ void addToConfig(JsonObject& root) override { JsonObject top = root.createNestedObject(FPSTR(_name)); top[FPSTR(_enabled)] = enabled; //save these vars persistently whenever settings are saved top["great"] = userVar0; top["testBool"] = testBool; top["testInt"] = testInt; top["testLong"] = testLong; top["testULong"] = testULong; top["testFloat"] = testFloat; top["testString"] = testString; JsonArray pinArray = top.createNestedArray("pin"); pinArray.add(testPins[0]); pinArray.add(testPins[1]); } /* * 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 immediately after boot, or after saving on the Usermod Settings page) * * 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 :) * * Return true in case the config values returned from Usermod Settings were complete, or false if you'd like WLED to save your defaults to disk (so any missing values are editable in Usermod Settings) * * getJsonValue() returns false if the value is missing, or copies the value into the variable provided and returns true if the value is present * The configComplete variable is true only if the "exampleUsermod" object and all values are present. If any values are missing, WLED will know to call addToConfig() to save them * * This function is guaranteed to be called on boot, but could also be called every time settings are updated */ bool readFromConfig(JsonObject& root) override { // default settings values could be set here (or below using the 3-argument getJsonValue()) instead of in the class definition or constructor // setting them inside readFromConfig() is slightly more robust, handling the rare but plausible use case of single value being missing after boot (e.g. if the cfg.json was manually edited and a value was removed) JsonObject top = root[FPSTR(_name)]; bool configComplete = !top.isNull(); configComplete &= getJsonValue(top["great"], userVar0); configComplete &= getJsonValue(top["testBool"], testBool); configComplete &= getJsonValue(top["testULong"], testULong); configComplete &= getJsonValue(top["testFloat"], testFloat); configComplete &= getJsonValue(top["testString"], testString); // A 3-argument getJsonValue() assigns the 3rd argument as a default value if the Json value is missing configComplete &= getJsonValue(top["testInt"], testInt, 42); configComplete &= getJsonValue(top["testLong"], testLong, -42424242); // "pin" fields have special handling in settings page (or some_pin as well) configComplete &= getJsonValue(top["pin"][0], testPins[0], -1); configComplete &= getJsonValue(top["pin"][1], testPins[1], -1); return configComplete; } /* * appendConfigData() is called when user enters usermod settings page * it may add additional metadata for certain entry fields (adding drop down is possible) * be careful not to add too much as oappend() buffer is limited to 3k */ void appendConfigData() override { oappend(F("addInfo('")); oappend(String(FPSTR(_name)).c_str()); oappend(F(":great")); oappend(F("',1,'(this is a great config value)');")); oappend(F("addInfo('")); oappend(String(FPSTR(_name)).c_str()); oappend(F(":testString")); oappend(F("',1,'enter any string you want');")); oappend(F("dd=addDropdown('")); oappend(String(FPSTR(_name)).c_str()); oappend(F("','testInt');")); oappend(F("addOption(dd,'Nothing',0);")); oappend(F("addOption(dd,'Everything',42);")); } /* * handleOverlayDraw() is called just before every show() (LED strip update frame) after effects have set the colors. * Use this to blank out some LEDs or set them to a different color regardless of the set effect mode. * Commonly used for custom clocks (Cronixie, 7 segment) */ void handleOverlayDraw() override { //strip.setPixelColor(0, RGBW32(0,0,0,0)) // set the first pixel to black } /** * handleButton() can be used to override default button behaviour. Returning true * will prevent button working in a default way. * Replicating button.cpp */ bool handleButton(uint8_t b) override { yield(); // ignore certain button types as they may have other consequences if (!enabled || buttons[b].type == BTN_TYPE_NONE || buttons[b].type == BTN_TYPE_RESERVED || buttons[b].type == BTN_TYPE_PIR_SENSOR || buttons[b].type == BTN_TYPE_ANALOG || buttons[b].type == BTN_TYPE_ANALOG_INVERTED) { return false; } bool handled = false; // do your button handling here return handled; } #ifndef WLED_DISABLE_MQTT /** * handling of MQTT message * topic only contains stripped topic (part after /wled/MAC) */ bool onMqttMessage(char* topic, char* payload) override { // check if we received a command //if (strlen(topic) == 8 && strncmp_P(topic, PSTR("/command"), 8) == 0) { // String action = payload; // if (action == "on") { // enabled = true; // return true; // } else if (action == "off") { // enabled = false; // return true; // } else if (action == "toggle") { // enabled = !enabled; // return true; // } //} return false; } /** * onMqttConnect() is called when MQTT connection is established */ void onMqttConnect(bool sessionPresent) override { // do any MQTT related initialisation here //publishMqtt("I am alive!"); } #endif /** * onStateChanged() is used to detect WLED state change * @mode parameter is CALL_MODE_... parameter used for notifications */ void onStateChange(uint8_t mode) override { // do something if WLED state changed (color, brightness, effect, preset, etc) } /* * 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() override { return USERMOD_ID_EXAMPLE; } //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! }; // add more strings here to reduce flash memory usage const char MyExampleUsermod::_name[] PROGMEM = "ExampleUsermod"; const char MyExampleUsermod::_enabled[] PROGMEM = "enabled"; // implementation of non-inline member methods void MyExampleUsermod::publishMqtt(const char* state, bool retain) { #ifndef WLED_DISABLE_MQTT //Check if MQTT Connected, otherwise it will crash the 8266 if (WLED_MQTT_CONNECTED) { char subuf[64]; strcpy(subuf, mqttDeviceTopic); strcat_P(subuf, PSTR("/example")); mqtt->publish(subuf, 0, retain, state); } #endif } static MyExampleUsermod example_usermod; REGISTER_USERMOD(example_usermod); ================================================ FILE: usermods/EleksTube_IPS/ChipSelect.h ================================================ #ifndef CHIP_SELECT_H #define CHIP_SELECT_H #include "Hardware.h" /* * `digit`s are as defined in Hardware.h, 0 == seconds ones, 5 == hours tens. */ class ChipSelect { private: uint8_t digits_map; const uint8_t all_on = 0x3F; const uint8_t all_off = 0x00; public: ChipSelect() : digits_map(all_off) {} void update() { // Documented in README.md. Q7 and Q6 are unused. Q5 is Seconds Ones, Q0 is Hours Tens. // Q7 is the first bit written, Q0 is the last. So we push two dummy bits, then start with // Seconds Ones and end with Hours Tens. // CS is Active Low, but digits_map is 1 for enable, 0 for disable. So we bit-wise NOT first. uint8_t to_shift = (~digits_map) << 2; digitalWrite(CSSR_LATCH_PIN, LOW); shiftOut(CSSR_DATA_PIN, CSSR_CLOCK_PIN, LSBFIRST, to_shift); digitalWrite(CSSR_LATCH_PIN, HIGH); } void begin() { pinMode(CSSR_LATCH_PIN, OUTPUT); pinMode(CSSR_DATA_PIN, OUTPUT); pinMode(CSSR_CLOCK_PIN, OUTPUT); digitalWrite(CSSR_DATA_PIN, LOW); digitalWrite(CSSR_CLOCK_PIN, LOW); digitalWrite(CSSR_LATCH_PIN, LOW); update(); } // These speak the indexes defined in Hardware.h. // So 0 is disabled, 1 is enabled (even though CS is active low, this gets mapped.) // So bit 0 (LSB), is index 0, is SECONDS_ONES // Translation to what the 74HC595 uses is done in update() void setDigitMap(uint8_t map, bool update_=true) { digits_map = map; if (update_) update(); } uint8_t getDigitMap() { return digits_map; } // Helper functions // Sets just the one digit by digit number void setDigit(uint8_t digit, bool update_=true) { setDigitMap(0x01 << digit, update_); } void setAll(bool update_=true) { setDigitMap(all_on, update_); } void clear(bool update_=true) { setDigitMap(all_off, update_); } void setSecondsOnes() { setDigit(SECONDS_ONES); } void setSecondsTens() { setDigit(SECONDS_TENS); } void setMinutesOnes() { setDigit(MINUTES_ONES); } void setMinutesTens() { setDigit(MINUTES_TENS); } void setHoursOnes() { setDigit(HOURS_ONES); } void setHoursTens() { setDigit(HOURS_TENS); } bool isSecondsOnes() { return ((digits_map & SECONDS_ONES_MAP) > 0); } bool isSecondsTens() { return ((digits_map & SECONDS_TENS_MAP) > 0); } bool isMinutesOnes() { return ((digits_map & MINUTES_ONES_MAP) > 0); } bool isMinutesTens() { return ((digits_map & MINUTES_TENS_MAP) > 0); } bool isHoursOnes() { return ((digits_map & HOURS_ONES_MAP) > 0); } bool isHoursTens() { return ((digits_map & HOURS_TENS_MAP) > 0); } }; #endif // CHIP_SELECT_H ================================================ FILE: usermods/EleksTube_IPS/EleksTube_IPS.cpp ================================================ #include "TFTs.h" #include "wled.h" //Large parts of the code are from https://github.com/SmittyHalibut/EleksTubeHAX class ElekstubeIPSUsermod : public Usermod { private: // strings to reduce flash memory usage (used more than twice) static const char _name[]; static const char _tubeSeg[]; static const char _digitOffset[]; char cronixieDisplay[7] = "HHMMSS"; TFTs tfts; void updateClockDisplay(TFTs::show_t show=TFTs::yes) { bool set[6] = {false}; for (uint8_t i = 0; i<6; i++) { char c = cronixieDisplay[i]; if (c >= '0' && c <= '9') { tfts.setDigit(5-i, c - '0', show); set[i] = true; } else if (c >= 'A' && c <= 'G') { tfts.setDigit(5-i, c - 'A' + 10, show); set[i] = true; //10.bmp to 16.bmp static display } else if (c == '-' || c == '_' || c == ' ') { tfts.setDigit(5-i, 255, show); set[i] = true; //blank } else { set[i] = false; //display HHMMSS time } } uint8_t hr = hour(localTime); uint8_t hrTens = hr/10; uint8_t mi = minute(localTime); uint8_t mittens = mi/10; uint8_t s = second(localTime); uint8_t sTens = s/10; if (!set[0]) tfts.setDigit(HOURS_TENS, hrTens, show); if (!set[1]) tfts.setDigit(HOURS_ONES, hr - hrTens*10, show); if (!set[2]) tfts.setDigit(MINUTES_TENS, mittens, show); if (!set[3]) tfts.setDigit(MINUTES_ONES, mi - mittens*10, show); if (!set[4]) tfts.setDigit(SECONDS_TENS, sTens, show); if (!set[5]) tfts.setDigit(SECONDS_ONES, s - sTens*10, show); } unsigned long lastTime = 0; public: uint8_t lastBri; uint32_t lastCols[6]; TFTs::show_t fshow=TFTs::yes; void setup() { tfts.begin(); tfts.fillScreen(TFT_BLACK); for (int8_t i = 5; i >= 0; i--) { tfts.setDigit(i, 255, TFTs::force); //turn all off } } void loop() { if (!toki.isTick()) return; updateLocalTime(); Segment& seg1 = strip.getSegment(tfts.tubeSegment); if (seg1.isActive()) { bool update = false; if (seg1.opacity != lastBri) update = true; lastBri = seg1.opacity; for (uint8_t i = 0; i < 6; i++) { uint32_t c = strip.getPixelColor(seg1.start + i); if (c != lastCols[i]) update = true; lastCols[i] = c; } if (update) fshow=TFTs::force; } else if (lastCols[0] != 0) { // Segment 1 deleted fshow=TFTs::force; lastCols[0] = 0; } updateClockDisplay(fshow); fshow=TFTs::yes; } /** * addToConfig() (called from set.cpp) stores persistent properties to cfg.json */ void addToConfig(JsonObject &root) { // we add JSON object: {"EleksTubeIPS": {"tubeSegment": 1, "digitOffset": 0}} JsonObject top = root.createNestedObject(FPSTR(_name)); // usermodname top[FPSTR(_tubeSeg)] = tfts.tubeSegment; top[FPSTR(_digitOffset)] = tfts.digitOffset; DEBUG_PRINTLN(F("EleksTube config saved.")); } /** * readFromConfig() is called before setup() to populate properties from values stored in cfg.json * * The function should return true if configuration was successfully loaded or false if there was no configuration. */ bool readFromConfig(JsonObject &root) { // we look for JSON object: {"EleksTubeIPS": {"tubeSegment": 1, "digitOffset": 0}} DEBUG_PRINT(FPSTR(_name)); JsonObject top = root[FPSTR(_name)]; if (top.isNull()) { DEBUG_PRINTLN(F(": No config found. (Using defaults.)")); return false; } tfts.tubeSegment = top[FPSTR(_tubeSeg)] | tfts.tubeSegment; uint8_t digitOffsetPrev = tfts.digitOffset; tfts.digitOffset = top[FPSTR(_digitOffset)] | tfts.digitOffset; if (tfts.digitOffset > 240) tfts.digitOffset = 240; if (tfts.digitOffset != digitOffsetPrev) fshow=TFTs::force; // use "return !top["newestParameter"].isNull();" when updating Usermod with new features return !top[FPSTR(_digitOffset)].isNull(); } /* * 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["nx"] = cronixieDisplay; root[FPSTR(_digitOffset)] = tfts.digitOffset; } /* * 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) { if (root["nx"].is()) { strncpy(cronixieDisplay, root["nx"], 6); } uint8_t digitOffsetPrev = tfts.digitOffset; tfts.digitOffset = root[FPSTR(_digitOffset)] | tfts.digitOffset; if (tfts.digitOffset > 240) tfts.digitOffset = 240; if (tfts.digitOffset != digitOffsetPrev) fshow=TFTs::force; } uint16_t getId() { return USERMOD_ID_ELEKSTUBE_IPS; } }; // strings to reduce flash memory usage (used more than twice) const char ElekstubeIPSUsermod::_name[] PROGMEM = "EleksTubeIPS"; const char ElekstubeIPSUsermod::_tubeSeg[] PROGMEM = "tubeSegment"; const char ElekstubeIPSUsermod::_digitOffset[] PROGMEM = "digitOffset"; static ElekstubeIPSUsermod elekstube_ips; REGISTER_USERMOD(elekstube_ips); ================================================ FILE: usermods/EleksTube_IPS/Hardware.h ================================================ /* * Define the hardware for the EleksTube IPS clock. Mostly pin definitions */ #ifndef ELEKSTUBEHAX_HARDWARE_H #define ELEKSTUBEHAX_HARDWARE_H #include #include // for HIGH and LOW // Common indexing scheme, used to identify the digit #define SECONDS_ONES (0) #define SECONDS_TENS (1) #define MINUTES_ONES (2) #define MINUTES_TENS (3) #define HOURS_ONES (4) #define HOURS_TENS (5) #define NUM_DIGITS (6) #define SECONDS_ONES_MAP (0x01 << SECONDS_ONES) #define SECONDS_TENS_MAP (0x01 << SECONDS_TENS) #define MINUTES_ONES_MAP (0x01 << MINUTES_ONES) #define MINUTES_TENS_MAP (0x01 << MINUTES_TENS) #define HOURS_ONES_MAP (0x01 << HOURS_ONES) #define HOURS_TENS_MAP (0x01 << HOURS_TENS) // WS2812 (or compatible) LEDs on the back of the display modules. #define BACKLIGHTS_PIN (12) // Buttons, active low, externally pulled up (with actual resistors!) #define BUTTON_LEFT_PIN (33) #define BUTTON_MODE_PIN (32) #define BUTTON_RIGHT_PIN (35) #define BUTTON_POWER_PIN (34) // I2C to DS3231 RTC. #define RTC_SCL_PIN (22) #define RTC_SDA_PIN (21) // Chip Select shift register, to select the display #define CSSR_DATA_PIN (14) #define CSSR_CLOCK_PIN (16) #define CSSR_LATCH_PIN (17) // SPI to displays // DEFINED IN User_Setup.h // Look for: TFT_MOSI, TFT_SCLK, TFT_CS, TFT_DC, and TFT_RST // Power for all TFT displays are grounded through a MOSFET so they can all be turned off. // Active HIGH. #define TFT_ENABLE_PIN (27) #endif // ELEKSTUBEHAX_HARDWARE_H ================================================ FILE: usermods/EleksTube_IPS/TFTs.h ================================================ #ifndef TFTS_H #define TFTS_H #include "wled.h" #include #include #include "Hardware.h" #include "ChipSelect.h" class TFTs : public TFT_eSPI { private: uint8_t digits[NUM_DIGITS]; // These read 16- and 32-bit types from the SD card file. // BMP data is stored little-endian, Arduino is little-endian too. // May need to reverse subscript order if porting elsewhere. uint16_t read16(fs::File &f) { uint16_t result; ((uint8_t *)&result)[0] = f.read(); // LSB ((uint8_t *)&result)[1] = f.read(); // MSB return result; } uint32_t read32(fs::File &f) { uint32_t result; ((uint8_t *)&result)[0] = f.read(); // LSB ((uint8_t *)&result)[1] = f.read(); ((uint8_t *)&result)[2] = f.read(); ((uint8_t *)&result)[3] = f.read(); // MSB return result; } uint16_t output_buffer[TFT_HEIGHT][TFT_WIDTH]; int16_t w = 135, h = 240, x = 0, y = 0, bufferedDigit = 255; uint16_t digitR, digitG, digitB, dimming = 255; uint32_t digitColor = 0; void drawBuffer() { bool oldSwapBytes = getSwapBytes(); setSwapBytes(true); pushImage(x, y, w, h, (uint16_t *)output_buffer); setSwapBytes(oldSwapBytes); } // These BMP functions are stolen directly from the TFT_SPIFFS_BMP example in the TFT_eSPI library. // Unfortunately, they aren't part of the library itself, so I had to copy them. // I've modified drawBmp to buffer the whole image at once instead of doing it line-by-line. //// BEGIN STOLEN CODE // Draw directly from file stored in RGB565 format. Fastest bool drawBin(const char *filename) { fs::File bmpFS; // Open requested file on SD card bmpFS = WLED_FS.open(filename, "r"); size_t sz = bmpFS.size(); if (sz > 64800) { bmpFS.close(); return false; } uint16_t r, g, b, dimming = 255; int16_t row, col; //draw img that is shorter than 240pix into the center w = 135; h = sz / (w * 2); x = 0; y = (height() - h) /2; uint8_t lineBuffer[w * 2]; if (!realtimeMode || realtimeOverride || (realtimeMode && useMainSegmentOnly)) strip.service(); // 0,0 coordinates are top left for (row = 0; row < h; row++) { bmpFS.read(lineBuffer, sizeof(lineBuffer)); uint8_t PixM, PixL; // Colors are already in 16-bit R5, G6, B5 format for (col = 0; col < w; col++) { if (dimming == 255 && !digitColor) { // not needed, copy directly output_buffer[row][col] = (lineBuffer[col*2+1] << 8) | (lineBuffer[col*2]); } else { // 16 BPP pixel format: R5, G6, B5 ; bin: RRRR RGGG GGGB BBBB PixM = lineBuffer[col*2+1]; PixL = lineBuffer[col*2]; // align to 8-bit value (MSB left aligned) r = (PixM) & 0xF8; g = ((PixM << 5) | (PixL >> 3)) & 0xFC; b = (PixL << 3) & 0xF8; r *= dimming; g *= dimming; b *= dimming; r = r >> 8; g = g >> 8; b = b >> 8; if (digitColor) { // grayscale pixel coloring uint8_t l = (r > g) ? ((r > b) ? r:b) : ((g > b) ? g:b); r = g = b = l; r *= digitR; g *= digitG; b *= digitB; r = r >> 8; g = g >> 8; b = b >> 8; } output_buffer[row][col] = ((r & 0xF8) << 8) | ((g & 0xFC) << 3) | (b >> 3); } } } drawBuffer(); bmpFS.close(); return true; } bool drawBmp(const char *filename) { fs::File bmpFS; // Open requested file on SD card bmpFS = WLED_FS.open(filename, "r"); uint32_t seekOffset, headerSize, paletteSize = 0; int16_t row; uint16_t r, g, b, dimming = 255, bitDepth; uint16_t magic = read16(bmpFS); if (magic != ('B' | ('M' << 8))) { // File not found or not a BMP Serial.println(F("BMP not found!")); bmpFS.close(); return false; } (void) read32(bmpFS); // filesize in bytes (void) read32(bmpFS); // reserved seekOffset = read32(bmpFS); // start of bitmap headerSize = read32(bmpFS); // header size w = read32(bmpFS); // width h = read32(bmpFS); // height (void) read16(bmpFS); // color planes (must be 1) bitDepth = read16(bmpFS); if (read32(bmpFS) != 0 || (bitDepth != 24 && bitDepth != 1 && bitDepth != 4 && bitDepth != 8)) { Serial.println(F("BMP format not recognized.")); bmpFS.close(); return false; } uint32_t palette[256]; if (bitDepth <= 8) // 1,4,8 bit bitmap: read color palette { (void) read32(bmpFS); (void) read32(bmpFS); (void) read32(bmpFS); // size, w resolution, h resolution paletteSize = read32(bmpFS); if (paletteSize == 0) paletteSize = 1 << bitDepth; //if 0, size is 2^bitDepth bmpFS.seek(14 + headerSize); // start of color palette for (uint16_t i = 0; i < paletteSize; i++) { palette[i] = read32(bmpFS); } } // draw img that is shorter than 240pix into the center x = (width() - w) /2; y = (height() - h) /2; bmpFS.seek(seekOffset); uint32_t lineSize = ((bitDepth * w +31) >> 5) * 4; uint8_t lineBuffer[lineSize]; uint8_t serviceStrip = (!realtimeMode || realtimeOverride || (realtimeMode && useMainSegmentOnly)) ? 7 : 0; // row is decremented as the BMP image is drawn bottom up for (row = h-1; row >= 0; row--) { if ((row & 0b00000111) == serviceStrip) strip.service(); //still refresh backlight to mitigate stutter every few rows bmpFS.read(lineBuffer, sizeof(lineBuffer)); uint8_t* bptr = lineBuffer; // Convert 24 to 16 bit colors while copying to output buffer. for (uint16_t col = 0; col < w; col++) { if (bitDepth == 24) { b = *bptr++; g = *bptr++; r = *bptr++; } else { uint32_t c = 0; if (bitDepth == 8) { c = palette[*bptr++]; } else if (bitDepth == 4) { c = palette[(*bptr >> ((col & 0x01)?0:4)) & 0x0F]; if (col & 0x01) bptr++; } else { // bitDepth == 1 c = palette[(*bptr >> (7 - (col & 0x07))) & 0x01]; if ((col & 0x07) == 0x07) bptr++; } b = c; g = c >> 8; r = c >> 16; } if (dimming != 255) { // only dim when needed r *= dimming; g *= dimming; b *= dimming; r = r >> 8; g = g >> 8; b = b >> 8; } if (digitColor) { // grayscale pixel coloring uint8_t l = (r > g) ? ((r > b) ? r:b) : ((g > b) ? g:b); r = g = b = l; r *= digitR; g *= digitG; b *= digitB; r = r >> 8; g = g >> 8; b = b >> 8; } output_buffer[row][col] = ((r & 0xF8) << 8) | ((g & 0xFC) << 3) | ((b & 0xFF) >> 3); } } drawBuffer(); bmpFS.close(); return true; } bool drawClk(const char *filename) { fs::File bmpFS; // Open requested file on SD card bmpFS = WLED_FS.open(filename, "r"); if (!bmpFS) { Serial.print("File not found: "); Serial.println(filename); return false; } uint16_t r, g, b, dimming = 255, magic; int16_t row, col; magic = read16(bmpFS); if (magic != 0x4B43) { // look for "CK" header Serial.print(F("File not a CLK. Magic: ")); Serial.println(magic); bmpFS.close(); return false; } w = read16(bmpFS); h = read16(bmpFS); x = (width() - w) / 2; y = (height() - h) / 2; uint8_t lineBuffer[w * 2]; if (!realtimeMode || realtimeOverride || (realtimeMode && useMainSegmentOnly)) strip.service(); // 0,0 coordinates are top left for (row = 0; row < h; row++) { bmpFS.read(lineBuffer, sizeof(lineBuffer)); uint8_t PixM, PixL; // Colors are already in 16-bit R5, G6, B5 format for (col = 0; col < w; col++) { if (dimming == 255 && !digitColor) { // not needed, copy directly output_buffer[row][col+x] = (lineBuffer[col*2+1] << 8) | (lineBuffer[col*2]); } else { // 16 BPP pixel format: R5, G6, B5 ; bin: RRRR RGGG GGGB BBBB PixM = lineBuffer[col*2+1]; PixL = lineBuffer[col*2]; // align to 8-bit value (MSB left aligned) r = (PixM) & 0xF8; g = ((PixM << 5) | (PixL >> 3)) & 0xFC; b = (PixL << 3) & 0xF8; r *= dimming; g *= dimming; b *= dimming; r = r >> 8; g = g >> 8; b = b >> 8; if (digitColor) { // grayscale pixel coloring uint8_t l = (r > g) ? ((r > b) ? r:b) : ((g > b) ? g:b); r = g = b = l; r *= digitR; g *= digitG; b *= digitB; r = r >> 8; g = g >> 8; b = b >> 8; } output_buffer[row][col+x] = ((r & 0xF8) << 8) | ((g & 0xFC) << 3) | (b >> 3); } } } drawBuffer(); bmpFS.close(); return true; } public: TFTs() : TFT_eSPI(), chip_select() { for (uint8_t digit=0; digit < NUM_DIGITS; digit++) digits[digit] = 0; } // no == Do not send to TFT. yes == Send to TFT if changed. force == Send to TFT. enum show_t { no, yes, force }; // A digit of 0xFF means blank the screen. const static uint8_t blanked = 255; uint8_t tubeSegment = 1; uint8_t digitOffset = 0; void begin() { pinMode(TFT_ENABLE_PIN, OUTPUT); digitalWrite(TFT_ENABLE_PIN, HIGH); //enable displays on boot // Start with all displays selected. chip_select.begin(); chip_select.setAll(); // Initialize the super class. init(); } void showDigit(uint8_t digit) { chip_select.setDigit(digit); uint8_t digitToDraw = digits[digit]; if (digitToDraw < 10) digitToDraw += digitOffset; if (digitToDraw == blanked) { fillScreen(TFT_BLACK); return; } // if last digit was the same, skip loading from FS to buffer if (!digitColor && digitToDraw == bufferedDigit) drawBuffer(); digitR = R(digitColor); digitG = G(digitColor); digitB = B(digitColor); // Filenames are no bigger than "254.bmp\0" char file_name[10]; // Fastest, raw RGB565 sprintf(file_name, "/%d.bin", digitToDraw); if (WLED_FS.exists(file_name)) { if (drawBin(file_name)) bufferedDigit = digitToDraw; return; } // Fast, raw RGB565, see https://github.com/aly-fly/EleksTubeHAX on how to create this clk format sprintf(file_name, "/%d.clk", digitToDraw); if (WLED_FS.exists(file_name)) { if (drawClk(file_name)) bufferedDigit = digitToDraw; return; } // Slow, regular RGB888 or 1,4,8 bit palette BMP sprintf(file_name, "/%d.bmp", digitToDraw); if (drawBmp(file_name)) bufferedDigit = digitToDraw; return; } void setDigit(uint8_t digit, uint8_t value, show_t show=yes) { uint8_t old_value = digits[digit]; digits[digit] = value; // Color in grayscale bitmaps if Segment 1 exists // TODO If secondary and tertiary are black, color all in primary, // else color first three from Seg 1 color slots and last three from Seg 2 color slots Segment& seg1 = strip.getSegment(tubeSegment); if (seg1.isActive()) { digitColor = strip.getPixelColor(seg1.start + digit); dimming = seg1.opacity; } else { digitColor = 0; dimming = 255; } if (show != no && (old_value != value || show == force)) { showDigit(digit); } } uint8_t getDigit(uint8_t digit) {return digits[digit];} void showAllDigits() {for (uint8_t digit=0; digit < NUM_DIGITS; digit++) showDigit(digit);} // Making chip_select public so we don't have to proxy all methods, and the caller can just use it directly. ChipSelect chip_select; }; #endif // TFTS_H ================================================ FILE: usermods/EleksTube_IPS/User_Setup.h ================================================ /* * This is intended to over-ride `User_Setup.h` that comes with the TFT_eSPI library. * I hate having to modify the library code. */ // ST7789 135 x 240 display with no chip select line #define ST7789_DRIVER // Configure all registers #define TFT_WIDTH 135 #define TFT_HEIGHT 240 #define CGRAM_OFFSET // Library will add offsets required //#define TFT_RGB_ORDER TFT_RGB // Colour order Red-Green-Blue //#define TFT_RGB_ORDER TFT_BGR // Colour order Blue-Green-Red //#define TFT_INVERSION_ON //#define TFT_INVERSION_OFF // EleksTube IPS #define TFT_SDA_READ // Read and write on the MOSI/SDA pin, no separate MISO pin #define TFT_MOSI 23 #define TFT_SCLK 18 //#define TFT_CS -1 // Not connected #define TFT_DC 25 // Data Command, aka Register Select or RS #define TFT_RST 26 // Connect reset to ensure display initialises #define LOAD_GLCD // Font 1. Original Adafruit 8 pixel font needs ~1820 bytes in FLASH //#define LOAD_FONT2 // Font 2. Small 16 pixel high font, needs ~3534 bytes in FLASH, 96 characters //#define LOAD_FONT4 // Font 4. Medium 26 pixel high font, needs ~5848 bytes in FLASH, 96 characters //#define LOAD_FONT6 // Font 6. Large 48 pixel font, needs ~2666 bytes in FLASH, only characters 1234567890:-.apm //#define LOAD_FONT7 // Font 7. 7 segment 48 pixel font, needs ~2438 bytes in FLASH, only characters 1234567890:. //#define LOAD_FONT8 // Font 8. Large 75 pixel font needs ~3256 bytes in FLASH, only characters 1234567890:-. //#define LOAD_FONT8N // Font 8. Alternative to Font 8 above, slightly narrower, so 3 digits fit a 160 pixel TFT //#define LOAD_GFXFF // FreeFonts. Include access to the 48 Adafruit_GFX free fonts FF1 to FF48 and custom fonts //#define SMOOTH_FONT //#define SPI_FREQUENCY 27000000 #define SPI_FREQUENCY 40000000 /* * To make the Library not over-write all this: */ #define USER_SETUP_LOADED ================================================ FILE: usermods/EleksTube_IPS/library.json.disabled ================================================ { "name:": "EleksTube_IPS", "build": { "libArchive": false }, "dependencies": { "TFT_eSPI" : "2.5.33" } } # Seems to add 300kb to the RAM requirement??? ================================================ FILE: usermods/EleksTube_IPS/readme.md ================================================ # EleksTube IPS Clock usermod This usermod allows WLED to run on the EleksTube IPS clock. It enables running all WLED effects on the background SK6812 lighting, while displaying digit bitmaps on the 6 IPS screens. Code is largely based on https://github.com/SmittyHalibut/EleksTubeHAX by Mark Smith! Supported: - Display with custom bitmaps (.bmp) or raw RGB565 images (.bin) from filesystem - Background lighting - All 4 hardware buttons - RTC (with RTC usermod) - Standard WLED time features (NTP, DST, timezones) Not supported: - On-device setup with buttons (WiFi setup only) Your images must be 1-135 pixels wide and 1-240 pixels high. BMP 1, 4, 8, and 24 bits per pixel formats are supported. ## Installation Compile and upload to clock using the `elekstube_ips` PlatformIO environment Once uploaded (the clock can be flashed like any ESP32 module), go to `[WLED-IP]/edit` and upload the 0-9.bin files from [here](https://github.com/Aircoookie/NixieThemes/tree/master/themes/RealisticNixie/bin). You can find more clockfaces in the [NixieThemes](https://github.com/Aircoookie/NixieThemes/) repo. Use LED pin 12, relay pin 27 and button pin 34. ## Use of RGB565 images Binary 16-bit per pixel RGB565 format `.bin` and `.clk` images are now supported. This has the benefit of using only 2/3rds of the file space a 24 BPP `.bmp` occupies. The drawback is this format cannot be handled by common image programs and an extra conversion step is needed. You can use https://lvgl.io/tools/imageconverter to convert your .bmp to a .bin file (settings `True color` and `Binary RGB565`). Thank you to @RedNax67 for adding .bin and .clk support. For most clockface designs, using 4 or 8 BPP BMP format will reduce file size even more: | Bits per pixel | File size in kB (for 135x240 img) | % of 24 BPP BMP | Max unique colors | --- | --- | --- | --- | 24 | 98 | 100% | 16M (66K) 16 (.clk) | 64.8 | 66% | 66K 8 | 33.7 | 34% | 256 4 | 16.4 | 17% | 16 1 | 4.9 | 5% | 2 Comparison 1 vs. 4 vs. 8 vs. 24 BPP. With this clockface on the actual clock, 4 bit looks good, and 8 bit is almost indistinguishable from 24 bit. ![comparison](https://user-images.githubusercontent.com/21045690/156899667-5b55ed9f-6e03-4066-b2aa-1260e9570369.png) ================================================ FILE: usermods/Enclosure_with_OLED_temp_ESP07/assets/readme.md ================================================ # Enclosure and PCB ## IP67 rated enclosure ![Enclosure](controller.jpg) ## PCB ![PCB](pcb.png) ================================================ FILE: usermods/Enclosure_with_OLED_temp_ESP07/readme.md ================================================ # Almost universal controller board for outdoor applications This usermod is using ideas from @mrVanboy and @400killer Installation of file: Copy and replace file in wled00 directory. For BME280 sensor use usermod_bme280.cpp. Copy to wled00 and rename to usermod.cpp ## Project repository - [Original repository](https://github.com/srg74/Controller-for-WLED-firmware) - Main controller repository ## Features - SSD1306 128x32 and 128x64 I2C OLED display - On screen IP address, SSID and controller status (e.g. ON or OFF, recent effect) - Auto display shutoff for extending display lifetime - Dallas temperature sensor - Reporting temperature to MQTT broker ## Hardware ![Hardware connection](assets/controller.jpg) ## Functionality checked with - ESP-07S - PlatformIO - SSD1306 128x32 I2C OLED display - DS18B20 (temperature sensor) - BME280 (temperature, humidity and pressure sensor) - KY-022 (infrared receiver) - Push button (N.O. momentary switch) For Dallas sensor uncomment `U8g2@~2.27.3`,`DallasTemperature@~3.8.0`,`OneWire@~2.3.5 under` `[common]` section in `platformio.ini`: ```ini # platformio.ini ... [platformio] ... default_envs = esp07 ; default_envs = d1_mini ... [common] ... lib_deps_external = ... #To use the SSD1306 OLED display, uncomment following U8g2@~2.27.3 #For Dallas sensor, uncomment the following 2 lines DallasTemperature@~3.8.0 OneWire@~2.3.5 ... ``` For BME280 sensor, uncomment `U8g2@~2.27.3`,`BME280@~3.0.0 under` `[common]` section in `platformio.ini`: ```ini # platformio.ini ... [platformio] ... default_envs = esp07 ; default_envs = d1_mini ... [common] ... lib_deps_external = ... #To use the SSD1306 OLED display, uncomment following U8g2@~2.27.3 #For BME280 sensor uncomment following BME280@~3.0.0 ... ``` ================================================ FILE: usermods/Enclosure_with_OLED_temp_ESP07/usermod.cpp ================================================ #include "wled.h" #include #include // from https://github.com/olikraus/u8g2/ #include //Dallastemperature sensor #ifdef WLED_DISABLE_MQTT #error "This user mod requires MQTT to be enabled." #endif //The SCL and SDA pins are defined here. //Lolin32 boards use SCL=5 SDA=4 #define U8X8_PIN_SCL 5 #define U8X8_PIN_SDA 4 // Dallas sensor OneWire oneWire(13); DallasTemperature sensor(&oneWire); long temptimer = millis(); long lastMeasure = 0; #define Celsius // Show temperature measurement in Celsius otherwise is in Fahrenheit // 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 // --> First choice of cheap I2C OLED 128X32 0.91" U8X8_SSD1306_128X32_UNIVISION_HW_I2C u8x8(U8X8_PIN_NONE, U8X8_PIN_SCL, U8X8_PIN_SDA); // Pins are Reset, SCL, SDA // --> Second choice of cheap I2C OLED 128X64 0.96" or 1.3" //U8X8_SSD1306_128X64_NONAME_HW_I2C u8x8(U8X8_PIN_NONE, U8X8_PIN_SCL, U8X8_PIN_SDA); // Pins are Reset, SCL, SDA // gets called once at boot. Do all initialization that doesn't depend on // network here void userSetup() { sensor.begin(); //Start Dallas temperature sensor u8x8.begin(); //u8x8.setFlipMode(1); //Un-comment if using WLED Wemos shield 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.drawString(0, 0, "Loading..."); } // gets called every time WiFi is (re-)connected. Initialize own network // interfaces here void userConnected() {} // 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 knownMode = 0; uint8_t knownPalette = 0; long lastUpdate = 0; long lastRedraw = 0; bool displayTurnedOff = false; // How often we are redrawing screen #define USER_LOOP_REFRESH_RATE_MS 5000 void userLoop() { //----> Dallas temperature sensor MQTT publishing temptimer = millis(); // Timer to publishe new temperature every 60 seconds if (temptimer - lastMeasure > 60000) { lastMeasure = temptimer; //Check if MQTT Connected, otherwise it will crash the 8266 if (mqtt != nullptr) { sensor.requestTemperatures(); //Gets preferred temperature scale based on selection in definitions section #ifdef Celsius float board_temperature = sensor.getTempCByIndex(0); #else float board_temperature = sensor.getTempFByIndex(0); #endif //Create character string populated with user defined device topic from the UI, and the read temperature. Then publish to MQTT server. char subuf[38]; strcpy(subuf, mqttDeviceTopic); strcat(subuf, "/temperature"); mqtt->publish(subuf, 0, true, String(board_temperature).c_str()); } } // Check if we time interval for redrawing passes. if (millis() - lastUpdate < USER_LOOP_REFRESH_RATE_MS) { return; } lastUpdate = millis(); // Turn off display after 3 minutes with no change. if(!displayTurnedOff && millis() - lastRedraw > 3*60*1000) { u8x8.setPowerSave(1); displayTurnedOff = true; } // Check if values which are shown on display changed from the last time. 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 (knownMode != strip.getMainSegment().mode) { needRedraw = true; } else if (knownPalette != strip.getMainSegment().palette) { needRedraw = true; } if (!needRedraw) { return; } needRedraw = false; if (displayTurnedOff) { u8x8.setPowerSave(0); displayTurnedOff = false; } lastRedraw = millis(); // 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.getMainSegment().mode; knownPalette = strip.getMainSegment().palette; u8x8.clear(); u8x8.setFont(u8x8_font_chroma48medium8_r); // First row with Wifi name u8x8.setCursor(1, 0); u8x8.print(knownSsid.substring(0, u8x8.getCols() > 1 ? u8x8.getCols() - 2 : 0)); // Print `~` char to indicate that SSID is longer than our display if (knownSsid.length() > u8x8.getCols()) u8x8.print("~"); // Second row with IP or Password u8x8.setCursor(1, 1); // Print password in AP mode and if led is OFF. if (apActive && bri == 0) u8x8.print(apPass); else u8x8.print(knownIp); // Third row with mode name u8x8.setCursor(2, 2); char lineBuffer[17]; extractModeName(knownMode, JSON_mode_names, lineBuffer, 16); u8x8.print(lineBuffer); // Fourth row with palette name u8x8.setCursor(2, 3); extractModeName(knownPalette, JSON_palette_names, lineBuffer, 16); u8x8.print(lineBuffer); u8x8.setFont(u8x8_font_open_iconic_embedded_1x1); u8x8.drawGlyph(0, 0, 80); // wifi icon u8x8.drawGlyph(0, 1, 68); // home icon u8x8.setFont(u8x8_font_open_iconic_weather_2x2); u8x8.drawGlyph(0, 2, 66 + (bri > 0 ? 3 : 0)); // sun/moon icon } ================================================ FILE: usermods/Enclosure_with_OLED_temp_ESP07/usermod_bme280.cpp ================================================ #include "wled.h" #include #include // from https://github.com/olikraus/u8g2/ #include #include //BME280 sensor #ifdef WLED_DISABLE_MQTT #error "This user mod requires MQTT to be enabled." #endif void UpdateBME280Data(); #define Celsius // Show temperature measurement in Celsius otherwise is in Fahrenheit BME280I2C bme; // Default : forced mode, standby time = 1000 ms // Oversampling = pressure ×1, temperature ×1, humidity ×1, filter off, #ifdef ARDUINO_ARCH_ESP32 //ESP32 boards uint8_t SCL_PIN = 22; uint8_t SDA_PIN = 21; #else //ESP8266 boards uint8_t SCL_PIN = 5; uint8_t SDA_PIN = 4; // uint8_t RST_PIN = 16; // Un-comment for Heltec WiFi-Kit-8 #endif //The SCL and SDA pins are defined here. //ESP8266 Wemos D1 mini board use SCL=5 SDA=4 while ESP32 Wemos32 mini board use SCL=22 SDA=21 #define U8X8_PIN_SCL SCL_PIN #define U8X8_PIN_SDA SDA_PIN //#define U8X8_PIN_RESET RST_PIN // Un-comment for Heltec WiFi-Kit-8 // 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 // --> First choise of cheap I2C OLED 128X32 0.91" U8X8_SSD1306_128X32_UNIVISION_HW_I2C u8x8(U8X8_PIN_NONE, U8X8_PIN_SCL, U8X8_PIN_SDA); // Pins are Reset, SCL, SDA // --> Second choice of cheap I2C OLED 128X64 0.96" or 1.3" //U8X8_SSD1306_128X64_NONAME_HW_I2C u8x8(U8X8_PIN_NONE, U8X8_PIN_SCL, U8X8_PIN_SDA); // Pins are Reset, SCL, SDA // --> Third choice of Heltec WiFi-Kit-8 OLED 128X32 0.91" //U8X8_SSD1306_128X32_UNIVISION_HW_I2C u8x8(U8X8_PIN_RESET, U8X8_PIN_SCL, U8X8_PIN_SDA); // Constructor for Heltec WiFi-Kit-8 // gets called once at boot. Do all initialization that doesn't depend on network here // BME280 sensor timer long tempTimer = millis(); long lastMeasure = 0; float SensorPressure(NAN); float SensorTemperature(NAN); float SensorHumidity(NAN); void userSetup() { u8x8.begin(); u8x8.setPowerSave(0); u8x8.setFlipMode(1); 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.drawString(0, 0, "Loading..."); Wire.begin(SDA_PIN,SCL_PIN); while(!bme.begin()) { Serial.println("Could not find BME280I2C sensor!"); delay(1000); } switch(bme.chipModel()) { case BME280::ChipModel_BME280: Serial.println("Found BME280 sensor! Success."); break; case BME280::ChipModel_BMP280: Serial.println("Found BMP280 sensor! No Humidity available."); break; default: Serial.println("Found UNKNOWN sensor! Error!"); } } // gets called every time WiFi is (re-)connected. Initialize own network // interfaces here void userConnected() {} // 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 knownMode = 0; uint8_t knownPalette = 0; long lastUpdate = 0; long lastRedraw = 0; bool displayTurnedOff = false; // How often we are redrawing screen #define USER_LOOP_REFRESH_RATE_MS 5000 void userLoop() { // BME280 sensor MQTT publishing tempTimer = millis(); // Timer to publish new sensor data every 60 seconds if (tempTimer - lastMeasure > 60000) { lastMeasure = tempTimer; // Check if MQTT Connected, otherwise it will crash the 8266 if (mqtt != nullptr) { UpdateBME280Data(); float board_temperature = SensorTemperature; float board_pressure = SensorPressure; float board_humidity = SensorHumidity; // Create string populated with user defined device topic from the UI, and the read temperature, humidity and pressure. Then publish to MQTT server. String t = String(mqttDeviceTopic); t += "/temperature"; mqtt->publish(t.c_str(), 0, true, String(board_temperature).c_str()); String p = String(mqttDeviceTopic); p += "/pressure"; mqtt->publish(p.c_str(), 0, true, String(board_pressure).c_str()); String h = String(mqttDeviceTopic); h += "/humidity"; mqtt->publish(h.c_str(), 0, true, String(board_humidity).c_str()); } } // Check if we time interval for redrawing passes. if (millis() - lastUpdate < USER_LOOP_REFRESH_RATE_MS) { return; } lastUpdate = millis(); // Turn off display after 3 minutes with no change. if(!displayTurnedOff && millis() - lastRedraw > 3*60*1000) { u8x8.setPowerSave(1); displayTurnedOff = true; } // Check if values which are shown on display changed from the last time. 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 (knownMode != strip.getMainSegment().mode) { needRedraw = true; } else if (knownPalette != strip.getMainSegment().palette) { needRedraw = true; } if (!needRedraw) { return; } needRedraw = false; if (displayTurnedOff) { u8x8.setPowerSave(0); displayTurnedOff = false; } lastRedraw = millis(); // 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.getMainSegment().mode; knownPalette = strip.getMainSegment().palette; u8x8.clear(); u8x8.setFont(u8x8_font_chroma48medium8_r); // First row with Wifi name u8x8.setCursor(1, 0); u8x8.print(knownSsid.substring(0, u8x8.getCols() > 1 ? u8x8.getCols() - 2 : 0)); // Print `~` char to indicate that SSID is longer than our display if (knownSsid.length() > u8x8.getCols()) u8x8.print("~"); // Second row with IP or Password u8x8.setCursor(1, 1); // Print password in AP mode and if led is OFF. if (apActive && bri == 0) u8x8.print(apPass); else u8x8.print(knownIp); // Third row with mode name u8x8.setCursor(2, 2); char lineBuffer[17]; extractModeName(knownMode, JSON_mode_names, lineBuffer, 16); u8x8.print(lineBuffer); // Fourth row with palette name u8x8.setCursor(2, 3); extractModeName(knownPalette, JSON_palette_names, lineBuffer, 16); u8x8.print(lineBuffer); u8x8.setFont(u8x8_font_open_iconic_embedded_1x1); u8x8.drawGlyph(0, 0, 80); // wifi icon u8x8.drawGlyph(0, 1, 68); // home icon u8x8.setFont(u8x8_font_open_iconic_weather_2x2); u8x8.drawGlyph(0, 2, 66 + (bri > 0 ? 3 : 0)); // sun/moon icon } void UpdateBME280Data() { float temp(NAN), hum(NAN), pres(NAN); #ifdef Celsius BME280::TempUnit tempUnit(BME280::TempUnit_Celsius); #else BME280::TempUnit tempUnit(BME280::TempUnit_Fahrenheit); #endif BME280::PresUnit presUnit(BME280::PresUnit_Pa); bme.read(pres, temp, hum, tempUnit, presUnit); SensorTemperature=temp; SensorHumidity=hum; SensorPressure=pres; } ================================================ FILE: usermods/Fix_unreachable_netservices_v2/library.json ================================================ { "name": "Fix_unreachable_netservices_v2", "platforms": ["espressif8266"] } ================================================ FILE: usermods/Fix_unreachable_netservices_v2/readme.md ================================================ # Fix unreachable net services V2 **Attention: This usermod compiles only for ESP8266** This usermod-v2 modification performs a ping request to a local IP address every 60 seconds. This ensures WLED net services remain accessible in some problematic WiFi environments. The modification works with static or DHCP IP address configuration. _Story:_ Unfortunately, with many ESP projects where a web server or other network services are running, after some time, the connecton to the web server is lost. The connection can be reestablished with a ping request from the device. With this modification, in the worst case, the network functions are not available until the next ping request. (60 seconds) ## Webinterface The number of pings and reconnects is displayed on the info page in the web interface. The ping delay can be changed. Changes persist after a reboot. ## JSON API The usermod supports the following state changes: | JSON key | Value range | Description | |-------------|------------------|---------------------------------| | PingDelayMs | 5000 to 18000000 | Deactivate/activate the sensor | Changes also persist after a reboot. ## Installation 1. Add `Fix_unreachable_netservices` to `custom_usermods` in your PlatformIO environment. Hopefully I can help someone with that - @gegu ================================================ FILE: usermods/Fix_unreachable_netservices_v2/usermod_Fix_unreachable_netservices.cpp ================================================ #include "wled.h" #if defined(ESP8266) #include /* * This usermod performs a ping request to the local IP address every 60 seconds. * By this procedure the net services of WLED remains accessible in some problematic WLAN environments. * * Usermods allow you to add own functionality to WLED more easily * See: https://github.com/wled-dev/WLED/wiki/Add-own-functionality * * v2 usermods are class inheritance based and can (but don't have to) implement more functions, each of them is shown in this example. * Multiple v2 usermods can be added to one compilation easily. * * Creating a usermod: * This file serves as an example. If you want to create a usermod, it is recommended to use usermod_v2_empty.h from the usermods folder as a template. * Please remember to rename the class and file to a descriptive name. * You may also use multiple .h and .cpp files. * * Using a usermod: * 1. Copy the usermod into the sketch folder (same folder as wled00.ino) * 2. Register the usermod by adding #include "usermod_filename.h" in the top and registerUsermod(new MyUsermodClass()) in the bottom of usermods_list.cpp */ class FixUnreachableNetServices : public Usermod { private: //Private class members. You can declare variables and functions only accessible to your usermod here unsigned long m_lastTime = 0; // declare required variables unsigned long m_pingDelayMs = 60000; unsigned long m_connectedWiFi = 0; ping_option m_pingOpt; unsigned int m_pingCount = 0; bool m_updateConfig = false; public: //Functions called by WLED /** * 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() { //Serial.println("Hello from my usermod!"); } /** * connected() is called every time the WiFi is (re)connected * Use it to initialize network interfaces */ void connected() { //Serial.println("Connected to WiFi!"); ++m_connectedWiFi; // initialize ping_options structure memset(&m_pingOpt, 0, sizeof(struct ping_option)); m_pingOpt.count = 1; m_pingOpt.ip = WiFi.localIP(); } /** * loop */ void loop() { if (m_connectedWiFi > 0 && millis() - m_lastTime > m_pingDelayMs) { ping_start(&m_pingOpt); m_lastTime = millis(); ++m_pingCount; } if (m_updateConfig) { serializeConfig(); m_updateConfig = false; } } /** * 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) { //this code adds "u":{"⚡ Ping fix pings": m_pingCount} to the info object JsonObject user = root["u"]; if (user.isNull()) user = root.createNestedObject("u"); String uiDomString = "⚡ Ping fix pings\ Delay sec"; JsonArray infoArr = user.createNestedArray(uiDomString); //name infoArr.add(m_pingCount); //value //this code adds "u":{"⚡ Reconnects": m_connectedWiFi - 1} to the info object infoArr = user.createNestedArray("⚡ Reconnects"); //name infoArr.add(m_connectedWiFi - 1); //value } /** * 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["PingDelay"] = (m_pingDelayMs/1000); } /** * 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) { if (root["PingDelay"] != nullptr) { m_pingDelayMs = (1000 * max(1UL, min(300UL, root["PingDelay"].as()))); m_updateConfig = true; } } /** * provide the changeable values */ void addToConfig(JsonObject &root) { JsonObject top = root.createNestedObject("FixUnreachableNetServices"); top["PingDelayMs"] = m_pingDelayMs; } /** * restore the changeable values */ bool readFromConfig(JsonObject &root) { JsonObject top = root["FixUnreachableNetServices"]; if (top.isNull()) return false; m_pingDelayMs = top["PingDelayMs"] | m_pingDelayMs; m_pingDelayMs = max(5000UL, min(18000000UL, m_pingDelayMs)); // use "return !top["newestParameter"].isNull();" when updating Usermod with new features return true; } /** * 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_FIXNETSERVICES; } }; static FixUnreachableNetServices fix_unreachable_net_services; REGISTER_USERMOD(fix_unreachable_net_services); #else /* !ESP8266 */ #warning "Usermod FixUnreachableNetServices works only with ESP8266 builds" #endif ================================================ FILE: usermods/INA226_v2/INA226_v2.cpp ================================================ #include "wled.h" #include #define INA226_ADDRESS 0x40 // Default I2C address for INA226 #define DEFAULT_CHECKINTERVAL 60000 #define DEFAULT_INASAMPLES 128 #define DEFAULT_INASAMPLESENUM AVERAGE_128 #define DEFAULT_INACONVERSIONTIME 1100 #define DEFAULT_INACONVERSIONTIMEENUM CONV_TIME_1100 // A packed version of all INA settings enums and their human friendly counterparts packed into a 32 bit structure // Some values are shifted and need to be preprocessed before usage struct InaSettingLookup { uint16_t avgSamples : 11; // Max 1024, which could be in 10 bits if we shifted by 1; if we somehow handle the edge case with "1" uint8_t avgEnum : 4; // Shift by 8 to get the INA226_AVERAGES value, accepts 0x00 to 0x0F, we need 0x00 to 0x0E uint16_t convTimeUs : 14; // We could save 2 bits by shifting this, but we won't save anything at present. INA226_CONV_TIME convTimeEnum : 3; // Only the lowest 3 bits are defined in the conversion time enumerations }; const InaSettingLookup _inaSettingsLookup[] = { {1024, AVERAGE_1024 >> 8, 8244, CONV_TIME_8244}, {512, AVERAGE_512 >> 8, 4156, CONV_TIME_4156}, {256, AVERAGE_256 >> 8, 2116, CONV_TIME_2116}, {128, AVERAGE_128 >> 8, 1100, CONV_TIME_1100}, {64, AVERAGE_64 >> 8, 588, CONV_TIME_588}, {16, AVERAGE_16 >> 8, 332, CONV_TIME_332}, {4, AVERAGE_4 >> 8, 204, CONV_TIME_204}, {1, AVERAGE_1 >> 8, 140, CONV_TIME_140}}; // Note: Will update the provided arg to be the correct value INA226_AVERAGES getAverageEnum(uint16_t &samples) { for (const auto &setting : _inaSettingsLookup) { // If a user supplies 2000 samples, we serve up the highest possible value if (samples >= setting.avgSamples) { samples = setting.avgSamples; return static_cast(setting.avgEnum << 8); } } // Default value if not found samples = DEFAULT_INASAMPLES; return DEFAULT_INASAMPLESENUM; } INA226_CONV_TIME getConversionTimeEnum(uint16_t &timeUs) { for (const auto &setting : _inaSettingsLookup) { // If a user supplies 9000 μs, we serve up the highest possible value if (timeUs >= setting.convTimeUs) { timeUs = setting.convTimeUs; return setting.convTimeEnum; } } // Default value if not found timeUs = DEFAULT_INACONVERSIONTIME; return DEFAULT_INACONVERSIONTIMEENUM; } class UsermodINA226 : public Usermod { private: static const char _name[]; unsigned long _lastLoopCheck = 0; unsigned long _lastTriggerTime = 0; bool _settingEnabled : 1; // Enable the usermod bool _mqttPublish : 1; // Publish MQTT values bool _mqttPublishAlways : 1; // Publish always, regardless if there is a change bool _mqttHomeAssistant : 1; // Enable Home Assistant docs bool _initDone : 1; // Initialization is done bool _isTriggeredOperationMode : 1; // false = continuous, true = triggered bool _measurementTriggered : 1; // if triggered mode, then true indicates we're waiting for measurements uint16_t _settingInaConversionTimeUs : 12; // Conversion time, shift by 2 uint16_t _settingInaSamples : 11; // Number of samples for averaging, max 1024 uint8_t _i2cAddress; uint16_t _checkInterval; // milliseconds, user settings is in seconds float _decimalFactor; // a power of 10 factor. 1 would be no change, 10 is one decimal, 100 is two etc. User sees a power of 10 (0, 1, 2, ..) uint16_t _shuntResistor; // Shunt resistor value in milliohms uint16_t _currentRange; // Expected maximum current in milliamps uint8_t _lastStatus = 0; float _lastCurrent = 0; float _lastVoltage = 0; float _lastPower = 0; float _lastShuntVoltage = 0; bool _lastOverflow = false; #ifndef WLED_MQTT_DISABLE float _lastCurrentSent = 0; float _lastVoltageSent = 0; float _lastPowerSent = 0; float _lastShuntVoltageSent = 0; bool _lastOverflowSent = false; #endif INA226_WE *_ina226 = nullptr; float truncateDecimals(float val) { return roundf(val * _decimalFactor) / _decimalFactor; } void initializeINA226() { if (_ina226 != nullptr) { delete _ina226; } _ina226 = new INA226_WE(_i2cAddress); if (!_ina226->init()) { DEBUG_PRINTLN(F("INA226 initialization failed!")); return; } _ina226->setCorrectionFactor(1.0); uint16_t tmpShort = _settingInaSamples; _ina226->setAverage(getAverageEnum(tmpShort)); tmpShort = _settingInaConversionTimeUs << 2; _ina226->setConversionTime(getConversionTimeEnum(tmpShort)); if (_checkInterval >= 20000) { _isTriggeredOperationMode = true; _ina226->setMeasureMode(TRIGGERED); } else { _isTriggeredOperationMode = false; _ina226->setMeasureMode(CONTINUOUS); } _ina226->setResistorRange(static_cast(_shuntResistor) / 1000.0, static_cast(_currentRange) / 1000.0); } void fetchAndPushValues() { _lastStatus = _ina226->getI2cErrorCode(); if (_lastStatus != 0) return; float current = truncateDecimals(_ina226->getCurrent_mA() / 1000.0); float voltage = truncateDecimals(_ina226->getBusVoltage_V()); float power = truncateDecimals(_ina226->getBusPower() / 1000.0); float shuntVoltage = truncateDecimals(_ina226->getShuntVoltage_V()); bool overflow = _ina226->overflow; #ifndef WLED_DISABLE_MQTT mqttPublishIfChanged(F("current"), _lastCurrentSent, current, 0.01f); mqttPublishIfChanged(F("voltage"), _lastVoltageSent, voltage, 0.01f); mqttPublishIfChanged(F("power"), _lastPowerSent, power, 0.1f); mqttPublishIfChanged(F("shunt_voltage"), _lastShuntVoltageSent, shuntVoltage, 0.01f); mqttPublishIfChanged(F("overflow"), _lastOverflowSent, overflow); #endif _lastCurrent = current; _lastVoltage = voltage; _lastPower = power; _lastShuntVoltage = shuntVoltage; _lastOverflow = overflow; } void handleTriggeredMode(unsigned long currentTime) { if (_measurementTriggered) { // Test if we have a measurement every 400ms if (currentTime - _lastTriggerTime >= 400) { _lastTriggerTime = currentTime; if (_ina226->isBusy()) return; fetchAndPushValues(); _measurementTriggered = false; } } else { if (currentTime - _lastLoopCheck >= _checkInterval) { // Start a measurement and use isBusy() later to determine when it is done _ina226->startSingleMeasurementNoWait(); _lastLoopCheck = currentTime; _lastTriggerTime = currentTime; _measurementTriggered = true; } } } void handleContinuousMode(unsigned long currentTime) { if (currentTime - _lastLoopCheck >= _checkInterval) { _lastLoopCheck = currentTime; fetchAndPushValues(); } } #ifndef WLED_DISABLE_MQTT void mqttInitialize() { if (!WLED_MQTT_CONNECTED || !_mqttPublish || !_mqttHomeAssistant) return; char topic[128]; snprintf_P(topic, 127, "%s/current", mqttDeviceTopic); mqttCreateHassSensor(F("Current"), topic, F("current"), F("A")); snprintf_P(topic, 127, "%s/voltage", mqttDeviceTopic); mqttCreateHassSensor(F("Voltage"), topic, F("voltage"), F("V")); snprintf_P(topic, 127, "%s/power", mqttDeviceTopic); mqttCreateHassSensor(F("Power"), topic, F("power"), F("W")); snprintf_P(topic, 127, "%s/shunt_voltage", mqttDeviceTopic); mqttCreateHassSensor(F("Shunt Voltage"), topic, F("voltage"), F("V")); snprintf_P(topic, 127, "%s/overflow", mqttDeviceTopic); mqttCreateHassBinarySensor(F("Overflow"), topic); } void mqttPublishIfChanged(const __FlashStringHelper *topic, float &lastState, float state, float minChange) { if (WLED_MQTT_CONNECTED && _mqttPublish && (_mqttPublishAlways || fabsf(lastState - state) > minChange)) { char subuf[128]; snprintf_P(subuf, 127, PSTR("%s/%s"), mqttDeviceTopic, (const char *)topic); mqtt->publish(subuf, 0, false, String(state).c_str()); lastState = state; } } void mqttPublishIfChanged(const __FlashStringHelper *topic, bool &lastState, bool state) { if (WLED_MQTT_CONNECTED && _mqttPublish && (_mqttPublishAlways || lastState != state)) { char subuf[128]; snprintf_P(subuf, 127, PSTR("%s/%s"), mqttDeviceTopic, (const char *)topic); mqtt->publish(subuf, 0, false, state ? "true" : "false"); lastState = state; } } void mqttCreateHassSensor(const String &name, const String &topic, const String &deviceClass, const String &unitOfMeasurement) { String t = String(F("homeassistant/sensor/")) + mqttClientID + "/" + name + F("/config"); StaticJsonDocument<600> doc; doc[F("name")] = name; doc[F("state_topic")] = topic; doc[F("unique_id")] = String(mqttClientID) + name; if (unitOfMeasurement != "") doc[F("unit_of_measurement")] = unitOfMeasurement; if (deviceClass != "") doc[F("device_class")] = deviceClass; doc[F("expire_after")] = 1800; JsonObject device = doc.createNestedObject(F("device")); device[F("name")] = serverDescription; device[F("identifiers")] = "wled-sensor-" + String(mqttClientID); device[F("manufacturer")] = F(WLED_BRAND); device[F("model")] = F(WLED_PRODUCT_NAME); device[F("sw_version")] = versionString; String temp; serializeJson(doc, temp); DEBUG_PRINTLN(t); DEBUG_PRINTLN(temp); mqtt->publish(t.c_str(), 0, true, temp.c_str()); } void mqttCreateHassBinarySensor(const String &name, const String &topic) { String t = String(F("homeassistant/binary_sensor/")) + mqttClientID + "/" + name + F("/config"); StaticJsonDocument<600> doc; doc[F("name")] = name; doc[F("state_topic")] = topic; doc[F("unique_id")] = String(mqttClientID) + name; JsonObject device = doc.createNestedObject(F("device")); device[F("name")] = serverDescription; device[F("identifiers")] = "wled-sensor-" + String(mqttClientID); device[F("manufacturer")] = F(WLED_BRAND); device[F("model")] = F(WLED_PRODUCT_NAME); device[F("sw_version")] = versionString; String temp; serializeJson(doc, temp); DEBUG_PRINTLN(t); DEBUG_PRINTLN(temp); mqtt->publish(t.c_str(), 0, true, temp.c_str()); } #endif public: UsermodINA226() { // Default values _settingInaSamples = DEFAULT_INASAMPLES; _settingInaConversionTimeUs = DEFAULT_INACONVERSIONTIME; _i2cAddress = INA226_ADDRESS; _checkInterval = DEFAULT_CHECKINTERVAL; _decimalFactor = 100; _shuntResistor = 1000; _currentRange = 1000; } void setup() { initializeINA226(); } void loop() { if (!_settingEnabled || strip.isUpdating()) return; unsigned long currentTime = millis(); if (_isTriggeredOperationMode) { handleTriggeredMode(currentTime); } else { handleContinuousMode(currentTime); } } #ifndef WLED_DISABLE_MQTT void onMqttConnect(bool sessionPresent) { mqttInitialize(); } #endif uint16_t getId() { return USERMOD_ID_INA226; } void addToJsonInfo(JsonObject &root) override { JsonObject user = root["u"]; if (user.isNull()) user = root.createNestedObject("u"); #ifdef USERMOD_INA226_DEBUG JsonArray temp = user.createNestedArray(F("INA226 last loop")); temp.add(_lastLoopCheck); temp = user.createNestedArray(F("INA226 last status")); temp.add(_lastStatus); temp = user.createNestedArray(F("INA226 average samples")); temp.add(_settingInaSamples); temp.add(F("samples")); temp = user.createNestedArray(F("INA226 conversion time")); temp.add(_settingInaConversionTimeUs << 2); temp.add(F("μs")); // INA226 uses (2 * conversion time * samples) time to take a reading. temp = user.createNestedArray(F("INA226 expected sample time")); uint32_t sampleTimeNeededUs = (static_cast(_settingInaConversionTimeUs) << 2) * _settingInaSamples * 2; temp.add(truncateDecimals(sampleTimeNeededUs / 1000.0)); temp.add(F("ms")); temp = user.createNestedArray(F("INA226 mode")); temp.add(_isTriggeredOperationMode ? F("triggered") : F("continuous")); if (_isTriggeredOperationMode) { temp = user.createNestedArray(F("INA226 triggered")); temp.add(_measurementTriggered ? F("waiting for measurement") : F("")); } #endif JsonArray jsonCurrent = user.createNestedArray(F("Current")); JsonArray jsonVoltage = user.createNestedArray(F("Voltage")); JsonArray jsonPower = user.createNestedArray(F("Power")); JsonArray jsonShuntVoltage = user.createNestedArray(F("Shunt Voltage")); JsonArray jsonOverflow = user.createNestedArray(F("Overflow")); if (_lastLoopCheck == 0) { jsonCurrent.add(F("Not read yet")); jsonVoltage.add(F("Not read yet")); jsonPower.add(F("Not read yet")); jsonShuntVoltage.add(F("Not read yet")); jsonOverflow.add(F("Not read yet")); return; } if (_lastStatus != 0) { jsonCurrent.add(F("An error occurred")); jsonVoltage.add(F("An error occurred")); jsonPower.add(F("An error occurred")); jsonShuntVoltage.add(F("An error occurred")); jsonOverflow.add(F("An error occurred")); return; } jsonCurrent.add(_lastCurrent); jsonCurrent.add(F("A")); jsonVoltage.add(_lastVoltage); jsonVoltage.add(F("V")); jsonPower.add(_lastPower); jsonPower.add(F("W")); jsonShuntVoltage.add(_lastShuntVoltage); jsonShuntVoltage.add(F("V")); jsonOverflow.add(_lastOverflow ? F("true") : F("false")); } void addToConfig(JsonObject &root) { JsonObject top = root.createNestedObject(FPSTR(_name)); top[F("Enabled")] = _settingEnabled; top[F("I2CAddress")] = static_cast(_i2cAddress); top[F("CheckInterval")] = _checkInterval / 1000; top[F("INASamples")] = _settingInaSamples; top[F("INAConversionTime")] = _settingInaConversionTimeUs << 2; top[F("Decimals")] = log10f(_decimalFactor); top[F("ShuntResistor")] = _shuntResistor; top[F("CurrentRange")] = _currentRange; #ifndef WLED_DISABLE_MQTT top[F("MqttPublish")] = _mqttPublish; top[F("MqttPublishAlways")] = _mqttPublishAlways; top[F("MqttHomeAssistantDiscovery")] = _mqttHomeAssistant; #endif DEBUG_PRINTLN(F("INA226 config saved.")); } bool readFromConfig(JsonObject &root) override { JsonObject top = root[FPSTR(_name)]; bool configComplete = !top.isNull(); if (!configComplete) return false; bool tmpBool; if (getJsonValue(top[F("Enabled")], tmpBool)) _settingEnabled = tmpBool; else configComplete = false; configComplete &= getJsonValue(top[F("I2CAddress")], _i2cAddress); if (getJsonValue(top[F("CheckInterval")], _checkInterval)) { if (1 <= _checkInterval && _checkInterval <= 600) _checkInterval *= 1000; else _checkInterval = DEFAULT_CHECKINTERVAL; } else configComplete = false; uint16_t tmpShort; if (getJsonValue(top[F("INASamples")], tmpShort)) { // The method below will fix the provided value to a valid one getAverageEnum(tmpShort); _settingInaSamples = tmpShort; } else configComplete = false; if (getJsonValue(top[F("INAConversionTime")], tmpShort)) { // The method below will fix the provided value to a valid one getConversionTimeEnum(tmpShort); _settingInaConversionTimeUs = tmpShort >> 2; } else configComplete = false; if (getJsonValue(top[F("Decimals")], _decimalFactor)) { if (0 <= _decimalFactor && _decimalFactor <= 5) _decimalFactor = pow10f(_decimalFactor); else _decimalFactor = 100; } else configComplete = false; configComplete &= getJsonValue(top[F("ShuntResistor")], _shuntResistor); configComplete &= getJsonValue(top[F("CurrentRange")], _currentRange); #ifndef WLED_DISABLE_MQTT if (getJsonValue(top[F("MqttPublish")], tmpBool)) _mqttPublish = tmpBool; else configComplete = false; if (getJsonValue(top[F("MqttPublishAlways")], tmpBool)) _mqttPublishAlways = tmpBool; else configComplete = false; if (getJsonValue(top[F("MqttHomeAssistantDiscovery")], tmpBool)) _mqttHomeAssistant = tmpBool; else configComplete = false; #endif if (_initDone) { initializeINA226(); #ifndef WLED_DISABLE_MQTT mqttInitialize(); #endif } _initDone = true; return configComplete; } ~UsermodINA226() { delete _ina226; _ina226 = nullptr; } }; const char UsermodINA226::_name[] PROGMEM = "INA226"; static UsermodINA226 ina226_v2; REGISTER_USERMOD(ina226_v2); ================================================ FILE: usermods/INA226_v2/README.md ================================================ # Usermod INA226 This Usermod is designed to read values from an INA226 sensor and output the following: - Current - Voltage - Power - Shunt Voltage - Overflow status ## Configuration The following settings can be configured in the Usermod Menu: - **Enabled**: Enable or disable the usermod. - **I2CAddress**: The I2C address in decimal. Default is 64 (0x40). - **CheckInterval**: Number of seconds between readings. This should be higher than the time it takes to make a reading, determined by the two next options. - **INASamples**: The number of samples to configure the INA226 to use for a measurement. Higher counts provide more accuracy. See the 'Understanding Samples and Conversion Times' section for more details. - **INAConversionTime**: The time to use on converting and preparing readings on the INA226. Higher times provide more precision. See the 'Understanding Samples and Conversion Times' section for more details. - **Decimals**: Number of decimals in the output. - **ShuntResistor**: Shunt resistor value in milliohms. An R100 shunt resistor should be written as "100", while R010 should be "10". - **CurrentRange**: Expected maximum current in milliamps (e.g., 5 A = 5000 mA). - **MqttPublish**: Enable or disable MQTT publishing. - **MqttPublishAlways**: Publish always, regardless if there is a change. - **MqttHomeAssistantDiscovery**: Enable Home Assistant discovery. ## Understanding Samples and Conversion Times The INA226 uses a programmable ADC with configurable conversion times and averaging to optimize the measurement accuracy and speed. The conversion time and number of samples are determined based on the `INASamples` and `INAConversionTime` settings. The following table outlines the possible combinations: | Conversion Time (μs) | 1 Sample | 4 Samples | 16 Samples | 64 Samples | 128 Samples | 256 Samples | 512 Samples | 1024 Samples | |----------------------|----------|-----------|------------|------------|-------------|-------------|-------------|--------------| | 140 | 0.28 ms | 1.12 ms | 4.48 ms | 17.92 ms | 35.84 ms | 71.68 ms | 143.36 ms | 286.72 ms | | 204 | 0.408 ms | 1.632 ms | 6.528 ms | 26.112 ms | 52.224 ms | 104.448 ms | 208.896 ms | 417.792 ms | | 332 | 0.664 ms | 2.656 ms | 10.624 ms | 42.496 ms | 84.992 ms | 169.984 ms | 339.968 ms | 679.936 ms | | 588 | 1.176 ms | 4.704 ms | 18.816 ms | 75.264 ms | 150.528 ms | 301.056 ms | 602.112 ms | 1204.224 ms | | 1100 | 2.2 ms | 8.8 ms | 35.2 ms | 140.8 ms | 281.6 ms | 563.2 ms | 1126.4 ms | 2252.8 ms | | 2116 | 4.232 ms | 16.928 ms | 67.712 ms | 270.848 ms | 541.696 ms | 1083.392 ms | 2166.784 ms | 4333.568 ms | | 4156 | 8.312 ms | 33.248 ms | 132.992 ms | 531.968 ms | 1063.936 ms | 2127.872 ms | 4255.744 ms | 8511.488 ms | | 8244 | 16.488 ms| 65.952 ms | 263.808 ms | 1055.232 ms| 2110.464 ms | 4220.928 ms | 8441.856 ms | 16883.712 ms | It is important to pick a combination that provides the needed balance between accuracy and precision while ensuring new readings within the `CheckInterval` setting. When `USERMOD_INA226_DEBUG` is defined, the info pane contains the expected time to make a reading, which can be seen in the table above. As an example, if you want a new reading every 5 seconds (`CheckInterval`), a valid combination is `256 samples` and `4156 μs` which would provide new values every 2.1 seconds. The picked values also slightly affect power usage. If the `CheckInterval` is set to more than 20 seconds, the INA226 is configured in `triggered` reading mode, where it only uses power as long as it's working. Then the conversion time and average samples counts determine how long the chip stays turned on every `CheckInterval` time. ### Calculating Current and Power The INA226 calculates current by measuring the differential voltage across a shunt resistor and using the calibration register value to convert this measurement into current. Power is calculated by multiplying the current by the bus voltage. For detailed programming information and register configurations, refer to the [INA226 datasheet](https://www.ti.com/product/INA226). ## Author [@LordMike](https://github.com/LordMike) ## Compiling To enable, compile with `INA226` in `custom_usermods` (e.g. in `platformio_override.ini`). ```ini [env:ina226_example] extends = env:esp32dev custom_usermods = ${env:esp32dev.custom_usermods} INA226 build_flags = ${env:esp32dev.build_flags} ; -D USERMOD_INA226_DEBUG ; -- add a debug status to the info modal ``` ================================================ FILE: usermods/INA226_v2/library.json ================================================ { "name": "INA226_v2", "build": { "libArchive": false }, "dependencies": { "wollewald/INA226_WE":"~1.2.9" } } ================================================ FILE: usermods/Internal_Temperature_v2/Internal_Temperature_v2.cpp ================================================ #include "wled.h" class InternalTemperatureUsermod : public Usermod { private: static constexpr unsigned long minLoopInterval = 1000; // minimum allowable interval (ms) unsigned long loopInterval = 10000; unsigned long lastTime = 0; bool isEnabled = false; float temperature = 0.0f; uint8_t previousPlaylist = 0; // Stores the playlist that was active before high-temperature activation uint8_t previousPreset = 0; // Stores the preset that was active before high-temperature activation uint8_t presetToActivate = 0; // Preset to activate when temp goes above threshold (0 = disabled) float activationThreshold = 95.0f; // Temperature threshold to trigger high-temperature actions float resetMargin = 2.0f; // Margin below the activation threshold (Prevents frequent toggling when close to threshold) bool isAboveThreshold = false; // Flag to track if the high temperature preset is currently active static const char _name[]; static const char _enabled[]; static const char _loopInterval[]; static const char _activationThreshold[]; static const char _presetToActivate[]; // any private methods should go here (non-inline method should be defined out of class) void publishMqtt(const char *state, bool retain = false); // example for publishing MQTT message public: void setup() { } void loop() { // if usermod is disabled or called during strip updating just exit // NOTE: on very long strips strip.isUpdating() may always return true so update accordingly if (!isEnabled || strip.isUpdating() || millis() - lastTime <= loopInterval) return; lastTime = millis(); // Measure the temperature #ifdef ESP8266 // ESP8266 // does not seem possible temperature = -1; #elif defined(CONFIG_IDF_TARGET_ESP32S2) // ESP32S2 temperature = -1; #else // ESP32 ESP32S3 and ESP32C3 temperature = roundf(temperatureRead() * 10) / 10; #endif if(presetToActivate != 0){ // Check if temperature has exceeded the activation threshold if (temperature >= activationThreshold) { // Update the state flag if not already set if (!isAboveThreshold) { isAboveThreshold = true; } // Check if a 'high temperature' preset is configured and it's not already active if (currentPreset != presetToActivate) { // If a playlist is active, store it for reactivation later if (currentPlaylist > 0) { previousPlaylist = currentPlaylist; } // If a preset is active, store it for reactivation later else if (currentPreset > 0) { previousPreset = currentPreset; // If no playlist or preset is active, save current state for reactivation later } else { saveTemporaryPreset(); } // Activate the 'high temperature' preset applyPreset(presetToActivate); } } // Check if temperature is back below the threshold else if (temperature <= (activationThreshold - resetMargin)) { // Update the state flag if not already set if (isAboveThreshold){ isAboveThreshold = false; } // Check if the 'high temperature' preset is active if (currentPreset == presetToActivate) { // Check if a previous playlist was stored if (previousPlaylist > 0) { // Reactivate the stored playlist applyPreset(previousPlaylist); // Clear the stored playlist previousPlaylist = 0; } // Check if a previous preset was stored else if (previousPreset > 0) { // Reactivate the stored preset applyPreset(previousPreset); // Clear the stored preset previousPreset = 0; // If no previous playlist or preset was stored, revert to the stored state } else { applyTemporaryPreset(); } } } } #ifndef WLED_DISABLE_MQTT if (WLED_MQTT_CONNECTED) { char array[10]; snprintf(array, sizeof(array), "%f", temperature); publishMqtt(array); } #endif } void addToJsonInfo(JsonObject &root) { if (!isEnabled) return; // if "u" object does not exist yet wee need to create it JsonObject user = root["u"]; if (user.isNull()) user = root.createNestedObject("u"); JsonArray userTempArr = user.createNestedArray(FPSTR(_name)); userTempArr.add(temperature); userTempArr.add(F(" °C")); // if "sensor" object does not exist yet wee need to create it JsonObject sensor = root[F("sensor")]; if (sensor.isNull()) sensor = root.createNestedObject(F("sensor")); JsonArray sensorTempArr = sensor.createNestedArray(FPSTR(_name)); sensorTempArr.add(temperature); sensorTempArr.add(F("°C")); } void addToConfig(JsonObject &root) { JsonObject top = root.createNestedObject(FPSTR(_name)); top[FPSTR(_enabled)] = isEnabled; top[FPSTR(_loopInterval)] = loopInterval; top[FPSTR(_activationThreshold)] = activationThreshold; top[FPSTR(_presetToActivate)] = presetToActivate; } // Append useful info to the usermod settings gui void appendConfigData() { // Display 'ms' next to the 'Loop Interval' setting oappend(F("addInfo('Internal Temperature:Loop Interval', 1, 'ms');")); // Display '°C' next to the 'Activation Threshold' setting oappend(F("addInfo('Internal Temperature:Activation Threshold', 1, '°C');")); // Display '0 = Disabled' next to the 'Preset To Activate' setting oappend(F("addInfo('Internal Temperature:Preset To Activate', 1, '0 = unused');")); } bool readFromConfig(JsonObject &root) { JsonObject top = root[FPSTR(_name)]; bool configComplete = !top.isNull(); configComplete &= getJsonValue(top[FPSTR(_enabled)], isEnabled); configComplete &= getJsonValue(top[FPSTR(_loopInterval)], loopInterval); loopInterval = max(loopInterval, minLoopInterval); // Makes sure the loop interval isn't too small. configComplete &= getJsonValue(top[FPSTR(_presetToActivate)], presetToActivate); configComplete &= getJsonValue(top[FPSTR(_activationThreshold)], activationThreshold); return configComplete; } uint16_t getId() { return USERMOD_ID_INTERNAL_TEMPERATURE; } }; const char InternalTemperatureUsermod::_name[] PROGMEM = "Internal Temperature"; const char InternalTemperatureUsermod::_enabled[] PROGMEM = "Enabled"; const char InternalTemperatureUsermod::_loopInterval[] PROGMEM = "Loop Interval"; const char InternalTemperatureUsermod::_activationThreshold[] PROGMEM = "Activation Threshold"; const char InternalTemperatureUsermod::_presetToActivate[] PROGMEM = "Preset To Activate"; void InternalTemperatureUsermod::publishMqtt(const char *state, bool retain) { #ifndef WLED_DISABLE_MQTT // Check if MQTT Connected, otherwise it will crash the 8266 if (WLED_MQTT_CONNECTED) { char subuf[64]; strcpy(subuf, mqttDeviceTopic); strcat_P(subuf, PSTR("/mcutemp")); mqtt->publish(subuf, 0, retain, state); } #endif } static InternalTemperatureUsermod internal_temperature_v2; REGISTER_USERMOD(internal_temperature_v2); ================================================ FILE: usermods/Internal_Temperature_v2/library.json ================================================ { "name": "Internal_Temperature_v2", "build": { "libArchive": false } } ================================================ FILE: usermods/Internal_Temperature_v2/readme.md ================================================ # Internal Temperature Usermod ![Screenshot of WLED info page](assets/screenshot_info.png) ![Screenshot of WLED usermod settings page](assets/screenshot_settings.png) ## Features - 🌡️ Adds the internal temperature readout of the chip to the `Info` tab - 🥵 High temperature indicator/action. (Configurable threshold and preset) - 📣 Publishes the internal temperature over the MQTT topic: `mcutemp` ## Use Examples - Warn of excessive/damaging temperatures by the triggering of a 'warning' preset - Activate a cooling fan (when used with the multi-relay usermod) ## Compatibility - A shown temp of 53,33°C might indicate that the internal temp is not supported - ESP8266 does not have a internal temp sensor -> Disabled (Indicated with a readout of '-1') - ESP32S2 seems to crash on reading the sensor -> Disabled (Indicated with a readout of '-1') ## Installation - Add `Internal_Temperature` to `custom_usermods` in your `platformio.ini` (or `platformio_override.ini`). ## 📝 Change Log 2024-06-26 - Added "high-temperature-indication" feature - Documentation updated 2023-09-01 * "Internal Temperature" usermod created ## Authors - Soeren Willrodt [@lost-hope](https://github.com/lost-hope) - Dimitry Zhemkov [@dima-zhemkov](https://github.com/dima-zhemkov) - Adam Matthews [@adamsthws](https://github.com/adamsthws) ================================================ FILE: usermods/JSON_IR_remote/21-key_ir.json ================================================ { "desc": "21-key", "0xFFA25D": { "label": "On", "pos": "1x1", "cmd": "T=1" }, "0xFF629D": { "label": "Off", "pos": "1x2", "cmd": "T=0" }, "0xFFE21D": { "label": "Flash", "pos": "1x3", "cmnt": "Cycle Effects", "cmd": "CY=0&FX=~" }, "0xFF22DD": { "label": "Strobe", "pos": "2x1", "cmnt": "Sinelon Dual", "cmd": "CY=0&FX=93" }, "0xFF02FD": { "label": "Fade", "pos": "2x2", "cmnt": "Rain", "cmd": "CY=0&FX=43" }, "0xFFC23D": { "label": "Smooth", "pos": "2x3", "cmnt": "Aurora", "cmd": "CY=0&FX=38" }, "0xFFE01F": { "label": "Bright +", "pos": "3x1", "cmd": "A=~16" }, "0xFFA857": { "label": "Bright -", "pos": "3x2", "cmd": "A=~-16" }, "0xFF906F": { "label": "White", "pos": "3x3", "cmd": "FP=5&CL=hFFFFFF&C2=hFFFFFF&C3=hA8A8A8" }, "0xFF6897": { "label": "Red", "pos": "4x1", "cmnt": "Lava", "cmd": "FP=8" }, "0xFF9867": { "label": "Green", "pos": "4x2", "cmnt": "Forest", "cmd": "FP=10" }, "0xFFB04F": { "label": "Blue", "pos": "4x3", "cmnt": "Breeze", "cmd": "FP=15" }, "0xFF30CF": { "label": "Tomato", "pos": "5x1", "cmd": "FP=5&CL=hFF6347&C2=hFFBF47&C3=hA85859" }, "0xFF18E7": { "label": "LightGreen", "pos": "5x2", "cmnt": "Rivendale", "cmd": "FP=14" }, "0xFF7A85": { "label": "SkyBlue", "pos": "5x3", "cmnt": "Ocean", "cmd": "FP=9" }, "0xFF10EF": { "label": "Orange", "pos": "6x1", "cmnt": "Orangery", "cmd": "FP=47" }, "0xFF38C7": { "label": "Aqua", "pos": "6x2", "cmd": "FP=5&CL=hFFFF&C2=h7FFF&C3=h39A895" }, "0xFF5AA5": { "label": "Purple", "pos": "6x3", "cmd": "FP=5&CL=h663399&C2=h993399&C3=h473864" }, "0xFF42BD": { "label": "Yellow", "pos": "7x1", "cmd": "FP=5&CL=hFFFF00&C2=hFFC800&C3=hFDFFDE" }, "0xFF4AB5": { "label": "Cyan", "pos": "7x2", "cmnt": "Beech", "cmd": "FP=22" }, "0xFF52AD": { "label": "Pink", "pos": "7x3", "cmd": "FP=5&CL=hFFC0CB&C2=hFFD4C0&C3=hA88C96" } } ================================================ FILE: usermods/JSON_IR_remote/24-key_ir.json ================================================ { "desc": "24-key", "0xF700FF": { "label": "+", "pos": "1x1", "cmnt": "Speed +", "cmd": "SX=~16" }, "0xF7807F": { "label": "-", "pos": "1x2", "cmnt": "Speed -", "cmd": "SX=~-16" }, "0xF740BF": { "label": "On/Off", "pos": "1x3", "cmnt": "Toggle On/Off", "cmd": "T=2" }, "0xF7C03F": { "label": "W", "pos": "1x4", "cmnt": "Cycle color palette", "cmd": "FP=~" }, "0xF720DF": { "label": "R", "pos": "2x1", "cmnt": "Lava", "cmd": "FP=8" }, "0xF7A05F": { "label": "G", "pos": "2x2", "cmnt": "Forest", "cmd": "FP=10" }, "0xF7609F": { "label": "B", "pos": "2x3", "cmnt": "Breeze", "cmd": "FP=15" }, "0xF7E01F": { "label": "Bright -", "pos": "2x4", "cmnt": "Bright -", "cmd": "A=~-16" }, "0xF710EF": { "label": "Timer1H", "pos": "3x1", "cmnt": "Timer 60 min", "cmd": "NL=60&NT=0" }, "0xF7906F": { "label": "Timer4H", "pos": "3x2", "cmnt": "Timer 30 min", "cmd": "NL=30&NT=0" }, "0xF750AF": { "label": "Timer8H", "pos": "3x3", "cmnt": "Timer 15 min", "cmd": "NL=15&NT=0" }, "0xF7D02F": { "label": "Bright128", "pos": "3x4", "cmnt": "Bright 128", "cmd": "A=128" }, "0xF730CF": { "label": "Music1", "pos": "4x1", "cmnt": "Cycle FX +", "cmd": "FX=~" }, "0xF7B04F": { "label": "Music2", "pos": "4x2", "cmnt": "Cycle FX -", "cmd": "FX=~-1" }, "0xF7708F": { "label": "Music3", "pos": "4x3", "cmnt": "Reset FX and FP", "cmd": "FX=1&PF=6" }, "0xF7F00F": { "label": "Bright +", "pos": "4x4", "cmnt": "Bright +", "cmd": "A=~16" }, "0xF708F7": { "label": "Mode1", "pos": "5x1", "cmnt": "Preset 1", "cmd": "PL=1" }, "0xF78877": { "label": "Mode2", "pos": "5x2", "cmnt": "Preset 2", "cmd": "PL=2" }, "0xF748B7": { "label": "Mode3", "pos": "5x3", "cmnt": "Preset 3", "cmd": "PL=3" }, "0xF7C837": { "label": "Up", "pos": "5x4", "cmnt": "Intensity +", "cmd": "IX=~16" }, "0xF728D7": { "label": "Mode4", "pos": "6x1", "cmnt": "Preset 4", "cmd": "PL=4" }, "0xF7A857": { "label": "Mode5", "pos": "6x2", "cmnt": "Preset 5", "cmd": "PL=5" }, "0xF76897": { "label": "Cycle", "pos": "6x3", "cmnt": "Toggle preset cycle", "cmd": "CY=1&PT=60000" }, "0xF7E817": { "label": "Down", "pos": "6x4", "cmnt": "Intensity -", "cmd": "IX=~-16" } } ================================================ FILE: usermods/JSON_IR_remote/32-key_ir.json ================================================ { "desc": "32-key", "0xFF08F7": { "label": "On", "pos": "1x1", "cmd": "T=1" }, "0xFFC03F": { "label": "Off", "pos": "1x2", "cmd": "T=0" }, "0xFF807F": { "label": "Auto", "pos": "1x3", "cmnt": "Toggle preset cycle", "cmd": "CY=2" }, "0xFF609F": { "label": "Mode", "pos": "1x4", "cmnt": "Cycle effects", "cmd": "FX=~&CY=0" }, "0xFF906F": { "label": "4H", "pos": "2x1", "cmnt": "Timer 60min", "cmd": "NL=60&NT=0" }, "0xFFB847": { "label": "6H", "pos": "2x2", "cmnt": "Timer 90min", "cmd": "NL=90&NT=0" }, "0xFFF807": { "label": "8H", "pos": "2x3", "cmnt": "Timer 120min", "cmd": "NL=120&NT=0" }, "0xFFB04F": { "label": "Timer Off", "pos": "2x4", "cmd": "NL=0" }, "0xFF9867": { "label": "Red", "pos": "3x1", "cmnt": "Lava", "cmd": "FP=8" }, "0xFFD827": { "label": "Green", "pos": "3x2", "cmnt": "Forest", "cmd": "FP=10" }, "0xFF8877": { "label": "Blue", "pos": "3x3", "cmnt": "Breeze", "cmd": "FP=15" }, "0xFFA857": { "label": "White", "pos": "3x4", "cmd": "FP=5&CL=hFFFFFF&C2=hFFE4CD&C3=hE4E4FF" }, "0xFFE817": { "label": "OrangeRed", "pos": "4x1", "cmnt": "Sakura", "cmd": "FP=49" }, "0xFF48B7": { "label": "SeaGreen", "pos": "4x2", "cmnt": "Rivendale", "cmd": "FP=14" }, "0xFF6897": { "label": "RoyalBlue", "pos": "4x3", "cmnt": "Ocean", "cmd": "FP=9" }, "0xFFB24D": { "label": "DarkBlue", "pos": "4x4", "cmnt": "Breeze", "cmd": "FP=15" }, "0xFF02FD": { "label": "Orange", "pos": "5x1", "cmnt": "Orangery", "cmd": "FP=47" }, "0xFF32CD": { "label": "YellowGreen", "pos": "5x2", "cmnt": "Aurora", "cmd": "FP=37" }, "0xFF20DF": { "label": "SkyBlue", "pos": "5x3", "cmnt": "Beech", "cmd": "FP=22" }, "0xFF00FF": { "label": "Orchid", "pos": "5x4", "cmd": "FP=5&CL=hDA70D6&C2=hDA70A0&C3=h89618F" }, "0xFF50AF": { "label": "Yellow", "pos": "6x1", "cmd": "FP=5&CL=hFFFF00&C2=hFFC800&C3=hFDFFDE" }, "0xFF7887": { "label": "DarkGreen", "pos": "6x2", "cmnt": "Orange and Teal", "cmd": "FP=44" }, "0xFF708F": { "label": "RebeccaPurple", "pos": "6x3", "cmd": "FP=5&CL=h800080&C2=h800040&C3=h4B1C54" }, "0xFF58A7": { "label": "Plum", "pos": "6x4", "cmd": "FP=5&CL=hDDA0DD&C2=hDDA0BE&C3=h8D7791" }, "0xFF38C7": { "label": "Strobe", "pos": "7x1", "cmnt": "Dancing Shadows", "cmd": "FX=112&CY=0" }, "0xFF28D7": { "label": "In Waves", "pos": "7x2", "cmnt": "Noise 1", "cmd": "FX=70&CY=0" }, "0xFFF00F": { "label": "Speed +", "pos": "7x3", "cmd": "SX=~16" }, "0xFF30CF": { "label": "Speed -", "pos": "7x4", "cmd": "SX=~-16" }, "0xFF40BF": { "label": "Jump", "pos": "8x1", "cmnt": "Colortwinkles", "cmd": "FX=74&CY=0" }, "0xFF12ED": { "label": "Fade", "pos": "8x2", "cmnt": "Sunrise", "cmd": "FX=104&CY=0" }, "0xFF2AD5": { "label": "Flash", "pos": "8x3", "cmnt": "Railway", "cmd": "FX=78&CY=0" }, "0xFFA05F": { "label": "Chase Flash", "pos": "8x4", "cmnt": "Washing Machine", "cmd": "FX=113&CY=0" } } ================================================ FILE: usermods/JSON_IR_remote/40-key-black_ir.json ================================================ { "desc": "40-key-black", "0xFF3AC5": { "label": "Bright +", "pos": "1x1", "cmd": "A=~16" }, "0xFFBA45": { "label": "Bright -", "pos": "1x2", "cmd": "A=~-16" }, "0xFF827D": { "label": "Off", "pos": "1x3", "cmd": "T=0" }, "0xFF02FD": { "label": "On", "pos": "1x4", "cmd": "T=1" }, "0xFF1AE5": { "label": "Red", "pos": "2x1", "cmnt": "Lava", "cmd": "FP=8" }, "0xFF9A65": { "label": "Green", "pos": "2x2", "cmnt": "Forest", "cmd": "FP=10" }, "0xFFA25D": { "label": "Blue", "pos": "2x3", "cmnt": "Breeze", "cmd": "FP=15" }, "0xFF22DD": { "label": "White", "pos": "2x4", "cmd": "FP=5&CL=hFFFFFF&C2=hFFFFFF&C3=hA8A8A8" }, "0xFF2AD5": { "label": "Tomato", "pos": "3x1", "cmnt": "Yelmag", "cmd": "FP=5&CL=hFF6347&C2=hFFBF47&C3=hA85859" }, "0xFFAA55": { "label": "LightGreen", "pos": "3x2", "cmnt": "Rivendale", "cmd": "FP=14" }, "0xFF926D": { "label": "SkyBlue", "pos": "3x3", "cmnt": "Ocean", "cmd": "FP=9" }, "0xFF12ED": { "label": "WarmWhite", "pos": "3x4", "cmnt": "Warm White", "cmd": "FP=5&CL=hFFE4CD&C2=hFFFCCD&C3=hA89892" }, "0xFF0AF5": { "label": "OrangeRed", "pos": "4x1", "cmnt": "Sakura", "cmd": "FP=49" }, "0xFF8A75": { "label": "Cyan", "pos": "4x2", "cmnt": "Beech", "cmd": "FP=22" }, "0xFFB24D": { "label": "RebeccaPurple", "pos": "4x3", "cmd": "FP=5&CL=h663399&C2=h993399&C3=h473864" }, "0xFF32CD": { "label": "CoolWhite", "pos": "4x4", "cmnt": "Cool White", "cmd": "FP=5&CL=hE4E4FF&C2=hF1E4FF&C3=h9C9EA8" }, "0xFF38C7": { "label": "Orange", "pos": "5x1", "cmnt": "Orangery", "cmd": "FP=47" }, "0xFFB847": { "label": "Turquoise", "pos": "5x2", "cmd": "FP=5&CL=h40E0D0&C2=h40A0E0&C3=h4E9381" }, "0xFF7887": { "label": "Purple", "pos": "5x3", "cmd": "FP=5&CL=h800080&C2=h800040&C3=h4B1C54" }, "0xFFF807": { "label": "MedGray", "pos": "5x4", "cmnt": "Cycle palette +", "cmd": "FP=~" }, "0xFF18E7": { "label": "Yellow", "pos": "6x1", "cmd": "FP=5&CL=hFFFF00&C2=h7FFF00&C3=hA89539" }, "0xFF9867": { "label": "DarkCyan", "pos": "6x2", "cmd": "FP=5&CL=h8B8B&C2=h458B&C3=h1F5B51" }, "0xFF58A7": { "label": "Plum", "pos": "6x3", "cmnt": "Magenta", "cmd": "FP=40" }, "0xFFD827": { "label": "DarkGray", "pos": "6x4", "cmnt": "Cycle palette -", "cmd": "FP=~-" }, "0xFF28D7": { "label": "Jump3", "pos": "7x1", "cmnt": "Colortwinkles", "cmd": "CY=0&FX=74" }, "0xFFA857": { "label": "Fade3", "pos": "7x2", "cmnt": "Rain", "cmd": "CY=0&FX=43" }, "0xFF6897": { "label": "Flash", "pos": "7x3", "cmnt": "Cycle Effects", "cmd": "CY=0&FX=~" }, "0xFFE817": { "label": "Quick", "pos": "7x4", "cmnt": "Fx speed +16", "cmd": "SX=~16" }, "0xFF08F7": { "label": "Jump7", "pos": "8x1", "cmnt": "Sinelon Dual", "cmd": "CY=0&FX=93" }, "0xFF8877": { "label": "Fade7", "pos": "8x2", "cmnt": "Lighthouse", "cmd": "CY=0&FX=41" }, "0xFF48B7": { "label": "Auto", "pos": "8x3", "cmnt": "Toggle preset cycle", "cmd": "CY=2" }, "0xFFC837": { "label": "Slow", "pos": "8x4", "cmnt": "FX speed -16", "cmd": "SX=~-16" }, "0xFF30CF": { "label": "Custom1", "pos": "9x1", "cmnt": "Noise 1", "cmd": "CY=0&FX=70" }, "0xFFB04F": { "label": "Custom2", "pos": "9x2", "cmnt": "Dancing Shadows", "cmd": "CY=0&FX=112" }, "0xFF708F": { "label": "Music +", "pos": "9x3", "cmnt": "FX Intensity +16", "cmd": "IX=~16" }, "0xFFF00F": { "label": "Timer60", "pos": "9x4", "cmnt": "Timer 60 min", "cmd": "NL=60&NT=0" }, "0xFF10EF": { "label": "Custom3", "pos": "10x1", "cmnt": "Twinklefox", "cmd": "CY=0&FX=80" }, "0xFF906F": { "label": "Custom4", "pos": "10x2", "cmnt": "Twinklecat", "cmd": "CY=0&FX=81" }, "0xFF50AF": { "label": "Music -", "pos": "10x3", "cmnt": "FX Intesity -16", "cmd": "IX=~-16" }, "0xFFD02F": { "label": "Timer120", "pos": "10x4", "cmnt": "Timer 120 min", "cmd": "NL=120&NT=0" } } ================================================ FILE: usermods/JSON_IR_remote/40-key-blue_ir.json ================================================ { "desc": "40-key-blue", "0xFF3AC5": { "label": "Bright +", "pos": "1x1", "cmd": "A=~16" }, "0xFFBA45": { "label": "Bright -", "pos": "1x2", "cmd": "A=~-16" }, "0xFF827D": { "label": "Off", "pos": "1x3", "cmd": "T=0" }, "0xFF02FD": { "label": "On", "pos": "1x4", "cmd": "T=1" }, "0xFF1AE5": { "label": "Red", "pos": "2x1", "cmnt": "Lava", "cmd": "FP=8" }, "0xFF9A65": { "label": "Green", "pos": "2x2", "cmnt": "Forest", "cmd": "FP=10" }, "0xFFA25D": { "label": "Blue", "pos": "2x3", "cmnt": "Breeze", "cmd": "FP=15" }, "0xFF22DD": { "label": "White", "pos": "2x4", "cmd": "FP=5&CL=hFFFFFF&C2=hFFFFFF&C3=hA8A8A8" }, "0xFF2AD5": { "label": "Tomato", "pos": "3x1", "cmnt": "Yelmag", "cmd": "FP=5&CL=hFF6347&C2=hFFBF47&C3=hA85859" }, "0xFFAA55": { "label": "LightGreen", "pos": "3x2", "cmnt": "Rivendale", "cmd": "FP=14" }, "0xFF926D": { "label": "SkyBlue", "pos": "3x3", "cmnt": "Ocean", "cmd": "FP=9" }, "0xFF12ED": { "label": "WarmWhite", "pos": "3x4", "cmnt": "Warm White", "cmd": "FP=5&CL=hFFE4CD&C2=hFFFCCD&C3=hA89892" }, "0xFF0AF5": { "label": "OrangeRed", "pos": "4x1", "cmnt": "Sakura", "cmd": "FP=49" }, "0xFF8A75": { "label": "Cyan", "pos": "4x2", "cmnt": "Beech", "cmd": "FP=22" }, "0xFFB24D": { "label": "RebeccaPurple", "pos": "4x3", "cmd": "FP=5&CL=h663399&C2=h993399&C3=h473864" }, "0xFF32CD": { "label": "CoolWhite", "pos": "4x4", "cmnt": "Cool White", "cmd": "FP=5&CL=hE4E4FF&C2=hF1E4FF&C3=h9C9EA8" }, "0xFF38C7": { "label": "Orange", "pos": "5x1", "cmnt": "Orangery", "cmd": "FP=47" }, "0xFFB847": { "label": "Turquoise", "pos": "5x2", "cmd": "FP=5&CL=h40E0D0&C2=h40A0E0&C3=h4E9381" }, "0xFF7887": { "label": "Purple", "pos": "5x3", "cmd": "FP=5&CL=h800080&C2=h800040&C3=h4B1C54" }, "0xFFF807": { "label": "MedGray", "pos": "5x4", "cmnt": "Cycle palette +", "cmd": "FP=~" }, "0xFF18E7": { "label": "Yellow", "pos": "6x1", "cmd": "FP=5&CL=hFFFF00&C2=h7FFF00&C3=hA89539" }, "0xFF9867": { "label": "DarkCyan", "pos": "6x2", "cmd": "FP=5&CL=h8B8B&C2=h458B&C3=h1F5B51" }, "0xFF58A7": { "label": "Plum", "pos": "6x3", "cmnt": "Magenta", "cmd": "FP=40" }, "0xFFD827": { "label": "DarkGray", "pos": "6x4", "cmnt": "Cycle palette -", "cmd": "FP=~-" }, "0xFF28D7": { "label": "W +", "pos": "7x1" }, "0xFFA857": { "label": "W -", "pos": "7x2" }, "0xFF6897": { "label": "W On", "pos": "7x3" }, "0xFFE817": { "label": "W Off", "pos": "7x4" }, "0xFF08F7": { "label": "W25", "pos": "8x1" }, "0xFF8877": { "label": "W50", "pos": "8x2" }, "0xFF48B7": { "label": "W75", "pos": "8x3" }, "0xFFC837": { "label": "W100", "pos": "8x4" }, "0xFF30CF": { "label": "Jump3", "pos": "9x1", "cmnt": "Colortwinkles", "cmd": "CY=0&FX=74" }, "0xFFB04F": { "label": "Fade3", "pos": "9x2", "cmnt": "Rain", "cmd": "CY=0&FX=43" }, "0xFF708F": { "label": "Jump7", "pos": "9x3", "cmnt": "Sinelon Dual", "cmd": "CY=0&FX=93" }, "0xFFF00F": { "label": "Quick", "pos": "9x4", "cmnt": "Fx speed +16", "cmd": "SX=~16" }, "0xFF10EF": { "label": "Fade", "pos": "10x1", "cmnt": "Lighthouse", "cmd": "CY=0&FX=41" }, "0xFF906F": { "label": "Flash", "pos": "10x2", "cmnt": "Cycle Effects", "cmd": "CY=0&FX=~" }, "0xFF50AF": { "label": "Auto", "pos": "10x3", "cmnt": "Toggle preset cycle", "cmd": "CY=2" }, "0xFFD02F": { "label": "Slow", "pos": "10x4", "cmnt": "Sinelon Dual", "cmd": "CY=0&FX=93" } } ================================================ FILE: usermods/JSON_IR_remote/44-key_ir.json ================================================ { "desc": "44-key", "0xFF3AC5": { "label": "Bright +", "pos": "1x1", "cmd": "A=~16" }, "0xFFBA45": { "label": "Bright -", "pos": "1x2", "cmd": "A=~-16" }, "0xFF827D": { "label": "Off", "pos": "1x3", "cmd": "T=0" }, "0xFF02FD": { "label": "On", "pos": "1x4", "cmd": "T=1" }, "0xFF1AE5": { "label": "Red", "pos": "2x1", "cmnt": "Lava", "cmd": "FP=8" }, "0xFF9A65": { "label": "Green", "pos": "2x2", "cmnt": "Forest", "cmd": "FP=10" }, "0xFFA25D": { "label": "Blue", "pos": "2x3", "cmnt": "Breeze", "cmd": "FP=15" }, "0xFF22DD": { "label": "White", "pos": "2x4", "cmd": "FP=5&CL=hFFFFFF&C2=hFFFFFF&C3=hA8A8A8" }, "0xFF2AD5": { "label": "Tomato", "pos": "3x1", "cmd": "FP=5&CL=hFF6347&C2=hFFBF47&C3=hA85859" }, "0xFFAA55": { "label": "LightGreen", "pos": "3x2", "cmnt": "Rivendale", "cmd": "FP=14" }, "0xFF926D": { "label": "DeepBlue", "pos": "3x3", "cmnt": "Ocean", "cmd": "FP=9" }, "0xFF12ED": { "label": "Warmwhite2", "pos": "3x4", "cmnt": "Warm White", "cmd": "FP=5&CL=hFFE4CD&C2=hFFFCCD&C3=hA89892" }, "0xFF0AF5": { "label": "Orange", "pos": "4x1", "cmnt": "Sakura", "cmd": "FP=49" }, "0xFF8A75": { "label": "Turquoise", "pos": "4x2", "cmnt": "Beech", "cmd": "FP=22" }, "0xFFB24D": { "label": "Purple", "pos": "4x3", "cmd": "FP=5&CL=h663399&C2=h993399&C3=h473864" }, "0xFF32CD": { "label": "WarmWhite", "pos": "4x4", "cmd": "FP=5&CL=hE4E4FF&C2=hF1E4FF&C3=h9C9EA8" }, "0xFF38C7": { "label": "Yellowish", "pos": "5x1", "cmnt": "Orangery", "cmd": "FP=47" }, "0xFFB847": { "label": "Cyan", "pos": "5x2", "cmnt": "Beech", "cmd": "FP=22" }, "0xFF7887": { "label": "Magenta", "pos": "5x3", "cmd": "FP=5&CL=hFF00FF&C2=hFF007F&C3=h9539A8" }, "0xFFF807": { "label": "ColdWhite", "pos": "5x4", "cmd": "FP=5&CL=hE4E4FF&C2=hF1E4FF&C3=h9C9EA8" }, "0xFF18E7": { "label": "Yellow", "pos": "6x1", "cmd": "FP=5&CL=hFFFF00&C2=hFFC800&C3=hFDFFDE" }, "0xFF9867": { "label": "Aqua", "pos": "6x2", "cmd": "FP=5&CL=hFFFF&C2=h7FFF&C3=h39A895" }, "0xFF58A7": { "label": "Pink", "pos": "6x3", "cmd": "FP=5&CL=hFFC0CB&C2=hFFD4C0&C3=hA88C96" }, "0xFFD827": { "label": "ColdWhite2", "pos": "6x4", "cmd": "FP=5&CL=hE4E4FF&C2=hF1E4FF&C3=h9C9EA8" }, "0xFF28D7": { "label": "Red +", "pos": "7x1", "cmd": "FP=5&R=~16" }, "0xFFA857": { "label": "Green +", "pos": "7x2", "cmd": "FP=5&G=~16" }, "0xFF6897": { "label": "Blue +", "pos": "7x3", "cmd": "FP=5&B=~16" }, "0xFFE817": { "label": "Quick", "pos": "7x4", "cmnt": "Fx speed +16", "cmd": "SX=~16" }, "0xFF08F7": { "label": "Red -", "pos": "8x1", "cmd": "FP=5&R=~-16" }, "0xFF8877": { "label": "Green -", "pos": "8x2", "cmd": "FP=5&G=~-16" }, "0xFF48B7": { "label": "Blue -", "pos": "8x3", "cmd": "FP=5&B=~-16" }, "0xFFC837": { "label": "Slow", "pos": "8x4", "cmnt": "FX speed -16", "cmd": "SX=~-16" }, "0xFF30CF": { "label": "Diy1", "pos": "9x1", "cmd": "CY=0&PL=1" }, "0xFFB04F": { "label": "Diy2", "pos": "9x2", "cmd": "CY=0&PL=2" }, "0xFF708F": { "label": "Diy3", "pos": "9x3", "cmd": "CY=0&PL=3" }, "0xFFF00F": { "label": "Auto", "pos": "9x4", "cmnt": "Toggle preset cycle", "cmd": "CY=2" }, "0xFF10EF": { "label": "Diy4", "pos": "10x1", "cmd": "CY=0&PL=4" }, "0xFF906F": { "label": "Diy5", "pos": "10x2", "cmd": "CY=0&PL=5" }, "0xFF50AF": { "label": "Diy6", "pos": "10x3", "cmd": "CY=0&PL=6" }, "0xFFD02F": { "label": "Flash", "pos": "10x4", "cmnt": "Cycle Effects", "cmd": "CY=0&FX=~" }, "0xFF20DF": { "label": "Jump3", "pos": "11x1", "cmnt": "Colortwinkles", "cmd": "CY=0&FX=74" }, "0xFFA05F": { "label": "Jump7", "pos": "11x2", "cmnt": "Sinelon Dual", "cmd": "CY=0&FX=93" }, "0xFF609F": { "label": "Fade3", "pos": "11x3", "cmnt": "Rain", "cmd": "CY=0&FX=43" }, "0xFFE01F": { "label": "Fade7", "pos": "11x4", "cmnt": "Lighthouse", "cmd": "CY=0&FX=41" } } ================================================ FILE: usermods/JSON_IR_remote/6-key_ir.json ================================================ { "desc": "6-key", "0xFF0FF0": { "label": "Power", "pos": "1x1", "cmd": "T=2" }, "0xFF8F70": { "label": "Channel +", "pos": "2x1", "cmnt": "Cycle palette up", "cmd": "FP=~" }, "0xFF4FB0": { "label": "Channel -", "pos": "3x1", "cmnt": "Cycle palette down", "cmd": "FP=~-" }, "0xFFCF30": { "label": "Volume +", "pos": "4x1", "cmnt": "Brighten", "cmd": "A=~16" }, "0xFF2FD0": { "label": "Volume -", "pos": "5x1", "cmnt": "Dim", "cmd": "A=~-16" }, "0xFFAF50": { "label": "Mute", "pos": "6x1", "cmnt": "Cycle effects", "cmd": "CY=0&FX=~" } } ================================================ FILE: usermods/JSON_IR_remote/9-key_ir.json ================================================ { "desc": "9-key", "0xFF629D": { "label": "Power", "cmd": "T=2" }, "0xFF22DD": { "label": "A", "cmnt": "Preset 1", "cmd": "PL=1" }, "0xFF02FD": { "label": "B", "cmnt": "Preset 2", "cmd": "PL=2" }, "0xFFC23D": { "label": "C", "cmnt": "Preset 3", "cmd": "PL=3" }, "0xFF30CF": { "label": "Left", "cmnt": "Speed -", "cmd": "SI=~-16" }, "0xFF7A85": { "label": "Right", "cmnt": "Speed +", "cmd": "SI=~16" }, "0xFF9867": { "label": "Up", "cmnt": "Bright +", "cmd": "A=~16" }, "0xFF38C7": { "label": "Down", "cmnt": "Bright -", "cmd": "A=~-16" }, "0xFF18E7": { "label": "Select", "cmnt": "Cycle effects", "cmd": "CY=0&FX=~" } } ================================================ FILE: usermods/JSON_IR_remote/ir_json_maker.py ================================================ import colorsys import json import openpyxl named_colors = {'AliceBlue': '0xF0F8FF', 'AntiqueWhite': '0xFAEBD7', 'Aqua': '0x00FFFF', 'Aquamarine': '0x7FFFD4', 'Azure': '0xF0FFFF', 'Beige': '0xF5F5DC', 'Bisque': '0xFFE4C4', 'Black': '0x000000', 'BlanchedAlmond': '0xFFEBCD', 'Blue': '0x0000FF', 'BlueViolet': '0x8A2BE2', 'Brown': '0xA52A2A', 'BurlyWood': '0xDEB887', 'CadetBlue': '0x5F9EA0', 'Chartreuse': '0x7FFF00', 'Chocolate': '0xD2691E', 'Coral': '0xFF7F50', 'CornflowerBlue': '0x6495ED', 'Cornsilk': '0xFFF8DC', 'Crimson': '0xDC143C', 'Cyan': '0x00FFFF', 'DarkBlue': '0x00008B', 'DarkCyan': '0x008B8B', 'DarkGoldenRod': '0xB8860B', 'DarkGray': '0xA9A9A9', 'DarkGrey': '0xA9A9A9', 'DarkGreen': '0x006400', 'DarkKhaki': '0xBDB76B', 'DarkMagenta': '0x8B008B', 'DarkOliveGreen': '0x556B2F', 'DarkOrange': '0xFF8C00', 'DarkOrchid': '0x9932CC', 'DarkRed': '0x8B0000', 'DarkSalmon': '0xE9967A', 'DarkSeaGreen': '0x8FBC8F', 'DarkSlateBlue': '0x483D8B', 'DarkSlateGray': '0x2F4F4F', 'DarkSlateGrey': '0x2F4F4F', 'DarkTurquoise': '0x00CED1', 'DarkViolet': '0x9400D3', 'DeepPink': '0xFF1493', 'DeepSkyBlue': '0x00BFFF', 'DimGray': '0x696969', 'DimGrey': '0x696969', 'DodgerBlue': '0x1E90FF', 'FireBrick': '0xB22222', 'FloralWhite': '0xFFFAF0', 'ForestGreen': '0x228B22', 'Fuchsia': '0xFF00FF', 'Gainsboro': '0xDCDCDC', 'GhostWhite': '0xF8F8FF', 'Gold': '0xFFD700', 'GoldenRod': '0xDAA520', 'Gray': '0x808080', 'Grey': '0x808080', 'Green': '0x008000', 'GreenYellow': '0xADFF2F', 'HoneyDew': '0xF0FFF0', 'HotPink': '0xFF69B4', 'IndianRed': '0xCD5C5C', 'Indigo': '0x4B0082', 'Ivory': '0xFFFFF0', 'Khaki': '0xF0E68C', 'Lavender': '0xE6E6FA', 'LavenderBlush': '0xFFF0F5', 'LawnGreen': '0x7CFC00', 'LemonChiffon': '0xFFFACD', 'LightBlue': '0xADD8E6', 'LightCoral': '0xF08080', 'LightCyan': '0xE0FFFF', 'LightGoldenRodYellow': '0xFAFAD2', 'LightGray': '0xD3D3D3', 'LightGrey': '0xD3D3D3', 'LightGreen': '0x90EE90', 'LightPink': '0xFFB6C1', 'LightSalmon': '0xFFA07A', 'LightSeaGreen': '0x20B2AA', 'LightSkyBlue': '0x87CEFA', 'LightSlateGray': '0x778899', 'LightSlateGrey': '0x778899', 'LightSteelBlue': '0xB0C4DE', 'LightYellow': '0xFFFFE0', 'Lime': '0x00FF00', 'LimeGreen': '0x32CD32', 'Linen': '0xFAF0E6', 'Magenta': '0xFF00FF', 'Maroon': '0x800000', 'MediumAquaMarine': '0x66CDAA', 'MediumBlue': '0x0000CD', 'MediumOrchid': '0xBA55D3', 'MediumPurple': '0x9370DB', 'MediumSeaGreen': '0x3CB371', 'MediumSlateBlue': '0x7B68EE', 'MediumSpringGreen': '0x00FA9A', 'MediumTurquoise': '0x48D1CC', 'MediumVioletRed': '0xC71585', 'MidnightBlue': '0x191970', 'MintCream': '0xF5FFFA', 'MistyRose': '0xFFE4E1', 'Moccasin': '0xFFE4B5', 'NavajoWhite': '0xFFDEAD', 'Navy': '0x000080', 'OldLace': '0xFDF5E6', 'Olive': '0x808000', 'OliveDrab': '0x6B8E23', 'Orange': '0xFFA500', 'OrangeRed': '0xFF4500', 'Orchid': '0xDA70D6', 'PaleGoldenRod': '0xEEE8AA', 'PaleGreen': '0x98FB98', 'PaleTurquoise': '0xAFEEEE', 'PaleVioletRed': '0xDB7093', 'PapayaWhip': '0xFFEFD5', 'PeachPuff': '0xFFDAB9', 'Peru': '0xCD853F', 'Pink': '0xFFC0CB', 'Plum': '0xDDA0DD', 'PowderBlue': '0xB0E0E6', 'Purple': '0x800080', 'RebeccaPurple': '0x663399', 'Red': '0xFF0000', 'RosyBrown': '0xBC8F8F', 'RoyalBlue': '0x4169E1', 'SaddleBrown': '0x8B4513', 'Salmon': '0xFA8072', 'SandyBrown': '0xF4A460', 'SeaGreen': '0x2E8B57', 'SeaShell': '0xFFF5EE', 'Sienna': '0xA0522D', 'Silver': '0xC0C0C0', 'SkyBlue': '0x87CEEB', 'SlateBlue': '0x6A5ACD', 'SlateGray': '0x708090', 'SlateGrey': '0x708090', 'Snow': '0xFFFAFA', 'SpringGreen': '0x00FF7F', 'SteelBlue': '0x4682B4', 'Tan': '0xD2B48C', 'Teal': '0x008080', 'Thistle': '0xD8BFD8', 'Tomato': '0xFF6347', 'Turquoise': '0x40E0D0', 'Violet': '0xEE82EE', 'Wheat': '0xF5DEB3', 'White': '0xFFFFFF', 'WhiteSmoke': '0xF5F5F5', 'Yellow': '0xFFFF00', 'YellowGreen': '0x9ACD32'} def shift_color(col, shift=30, sat=1.0, val=1.0): r = (col & (255 << 16)) >> 16 g = (col & (255 << 8)) >> 8 b = col & 255 hsv = colorsys.rgb_to_hsv(r, g, b) h = (((hsv[0] * 360) + shift) % 360) / 360 rgb = colorsys.hsv_to_rgb(h, hsv[1] * sat, hsv[2] * val) return (int(rgb[0]) << 16) + (int(rgb[1]) << 8) + int(rgb[2]) def parse_sheet(ws): print(f'Parsing worksheet {ws.title}') ir = {"desc": ws.title} rows = ws.rows keys = [col.value.lower() for col in next(rows)] for row in rows: rec = dict(zip(keys, [col.value for col in row])) if rec.get('code') is None: continue cd = {"label": rec.get('label')} if rec.get('row'): cd['pos'] = f'{rec["row"]}x{rec["col"]}' if rec.get('comment'): cd['cmnt'] = rec.get('comment') if rec.get('rpt'): cd['rpt'] = bool(rec['rpt']) if rec.get('cmd'): cd['cmd'] = rec['cmd'] elif all((rec.get('primary'), rec.get('secondary'), rec.get('tertiary'))): c1 = int(rec.get('primary'), 16) c2 = int(rec.get('secondary'), 16) c3 = int(rec.get('tertiary'), 16) cd['cmd'] = f'FP=5&CL=h{c1:X}&C2=h{c2:X}&C3=h{c3:X}' elif all((rec.get('primary'), rec.get('secondary'))): c1 = int(rec.get('primary'), 16) c2 = int(rec.get('secondary'), 16) c3 = shift_color(c1, -1, sat=0.66, val=0.66) cd['cmd'] = f'FP=5&CL=h{c1:X}&C2=h{c2:X}&C3=h{c3:X}' elif rec.get('primary'): c1 = int(rec.get('primary'), 16) c2 = shift_color(c1, 30) c3 = shift_color(c1, -10, sat=0.66, val=0.66) cd['cmd'] = f'FP=5&CL=h{c1:X}&C2=h{c2:X}&C3=h{c3:X}' elif rec.get('label') in named_colors: c1 = int(named_colors[rec.get('label')], 16) c2 = shift_color(c1, 30) c3 = shift_color(c1, -10, sat=0.66, val=0.66) cd['cmd'] = f'FP=5&CL=h{c1:X}&C2=h{c2:X}&C3=h{c3:X}' else: print(f'Did not find a command or color for {rec["label"]}. Hint use named CSS colors as labels') ir[rec['code']] = cd with open(f'{ws.title}_ir.json', 'w') as fp: json.dump(ir, fp, indent=2) if __name__ == '__main__': wb = openpyxl.load_workbook('IR_Remote_Codes.xlsx') for ws in wb.worksheets: parse_sheet(ws) ================================================ FILE: usermods/JSON_IR_remote/readme.md ================================================ # JSON IR remote ## Purpose The JSON IR remote enables users to customize IR remote behavior without writing custom code and compiling. It also allows using any remote compatible with your IR receiver. Using the JSON IR remote, you can map buttons from any remote to any HTTP request API or JSON API command. ## Usage * Upload the IR config file, named _ir.json_ to your board using the [ip address]/edit url. Pick from one of the included files or create your own. * On the config > LED settings page, set the correct IR pin. * On the config > Sync Interfaces page, select "JSON Remote" as the Infrared remote. ## Modification * See if there is a json file with the same number of buttons as your remote. Many remotes will have the same internals and emit the same codes but have different labels. * In the ir.json file, each key will be the hex encoded IR code. * The "cmd" property will be the HTTP Request API or JSON API to execute when that button is pressed. * A limited number of c functions are supported (!incBrightness, !decBrightness, !presetFallback) * When using !presetFallback, include properties PL (preset to load), FX (effect to fall back to) and FP (palette to fall back to) * If the command is _repeatable_ and does not contain the "~" character, add a "rpt": true property. * Other properties are ignored, but having a label property may help when editing. Sample: { "0xFF629D": {"cmd": "T=2", "rpt": true, "label": "Toggle on/off"}, // HTTP command "0xFF9867": {"cmd": "A=~16", "label": "Inc brightness"}, // HTTP command with incrementing "0xFF38C7": {"cmd": {"bri": 10}, "label": "Dim to 10"}, // JSON command "0xFF22DD": {"cmd": "!presetFallback", "PL": 1, "FX": 16, "FP": 6, "label": "Preset 1 or fallback to Saw - Party"}, // c function } ================================================ FILE: usermods/LD2410_v2/LD2410_v2.cpp ================================================ #include "wled.h" #include #ifdef WLED_DISABLE_MQTT #error "This user mod requires MQTT to be enabled." #endif class LD2410Usermod : public Usermod { private: bool enabled = true; bool initDone = false; bool sensorFound = false; unsigned long lastTime = 0; unsigned long last_mqtt_sent = 0; int8_t default_uart_rx = 19; int8_t default_uart_tx = 18; String mqttMovementTopic = F(""); String mqttStationaryTopic = F(""); bool mqttInitialized = false; bool HomeAssistantDiscovery = true; // Publish Home Assistant Discovery messages ld2410 radar; bool stationary_detected = false; bool last_stationary_state = false; bool movement_detected = false; bool last_movement_state = false; // These config variables have defaults set inside readFromConfig() int8_t uart_rx_pin; int8_t uart_tx_pin; // string that are used multiple time (this will save some flash memory) static const char _name[]; static const char _enabled[]; void publishMqtt(const char* topic, const char* state, bool retain); // example for publishing MQTT message void _mqttInitialize() { mqttMovementTopic = String(mqttDeviceTopic) + F("/ld2410/movement"); mqttStationaryTopic = String(mqttDeviceTopic) + F("/ld2410/stationary"); if (HomeAssistantDiscovery){ _createMqttSensor(F("Movement"), mqttMovementTopic, F("motion"), F("")); _createMqttSensor(F("Stationary"), mqttStationaryTopic, F("occupancy"), F("")); } } // Create an MQTT Sensor for Home Assistant Discovery purposes, this includes a pointer to the topic that is published to in the Loop. void _createMqttSensor(const String &name, const String &topic, const String &deviceClass, const String &unitOfMeasurement) { String t = String(F("homeassistant/binary_sensor/")) + mqttClientID + F("/") + name + F("/config"); StaticJsonDocument<600> doc; doc[F("name")] = String(serverDescription) + F(" Module"); doc[F("state_topic")] = topic; doc[F("unique_id")] = String(mqttClientID) + name; if (unitOfMeasurement != "") doc[F("unit_of_measurement")] = unitOfMeasurement; if (deviceClass != "") doc[F("device_class")] = deviceClass; doc[F("expire_after")] = 1800; doc[F("payload_off")] = "OFF"; doc[F("payload_on")] = "ON"; JsonObject device = doc.createNestedObject(F("device")); // attach the sensor to the same device device[F("name")] = serverDescription; device[F("identifiers")] = "wled-sensor-" + String(mqttClientID); device[F("manufacturer")] = F("WLED"); device[F("model")] = F("FOSS"); device[F("sw_version")] = versionString; String temp; serializeJson(doc, temp); DEBUG_PRINTLN(t); DEBUG_PRINTLN(temp); mqtt->publish(t.c_str(), 0, true, temp.c_str()); } public: inline bool isEnabled() { return enabled; } void setup() { Serial1.begin(256000, SERIAL_8N1, uart_rx_pin, uart_tx_pin); Serial.print(F("\nLD2410 radar sensor initialising: ")); if(radar.begin(Serial1)){ Serial.println(F("OK")); } else { Serial.println(F("not connected")); } initDone = true; } void loop() { // NOTE: on very long strips strip.isUpdating() may always return true so update accordingly if (!enabled || strip.isUpdating()) return; radar.read(); unsigned long curr_time = millis(); if(curr_time - lastTime > 1000) //Try to Report every 1000ms { lastTime = curr_time; sensorFound = radar.isConnected(); if(!sensorFound) return; stationary_detected = radar.presenceDetected(); if(stationary_detected != last_stationary_state){ if (WLED_MQTT_CONNECTED){ publishMqtt("/ld2410/stationary", stationary_detected ? "ON":"OFF", false); last_stationary_state = stationary_detected; } } movement_detected = radar.movingTargetDetected(); if(movement_detected != last_movement_state){ if (WLED_MQTT_CONNECTED){ publishMqtt("/ld2410/movement", movement_detected ? "ON":"OFF", false); last_movement_state = movement_detected; } } // If there hasn't been any activity, send current state to confirm sensor is alive if(curr_time - last_mqtt_sent > 1000*60*5 && WLED_MQTT_CONNECTED){ publishMqtt("/ld2410/stationary", stationary_detected ? "ON":"OFF", false); publishMqtt("/ld2410/movement", movement_detected ? "ON":"OFF", false); } } } void addToJsonInfo(JsonObject& root) { // if "u" object does not exist yet wee need to create it JsonObject user = root[F("u")]; if (user.isNull()) user = root.createNestedObject(F("u")); JsonArray ld2410_sta_json = user.createNestedArray(F("LD2410 Stationary")); JsonArray ld2410_mov_json = user.createNestedArray(F("LD2410 Movement")); if (!enabled){ ld2410_sta_json.add(F("disabled")); ld2410_mov_json.add(F("disabled")); } else if(!sensorFound){ ld2410_sta_json.add(F("LD2410")); ld2410_sta_json.add(" Not Found"); } else { ld2410_sta_json.add("Sta "); ld2410_sta_json.add(stationary_detected ? "ON":"OFF"); ld2410_mov_json.add("Mov "); ld2410_mov_json.add(movement_detected ? "ON":"OFF"); } } void addToConfig(JsonObject& root) { JsonObject top = root.createNestedObject(FPSTR(_name)); top[FPSTR(_enabled)] = enabled; //save these vars persistently whenever settings are saved top["uart_rx_pin"] = default_uart_rx; top["uart_tx_pin"] = default_uart_tx; } bool readFromConfig(JsonObject& root) { // default settings values could be set here (or below using the 3-argument getJsonValue()) instead of in the class definition or constructor // setting them inside readFromConfig() is slightly more robust, handling the rare but plausible use case of single value being missing after boot (e.g. if the cfg.json was manually edited and a value was removed) JsonObject top = root[FPSTR(_name)]; bool configComplete = !top.isNull(); if (!configComplete) { DEBUG_PRINT(FPSTR(_name)); DEBUG_PRINT(F("LD2410")); DEBUG_PRINTLN(F(": No config found. (Using defaults.)")); return false; } configComplete &= getJsonValue(top["uart_rx_pin"], uart_rx_pin, default_uart_rx); configComplete &= getJsonValue(top["uart_tx_pin"], uart_tx_pin, default_uart_tx); return configComplete; } #ifndef WLED_DISABLE_MQTT /** * onMqttConnect() is called when MQTT connection is established */ void onMqttConnect(bool sessionPresent) { // do any MQTT related initialisation here if(!radar.isConnected()) return; publishMqtt("/ld2410/status", "I am alive!", false); if (!mqttInitialized) { _mqttInitialize(); mqttInitialized = true; } } #endif uint16_t getId() { return USERMOD_ID_LD2410; } }; // add more strings here to reduce flash memory usage const char LD2410Usermod::_name[] PROGMEM = "LD2410Usermod"; const char LD2410Usermod::_enabled[] PROGMEM = "enabled"; // implementation of non-inline member methods void LD2410Usermod::publishMqtt(const char* topic, const char* state, bool retain) { #ifndef WLED_DISABLE_MQTT //Check if MQTT Connected, otherwise it will crash if (WLED_MQTT_CONNECTED) { last_mqtt_sent = millis(); char subuf[64]; strcpy(subuf, mqttDeviceTopic); strcat(subuf, topic); mqtt->publish(subuf, 0, retain, state); } #endif } static LD2410Usermod ld2410_v2; REGISTER_USERMOD(ld2410_v2); ================================================ FILE: usermods/LD2410_v2/library.json ================================================ { "name": "LD2410_v2", "build": { "libArchive": false }, "dependencies": { "ncmreynolds/ld2410":"^0.1.3" } } ================================================ FILE: usermods/LD2410_v2/readme.md ================================================ # BH1750 usermod > This usermod requires a second UART and was only tested on the ESP32 This usermod will read from a LD2410 movement/presence sensor. The movement and presence state are displayed in both the Info section of the web UI, as well as published to the `/movement` and `/stationary` MQTT topics respectively. ## Dependencies - Libraries - `ncmreynolds/ld2410@^0.1.3` - Data is published over MQTT - make sure you've enabled the MQTT sync interface. ## Compilation To enable, compile with `LD2140` in `custom_usermods` (e.g. in `platformio_override.ini`) ```ini [env:usermod_USERMOD_LD2410_esp32dev] extends = env:esp32dev custom_usermods = ${env:esp32dev.custom_usermods} LD2140_v2 ``` ### Configuration Options The Usermod screen allows you to: - enable/disable the usermod - Configure the RX/TX pins ## Change log - 2024-06 Created by @wesleygas (https://github.com/wesleygas/) ================================================ FILE: usermods/LDR_Dusk_Dawn_v2/LDR_Dusk_Dawn_v2.cpp ================================================ #include "wled.h" #ifndef ARDUINO_ARCH_ESP32 // 8266 does not support analogRead on user selectable pins #error only ESP32 is supported by usermod LDR_DUSK_DAWN #endif class LDR_Dusk_Dawn_v2 : public Usermod { private: // Defaults bool ldrEnabled = false; int ldrPin = 34; //A2 on Adafruit Huzzah32 int ldrThresholdMinutes = 5; // How many minutes of readings above/below threshold until it switches LED state int ldrThreshold = 1000; // Readings higher than this number will turn off LED. int ldrOnPreset = 1; // Default "On" Preset int ldrOffPreset = 2; // Default "Off" Preset // Variables bool initDone = false; bool ldrEnabledPreviously = false; // Was LDR enabled for the previous check? First check is always no. int ldrOffCount; // Number of readings above the threshold int ldrOnCount; // Number of readings below the threshold int ldrReading = 0; // Last LDR reading int ldrLEDState; // Current LED on/off state unsigned long lastMillis = 0; static const char _name[]; public: void setup() { // register ldrPin if ((ldrPin >= 0) && (digitalPinToAnalogChannel(ldrPin) >= 0)) { if(!PinManager::allocatePin(ldrPin, false, PinOwner::UM_LDR_DUSK_DAWN)) ldrEnabled = false; // pin already in use -> disable usermod else pinMode(ldrPin, INPUT); // alloc success -> configure pin for input } else ldrEnabled = false; // invalid pin -> disable usermod initDone = true; } void loop() { // Only update every 10 seconds if (millis() - lastMillis > 10000) { if ( (ldrEnabled == true) && (ldrPin >= 0) && (digitalPinToAnalogChannel(ldrPin) >= 0) ) { // make sure that pin is valid for analogread() // Default state is off if (ldrEnabledPreviously == false) { applyPreset(ldrOffPreset); ldrEnabledPreviously = true; ldrLEDState = 0; } // Get LDR reading and increment counter by number of seconds since last read ldrReading = analogRead(ldrPin); if (ldrReading <= ldrThreshold) { ldrOnCount = ldrOnCount + 10; ldrOffCount = 0; } else { ldrOffCount = ldrOffCount + 10; ldrOnCount = 0; } if (ldrOnCount >= (ldrThresholdMinutes * 60)) { ldrOnCount = 0; // If LEDs were previously off, turn on if (ldrLEDState == 0) { applyPreset(ldrOnPreset); ldrLEDState = 1; } } if (ldrOffCount >= (ldrThresholdMinutes * 60)) { ldrOffCount = 0; // If LEDs were previously on, turn off if (ldrLEDState == 1) { applyPreset(ldrOffPreset); ldrLEDState = 0; } } } else { // LDR is disabled, reset variables to default ldrReading = 0; ldrOnCount = 0; ldrOffCount = 0; ldrLEDState = 0; ldrEnabledPreviously = false; } lastMillis = millis(); } } void addToConfig(JsonObject& root) { JsonObject top = root.createNestedObject(FPSTR(_name)); top["Enabled"] = ldrEnabled; top["LDR Pin"] = ldrPin; top["Threshold Minutes"] = ldrThresholdMinutes; top["Threshold"] = ldrThreshold; top["On Preset"] = ldrOnPreset; top["Off Preset"] = ldrOffPreset; } bool readFromConfig(JsonObject& root) { int8_t oldLdrPin = ldrPin; JsonObject top = root[FPSTR(_name)]; bool configComplete = !top.isNull(); configComplete &= getJsonValue(top["Enabled"], ldrEnabled); configComplete &= getJsonValue(top["LDR Pin"], ldrPin); configComplete &= getJsonValue(top["Threshold Minutes"], ldrThresholdMinutes); configComplete &= getJsonValue(top["Threshold"], ldrThreshold); configComplete &= getJsonValue(top["On Preset"], ldrOnPreset); configComplete &= getJsonValue(top["Off Preset"], ldrOffPreset); if (initDone && (ldrPin != oldLdrPin)) { // pin changed - un-register previous pin, register new pin if (oldLdrPin >= 0) PinManager::deallocatePin(oldLdrPin, PinOwner::UM_LDR_DUSK_DAWN); setup(); // setup new pin } return configComplete; } void addToJsonInfo(JsonObject& root) { // If "u" object does not exist yet we need to create it JsonObject user = root["u"]; if (user.isNull()) user = root.createNestedObject("u"); JsonArray LDR_Enabled = user.createNestedArray("LDR dusk/dawn enabled"); LDR_Enabled.add(ldrEnabled); if (!ldrEnabled) return; // do not add more if usermod is disabled JsonArray LDR_Reading = user.createNestedArray("LDR reading"); LDR_Reading.add(ldrReading); JsonArray LDR_State = user.createNestedArray("LDR turned LEDs on"); LDR_State.add(bool(ldrLEDState)); // Optional debug information: //JsonArray LDR_On_Count = user.createNestedArray("LDR on count"); //LDR_On_Count.add(ldrOnCount); //JsonArray LDR_Off_Count = user.createNestedArray("LDR off count"); //LDR_Off_Count.add(ldrOffCount); //bool pinValid = ((ldrPin >= 0) && (digitalPinToAnalogChannel(ldrPin) >= 0)); //if (PinManager::getPinOwner(ldrPin) != PinOwner::UM_LDR_DUSK_DAWN) pinValid = false; //JsonArray LDR_valid = user.createNestedArray(F("LDR pin")); //LDR_valid.add(ldrPin); //LDR_valid.add(pinValid ? F(" OK"): F(" invalid")); } uint16_t getId() { return USERMOD_ID_LDR_DUSK_DAWN; } }; const char LDR_Dusk_Dawn_v2::_name[] PROGMEM = "LDR_Dusk_Dawn_v2"; static LDR_Dusk_Dawn_v2 ldr_dusk_dawn_v2; REGISTER_USERMOD(ldr_dusk_dawn_v2); ================================================ FILE: usermods/LDR_Dusk_Dawn_v2/README.md ================================================ # LDR_Dusk_Dawn_v2 This usermod will obtain readings from a Light Dependent Resistor (LDR) and will turn on/off specific presets based on those readings. This is useful for exterior lighting situations where you want the lights to only be on when it is dark out. # Installation Add "LDR_Dusk_Dawn" to your platformio.ini environment's custom_usermods and build. Example: ``` [env:usermod_LDR_Dusk_Dawn_esp32dev] extends = env:esp32dev custom_usermods = ${env:esp32dev.custom_usermods} LDR_Dusk_Dawn # Enable LDR Dusk Dawn Usermod ``` # Usermod Settings Setting | Description | Default --- | --- | --- Enabled | Enable/Disable the LDR functionality. | Disabled LDR Pin | The analog capable pin your LDR is connected to. | 34 Threshold Minutes | The number of minutes of consistent readings above/below the on/off threshold before the LED state will change. | 5 Threshold | The analog read value threshold from the LDR. Readings lower than this number will count towards changing the LED state to off. You can see the current LDR reading by going into the info section when LDR functionality is enabled. | 1000 On Preset | The WLED preset to be used for the LED on state. | 1 Off Preset | The WLED preset to be used for the LED off state. | 2 ## Author [@jeffwdh](https://github.com/jeffwdh) jeffwdh@tarball.ca ================================================ FILE: usermods/LDR_Dusk_Dawn_v2/library.json ================================================ { "name": "LDR_Dusk_Dawn_v2", "build": { "libArchive": false } } ================================================ FILE: usermods/MAX17048_v2/MAX17048_v2.cpp ================================================ // force the compiler to show a warning to confirm that this file is included #warning **** Included USERMOD_MAX17048 V2.0 **** #include "wled.h" #include "Adafruit_MAX1704X.h" // the max interval to check battery level, 10 seconds #ifndef USERMOD_MAX17048_MAX_MONITOR_INTERVAL #define USERMOD_MAX17048_MAX_MONITOR_INTERVAL 10000 #endif // the min interval to check battery level, 500 ms #ifndef USERMOD_MAX17048_MIN_MONITOR_INTERVAL #define USERMOD_MAX17048_MIN_MONITOR_INTERVAL 500 #endif // how many seconds after boot to perform the first check, 10 seconds #ifndef USERMOD_MAX17048_FIRST_MONITOR_AT #define USERMOD_MAX17048_FIRST_MONITOR_AT 10000 #endif /* * Usermod to display Battery Life using Adafruit's MAX17048 LiPoly/ LiIon Fuel Gauge and Battery Monitor. */ class Usermod_MAX17048 : public Usermod { private: bool enabled = true; unsigned long maxReadingInterval = USERMOD_MAX17048_MAX_MONITOR_INTERVAL; unsigned long minReadingInterval = USERMOD_MAX17048_MIN_MONITOR_INTERVAL; unsigned long lastCheck = UINT32_MAX - (USERMOD_MAX17048_MAX_MONITOR_INTERVAL - USERMOD_MAX17048_FIRST_MONITOR_AT); unsigned long lastSend = UINT32_MAX - (USERMOD_MAX17048_MAX_MONITOR_INTERVAL - USERMOD_MAX17048_FIRST_MONITOR_AT); unsigned VoltageDecimals = 3; // Number of decimal places in published voltage values unsigned PercentDecimals = 1; // Number of decimal places in published percent values // string that are used multiple time (this will save some flash memory) static const char _name[]; static const char _enabled[]; static const char _maxReadInterval[]; static const char _minReadInterval[]; static const char _HomeAssistantDiscovery[]; bool monitorFound = false; bool firstReadComplete = false; bool initDone = false; Adafruit_MAX17048 maxLipo; float lastBattVoltage = -10; float lastBattPercent = -1; // MQTT and Home Assistant Variables bool HomeAssistantDiscovery = false; // Publish Home Assistant Device Information bool mqttInitialized = false; void _mqttInitialize() { char mqttBatteryVoltageTopic[128]; char mqttBatteryPercentTopic[128]; snprintf_P(mqttBatteryVoltageTopic, 127, PSTR("%s/batteryVoltage"), mqttDeviceTopic); snprintf_P(mqttBatteryPercentTopic, 127, PSTR("%s/batteryPercent"), mqttDeviceTopic); if (HomeAssistantDiscovery) { _createMqttSensor(F("BatteryVoltage"), mqttBatteryVoltageTopic, "voltage", F("V")); _createMqttSensor(F("BatteryPercent"), mqttBatteryPercentTopic, "battery", F("%")); } } void _createMqttSensor(const String &name, const String &topic, const String &deviceClass, const String &unitOfMeasurement) { String t = String(F("homeassistant/sensor/")) + mqttClientID + F("/") + name + F("/config"); StaticJsonDocument<600> doc; doc[F("name")] = String(serverDescription) + " " + name; doc[F("state_topic")] = topic; doc[F("unique_id")] = String(mqttClientID) + name; if (unitOfMeasurement != "") doc[F("unit_of_measurement")] = unitOfMeasurement; if (deviceClass != "") doc[F("device_class")] = deviceClass; doc[F("expire_after")] = 1800; JsonObject device = doc.createNestedObject(F("device")); // attach the sensor to the same device device[F("name")] = serverDescription; device[F("identifiers")] = "wled-sensor-" + String(mqttClientID); device[F("manufacturer")] = F("WLED"); device[F("model")] = F("FOSS"); device[F("sw_version")] = versionString; String temp; serializeJson(doc, temp); DEBUG_PRINTLN(t); DEBUG_PRINTLN(temp); mqtt->publish(t.c_str(), 0, true, temp.c_str()); } void publishMqtt(const char *topic, const char* state) { #ifndef WLED_DISABLE_MQTT //Check if MQTT Connected, otherwise it will crash the 8266 if (WLED_MQTT_CONNECTED){ char subuf[128]; snprintf_P(subuf, 127, PSTR("%s/%s"), mqttDeviceTopic, topic); mqtt->publish(subuf, 0, false, state); } #endif } public: inline void enable(bool enable) { enabled = enable; } inline bool isEnabled() { return enabled; } void setup() { // do your set-up here if (i2c_scl<0 || i2c_sda<0) { enabled = false; return; } monitorFound = maxLipo.begin(); initDone = true; } void loop() { // if usermod is disabled or called during strip updating just exit // NOTE: on very long strips strip.isUpdating() may always return true so update accordingly if (!enabled || strip.isUpdating()) return; unsigned long now = millis(); if (now - lastCheck < minReadingInterval) { return; } bool shouldUpdate = now - lastSend > maxReadingInterval; float battVoltage = maxLipo.cellVoltage(); float battPercent = maxLipo.cellPercent(); lastCheck = millis(); firstReadComplete = true; if (shouldUpdate) { lastBattVoltage = roundf(battVoltage * powf(10, VoltageDecimals)) / powf(10, VoltageDecimals); lastBattPercent = roundf(battPercent * powf(10, PercentDecimals)) / powf(10, PercentDecimals); lastSend = millis(); publishMqtt("batteryVoltage", String(lastBattVoltage, VoltageDecimals).c_str()); publishMqtt("batteryPercent", String(lastBattPercent, PercentDecimals).c_str()); DEBUG_PRINTLN(F("Battery Voltage: ") + String(lastBattVoltage, VoltageDecimals) + F("V")); DEBUG_PRINTLN(F("Battery Percent: ") + String(lastBattPercent, PercentDecimals) + F("%")); } } void onMqttConnect(bool sessionPresent) { if (WLED_MQTT_CONNECTED && !mqttInitialized) { _mqttInitialize(); mqttInitialized = true; } } inline float getBatteryVoltageV() { return (float) lastBattVoltage; } inline float getBatteryPercent() { return (float) lastBattPercent; } void addToJsonInfo(JsonObject& root) { // if "u" object does not exist yet wee need to create it JsonObject user = root["u"]; if (user.isNull()) user = root.createNestedObject("u"); JsonArray battery_json = user.createNestedArray(F("Battery Monitor")); if (!enabled) { battery_json.add(F("Disabled")); } else if(!monitorFound) { battery_json.add(F("MAX17048 Not Found")); } else if (!firstReadComplete) { // if we haven't read the sensor yet, let the user know // that we are still waiting for the first measurement battery_json.add((USERMOD_MAX17048_FIRST_MONITOR_AT - millis()) / 1000); battery_json.add(F(" sec until read")); } else { battery_json.add(F("Enabled")); JsonArray voltage_json = user.createNestedArray(F("Battery Voltage")); voltage_json.add(lastBattVoltage); voltage_json.add(F("V")); JsonArray percent_json = user.createNestedArray(F("Battery Percent")); percent_json.add(lastBattPercent); percent_json.add(F("%")); } } void addToJsonState(JsonObject& root) { JsonObject usermod = root[FPSTR(_name)]; if (usermod.isNull()) { usermod = root.createNestedObject(FPSTR(_name)); } usermod[FPSTR(_enabled)] = enabled; } void readFromJsonState(JsonObject& root) { JsonObject usermod = root[FPSTR(_name)]; if (!usermod.isNull()) { if (usermod[FPSTR(_enabled)].is()) { enabled = usermod[FPSTR(_enabled)].as(); } } } void addToConfig(JsonObject& root) { JsonObject top = root.createNestedObject(FPSTR(_name)); top[FPSTR(_enabled)] = enabled; top[FPSTR(_maxReadInterval)] = maxReadingInterval; top[FPSTR(_minReadInterval)] = minReadingInterval; top[FPSTR(_HomeAssistantDiscovery)] = HomeAssistantDiscovery; DEBUG_PRINT(F(_name)); DEBUG_PRINTLN(F(" config saved.")); } bool readFromConfig(JsonObject& root) { JsonObject top = root[FPSTR(_name)]; if (top.isNull()) { DEBUG_PRINT(F(_name)); DEBUG_PRINTLN(F(": No config found. (Using defaults.)")); return false; } bool configComplete = !top.isNull(); configComplete &= getJsonValue(top[FPSTR(_enabled)], enabled); configComplete &= getJsonValue(top[FPSTR(_maxReadInterval)], maxReadingInterval, USERMOD_MAX17048_MAX_MONITOR_INTERVAL); configComplete &= getJsonValue(top[FPSTR(_minReadInterval)], minReadingInterval, USERMOD_MAX17048_MIN_MONITOR_INTERVAL); configComplete &= getJsonValue(top[FPSTR(_HomeAssistantDiscovery)], HomeAssistantDiscovery, false); DEBUG_PRINT(FPSTR(_name)); if (!initDone) { // first run: reading from cfg.json DEBUG_PRINTLN(F(" config loaded.")); } else { DEBUG_PRINTLN(F(" config (re)loaded.")); // changing parameters from settings page } return configComplete; } uint16_t getId() { return USERMOD_ID_MAX17048; } }; // add more strings here to reduce flash memory usage const char Usermod_MAX17048::_name[] PROGMEM = "Adafruit MAX17048 Battery Monitor"; const char Usermod_MAX17048::_enabled[] PROGMEM = "enabled"; const char Usermod_MAX17048::_maxReadInterval[] PROGMEM = "max-read-interval-ms"; const char Usermod_MAX17048::_minReadInterval[] PROGMEM = "min-read-interval-ms"; const char Usermod_MAX17048::_HomeAssistantDiscovery[] PROGMEM = "HomeAssistantDiscovery"; static Usermod_MAX17048 max17048_v2; REGISTER_USERMOD(max17048_v2); ================================================ FILE: usermods/MAX17048_v2/library.json ================================================ { "name": "MAX17048_v2", "build": { "libArchive": false}, "dependencies": { "Adafruit_MAX1704X":"https://github.com/adafruit/Adafruit_MAX1704X#1.0.2" } } ================================================ FILE: usermods/MAX17048_v2/readme.md ================================================ # Adafruit MAX17048 Usermod (LiPo & LiIon Battery Monitor & Fuel Gauge) This usermod reads information from an Adafruit MAX17048 and outputs the following: - Battery Voltage - Battery Level Percentage ## Dependencies Data is published over MQTT - make sure you've enabled the MQTT sync interface. ## Compilation Add "MAX17048_v2" to your platformio.ini environment's custom_usermods and build. To enable, compile with `USERMOD_MAX17048` define in the build_flags (e.g. in `platformio.ini` or `platformio_override.ini`) such as in the example below: ```ini [env:usermod_max17048_d1_mini] extends = env:d1_mini custom_usermods = ${env:d1_mini.custom_usermods} MAX17048_v2 ``` ### Configuration Options The following settings can be set at compile-time but are configurable on the usermod menu (except First Monitor time): - USERMOD_MAX17048_MIN_MONITOR_INTERVAL (the min number of milliseconds between checks, defaults to 10,000 ms) - USERMOD_MAX17048_MAX_MONITOR_INTERVAL (the max number of milliseconds between checks, defaults to 10,000 ms) - USERMOD_MAX17048_FIRST_MONITOR_AT Additionally, the Usermod Menu allows you to: - Enable or Disable the usermod - Enable or Disable Home Assistant Discovery (turn on/off to sent MQTT Discovery entries for Home Assistant) - Configure SCL/SDA GPIO Pins ## API The following method is available to interact with the usermod from other code modules: - `getBatteryVoltageV` read the last battery voltage (in Volt) obtained from the sensor - `getBatteryPercent` reads the last battery percentage obtained from the sensor ## MQTT MQTT topics are as follows (`` is set in MQTT section of Sync Setup menu): Measurement type | MQTT topic --- | --- Battery Voltage | `/batteryVoltage` Battery Percent | `/batteryPercent` ## Authors Carlos Cruz [@ccruz09](https://github.com/ccruz09) ## Revision History Jan 2024 - Added Home Assistant Discovery - Implemented PinManager to register pins - Added API call for other modules to read battery voltage and percentage - Added info-screen outputs - Updated `readme.md` ================================================ FILE: usermods/MY9291/MY9291.cpp ================================================ #include "wled.h" #include "MY92xx.h" #define MY92XX_MODEL MY92XX_MODEL_MY9291 #define MY92XX_CHIPS 1 #define MY92XX_DI_PIN 13 #define MY92XX_DCKI_PIN 15 #define MY92XX_RED 0 #define MY92XX_GREEN 1 #define MY92XX_BLUE 2 #define MY92XX_WHITE 3 class MY9291Usermod : public Usermod { private: my92xx _my92xx = my92xx(MY92XX_MODEL, MY92XX_CHIPS, MY92XX_DI_PIN, MY92XX_DCKI_PIN, MY92XX_COMMAND_DEFAULT); public: void setup() { _my92xx.setState(true); } void connected() { } void loop() { uint32_t c = strip.getPixelColor(0); int w = ((c >> 24) & 0xff) * bri / 255.0; int r = ((c >> 16) & 0xff) * bri / 255.0; int g = ((c >> 8) & 0xff) * bri / 255.0; int b = (c & 0xff) * bri / 255.0; _my92xx.setChannel(MY92XX_RED, r); _my92xx.setChannel(MY92XX_GREEN, g); _my92xx.setChannel(MY92XX_BLUE, b); _my92xx.setChannel(MY92XX_WHITE, w); _my92xx.update(); } uint16_t getId() { return USERMOD_ID_MY9291; } }; static MY9291Usermod my9291; REGISTER_USERMOD(my9291); ================================================ FILE: usermods/MY9291/MY92xx.h ================================================ /* MY92XX LED Driver for Arduino Based on the C driver by MaiKe Labs Copyright (c) 2016 - 2026 MaiKe Labs Copyright (C) 2017 - 2018 Xose Pérez for the Arduino compatible library This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #ifndef _my92xx_h #define _my92xx_h #include #ifdef DEBUG_MY92XX #if ARDUINO_ARCH_ESP8266 #define DEBUG_MSG_MY92XX(...) DEBUG_MY92XX.printf( __VA_ARGS__ ) #elif ARDUINO_ARCH_AVR #define DEBUG_MSG_MY92XX(...) { char buffer[80]; snprintf(buffer, sizeof(buffer), __VA_ARGS__ ); DEBUG_MY92XX.print(buffer); } #endif #else #define DEBUG_MSG_MY92XX(...) #endif typedef enum my92xx_model_t { MY92XX_MODEL_MY9291 = 0X00, MY92XX_MODEL_MY9231 = 0X01, } my92xx_model_t; typedef enum my92xx_cmd_one_shot_t { MY92XX_CMD_ONE_SHOT_DISABLE = 0X00, MY92XX_CMD_ONE_SHOT_ENFORCE = 0X01, } my92xx_cmd_one_shot_t; typedef enum my92xx_cmd_reaction_t { MY92XX_CMD_REACTION_FAST = 0X00, MY92XX_CMD_REACTION_SLOW = 0X01, } my92xx_cmd_reaction_t; typedef enum my92xx_cmd_bit_width_t { MY92XX_CMD_BIT_WIDTH_16 = 0X00, MY92XX_CMD_BIT_WIDTH_14 = 0X01, MY92XX_CMD_BIT_WIDTH_12 = 0X02, MY92XX_CMD_BIT_WIDTH_8 = 0X03, } my92xx_cmd_bit_width_t; typedef enum my92xx_cmd_frequency_t { MY92XX_CMD_FREQUENCY_DIVIDE_1 = 0X00, MY92XX_CMD_FREQUENCY_DIVIDE_4 = 0X01, MY92XX_CMD_FREQUENCY_DIVIDE_16 = 0X02, MY92XX_CMD_FREQUENCY_DIVIDE_64 = 0X03, } my92xx_cmd_frequency_t; typedef enum my92xx_cmd_scatter_t { MY92XX_CMD_SCATTER_APDM = 0X00, MY92XX_CMD_SCATTER_PWM = 0X01, } my92xx_cmd_scatter_t; typedef struct { my92xx_cmd_scatter_t scatter : 1; my92xx_cmd_frequency_t frequency : 2; my92xx_cmd_bit_width_t bit_width : 2; my92xx_cmd_reaction_t reaction : 1; my92xx_cmd_one_shot_t one_shot : 1; unsigned char resv : 1; } __attribute__((aligned(1), packed)) my92xx_cmd_t; #define MY92XX_COMMAND_DEFAULT { \ .scatter = MY92XX_CMD_SCATTER_APDM, \ .frequency = MY92XX_CMD_FREQUENCY_DIVIDE_1, \ .bit_width = MY92XX_CMD_BIT_WIDTH_8, \ .reaction = MY92XX_CMD_REACTION_FAST, \ .one_shot = MY92XX_CMD_ONE_SHOT_DISABLE, \ .resv = 0 \ } class my92xx { public: my92xx(my92xx_model_t model, unsigned char chips, unsigned char di, unsigned char dcki, my92xx_cmd_t command); unsigned char getChannels(); void setChannel(unsigned char channel, unsigned int value); unsigned int getChannel(unsigned char channel); void setState(bool state); bool getState(); void update(); private: void _di_pulse(unsigned int times); void _dcki_pulse(unsigned int times); void _set_cmd(my92xx_cmd_t command); void _send(); void _write(unsigned int data, unsigned char bit_length); my92xx_cmd_t _command; my92xx_model_t _model = MY92XX_MODEL_MY9291; unsigned char _chips = 1; unsigned char _channels; uint16_t* _value; bool _state = false; unsigned char _pin_di; unsigned char _pin_dcki; }; #if ARDUINO_ARCH_ESP8266 extern "C" { void os_delay_us(unsigned int); } #elif ARDUINO_ARCH_AVR #define os_delay_us delayMicroseconds #endif void my92xx::_di_pulse(unsigned int times) { for (unsigned int i = 0; i < times; i++) { digitalWrite(_pin_di, HIGH); digitalWrite(_pin_di, LOW); } } void my92xx::_dcki_pulse(unsigned int times) { for (unsigned int i = 0; i < times; i++) { digitalWrite(_pin_dcki, HIGH); digitalWrite(_pin_dcki, LOW); } } void my92xx::_write(unsigned int data, unsigned char bit_length) { unsigned int mask = (0x01 << (bit_length - 1)); for (unsigned int i = 0; i < bit_length / 2; i++) { digitalWrite(_pin_dcki, LOW); digitalWrite(_pin_di, (data & mask) ? HIGH : LOW); digitalWrite(_pin_dcki, HIGH); data = data << 1; digitalWrite(_pin_di, (data & mask) ? HIGH : LOW); digitalWrite(_pin_dcki, LOW); digitalWrite(_pin_di, LOW); data = data << 1; } } void my92xx::_set_cmd(my92xx_cmd_t command) { // ets_intr_lock(); // TStop > 12us. os_delay_us(12); // Send 12 DI pulse, after 6 pulse's falling edge store duty data, and 12 // pulse's rising edge convert to command mode. _di_pulse(12); // Delay >12us, begin send CMD data os_delay_us(12); // Send CMD data unsigned char command_data = *(unsigned char*)(&command); for (unsigned char i = 0; i < _chips; i++) { _write(command_data, 8); } // TStart > 12us. Delay 12 us. os_delay_us(12); // Send 16 DI pulse,at 14 pulse's falling edge store CMD data, and // at 16 pulse's falling edge convert to duty mode. _di_pulse(16); // TStop > 12us. os_delay_us(12); // ets_intr_unlock(); } void my92xx::_send() { #ifdef DEBUG_MY92XX DEBUG_MSG_MY92XX("[MY92XX] Refresh: %s (", _state ? "ON" : "OFF"); for (unsigned char channel = 0; channel < _channels; channel++) { DEBUG_MSG_MY92XX(" %d", _value[channel]); } DEBUG_MSG_MY92XX(" )\n"); #endif unsigned char bit_length = 8; switch (_command.bit_width) { case MY92XX_CMD_BIT_WIDTH_16: bit_length = 16; break; case MY92XX_CMD_BIT_WIDTH_14: bit_length = 14; break; case MY92XX_CMD_BIT_WIDTH_12: bit_length = 12; break; case MY92XX_CMD_BIT_WIDTH_8: bit_length = 8; break; default: bit_length = 8; break; } // ets_intr_lock(); // TStop > 12us. os_delay_us(12); // Send color data for (unsigned char channel = 0; channel < _channels; channel++) { _write(_state ? _value[channel] : 0, bit_length); } // TStart > 12us. Ready for send DI pulse. os_delay_us(12); // Send 8 DI pulse. After 8 pulse falling edge, store old data. _di_pulse(8); // TStop > 12us. os_delay_us(12); // ets_intr_unlock(); } // ----------------------------------------------------------------------------- unsigned char my92xx::getChannels() { return _channels; } void my92xx::setChannel(unsigned char channel, unsigned int value) { if (channel < _channels) { _value[channel] = value; } } unsigned int my92xx::getChannel(unsigned char channel) { if (channel < _channels) { return _value[channel]; } return 0; } bool my92xx::getState() { return _state; } void my92xx::setState(bool state) { _state = state; } void my92xx::update() { _send(); } // ----------------------------------------------------------------------------- my92xx::my92xx(my92xx_model_t model, unsigned char chips, unsigned char di, unsigned char dcki, my92xx_cmd_t command) : _command(command) { _model = model; _chips = chips; _pin_di = di; _pin_dcki = dcki; // Init channels if (_model == MY92XX_MODEL_MY9291) { _channels = 4 * _chips; } else if (_model == MY92XX_MODEL_MY9231) { _channels = 3 * _chips; } _value = new uint16_t[_channels]; for (unsigned char i = 0; i < _channels; i++) { _value[i] = 0; } // Init GPIO pinMode(_pin_di, OUTPUT); pinMode(_pin_dcki, OUTPUT); digitalWrite(_pin_di, LOW); digitalWrite(_pin_dcki, LOW); // Clear all duty register _dcki_pulse(32 * _chips); // Send init command _set_cmd(command); DEBUG_MSG_MY92XX("[MY92XX] Initialized\n"); } #endif ================================================ FILE: usermods/MY9291/library.json ================================================ { "name": "MY9291", "build": { "libArchive": false }, "platforms": ["espressif8266"] } ================================================ FILE: usermods/PIR_sensor_switch/PIR_Highlight_Standby ================================================ #pragma once #include "wled.h" /* * -------------------- * Rawframe edit: * - TESTED ON WLED VS.0.10.1 - WHERE ONLY PRESET 16 SAVES SEGMENTS - some macros may not be needed if this changes. * - Code has been modified as my usage changed, as such it has poor use of functions vs if thens, but feel free to change it for me :) * * Edited to SWITCH between two lighting scenes/modes : STANDBY and HIGHLIGHT * * Usage: * - Standby is the default mode and Highlight is activated when the PIR detects activity. * - PIR delay now set to same value as Nightlight feature on boot but otherwise controlled as normal. * - Standby and Highlight brightness can be set on the fly (default values set on boot via macros calling presets). * - Macros are used to set Standby and Highlight states (macros can load saved presets etc). * * - Macro short button press = Highlight state default (used on boot only and sets default brightness). * - Macro double button press = Standby state default (used on boot only and sets default brightness). * - Macro long button press = Highlight state (after boot). * - Macro 16 = Standby state (after boot). * * ! It is advised not to set 'Apply preset at boot' or a boot macro (that activates a preset) as we will call our own macros on boot. * * - When the strip is off before PIR activates the strip will return to off for Standby mode, and vice versa. * - When the strip is turned off while in Highlight mode, it will return to standby mode. (This behaviour could be changed easily if for some reason you wanted the lights to go out when the pir is activated). * - Macros can be chained so you could do almost anything, such as have standby mode also turn on the nightlight function with a new time delay. * * Segment Notes: * - It's easier to save the segment selections in preset than apply via macro while we a limited to preset 16. (Ie, instead of selecting sections at the point of activating standby/highlight modes). * - Because only preset 16 saves segments, for now we are having to use addiotional macros to control segments where they are involved. Macros can be chained so this works but it would be better if macros also accepted json-api commands. (Testing http api segement behaviour of SS with SB left me a little confused). * * Future: * - Maybe a second timer/timetable that turns on/off standby mode also after set inactivity period / date & times. For now this can be achieved others ways so may not be worth eating more processing power. * * -------------------- * * This usermod handles PIR sensor states. * The strip will be switched on and the off timer will be resetted when the sensor goes HIGH. * When the sensor state goes LOW, the off timer is started and when it expires, the strip is switched off. * * * Usermods allow you to add own functionality to WLED more easily * See: https://github.com/wled-dev/WLED/wiki/Add-own-functionality * * v2 usermods are class inheritance based and can (but don't have to) implement more functions, each of them is shown in this example. * Multiple v2 usermods can be added to one compilation easily. * * Creating a usermod: * This file serves as an example. If you want to create a usermod, it is recommended to use usermod_v2_empty.h from the usermods folder as a template. * Please remember to rename the class and file to a descriptive name. * You may also use multiple .h and .cpp files. * * Using a usermod: * 1. Copy the usermod into the sketch folder (same folder as wled00.ino) * 2. Register the usermod by adding #include "usermod_filename.h" in the top and registerUsermod(new MyUsermodClass()) in the bottom of usermods_list.cpp */ class PIRsensorSwitch : public Usermod { private: // PIR sensor pin const uint8_t PIRsensorPin = 13; // D7 on D1 mini // notification mode for stateUpdated() const byte NotifyUpdateMode = CALL_MODE_NO_NOTIFY; // CALL_MODE_DIRECT_CHANGE // 1 min delay before switch off after the sensor state goes LOW uint32_t m_switchOffDelay = 60000; // off timer start time uint32_t m_offTimerStart = 0; // current PIR sensor pin state byte m_PIRsensorPinState = LOW; // PIR sensor enabled - ISR attached bool m_PIRenabled = true; // temp standby brightness store. initial value set as nightlight default target brightness byte briStandby _INIT(nightlightTargetBri); // temp hightlight brightness store. initial value set as current brightness byte briHighlight _INIT(bri); // highlight active/deactive monitor bool highlightActive = false; // wled on/off state in standby mode bool standbyoff = false; /* * return or change if new PIR sensor state is available */ static volatile bool newPIRsensorState(bool changeState = false, bool newState = false) { static volatile bool s_PIRsensorState = false; if (changeState) { s_PIRsensorState = newState; } return s_PIRsensorState; } /* * PIR sensor state has changed */ static void IRAM_ATTR ISR_PIRstateChange() { newPIRsensorState(true, true); } /* * switch strip on/off */ // now allowing adjustable standby and highlight brightness void switchStrip(bool switchOn) { //if (switchOn && bri == 0) { if (switchOn) { // **pir sensor is on and activated** //bri = briLast; if (bri != 0) { // is WLED currently on if (highlightActive) { // and is Highlight already on briHighlight = bri; // then update highlight brightness with current brightness } else { briStandby = bri; // else update standby brightness with current brightness } } else { // WLED is currently off if (!highlightActive) { // and Highlight is not already on briStandby = briLast; // then update standby brightness with last active brightness (before turned off) standbyoff = true; } else { // and Highlight is already on briHighlight = briLast; // then set hightlight brightness to last active brightness (before turned off) } } applyMacro(16); // apply highlight lighting without brightness if (bri != briHighlight) { bri = briHighlight; // set current highlight brightness to last set highlight brightness } stateUpdated(NotifyUpdateMode); highlightActive = true; // flag highlight is on } else { // **pir timer has elapsed** //briLast = bri; //bri = 0; if (bri != 0) { // is WLED currently on briHighlight = bri; // update highlight brightness with current brightness if (!standbyoff) { // bri = briStandby; // set standby brightness to last set standby brightness } else { // briLast = briStandby; // set standby off brightness bri = 0; // set power off in standby standbyoff = false; // turn off flag } applyMacro(macroLongPress); // apply standby lighting without brightness } else { // WLED is currently off briHighlight = briLast; // set last active brightness (before turned off) to highlight lighting brightness if (!standbyoff) { // bri = briStandby; // set standby brightness to last set standby brightness } else { // briLast = briStandby; // set standby off brightness bri = 0; // set power off in standby standbyoff = false; // turn off flag } applyMacro(macroLongPress); // apply standby lighting without brightness } stateUpdated(NotifyUpdateMode); highlightActive = false; // flag highlight is off } } /* * Read and update PIR sensor state. * Initilize/reset switch off timer */ bool updatePIRsensorState() { if (newPIRsensorState()) { m_PIRsensorPinState = digitalRead(PIRsensorPin); if (m_PIRsensorPinState == HIGH) { m_offTimerStart = 0; switchStrip(true); } else if (bri != 0) { // start switch off timer m_offTimerStart = millis(); } newPIRsensorState(true, false); return true; } return false; } /* * switch off the strip if the delay has elapsed */ bool handleOffTimer() { if (m_offTimerStart > 0) { if ((millis() - m_offTimerStart > m_switchOffDelay) || bri == 0 ) { // now also checking for manual power off during highlight mode switchStrip(false); m_offTimerStart = 0; return true; } } return false; } public: //Functions called by WLED /* * 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() { // PIR Sensor mode INPUT_PULLUP pinMode(PIRsensorPin, INPUT_PULLUP); // assign interrupt function and set CHANGE mode attachInterrupt(digitalPinToInterrupt(PIRsensorPin), ISR_PIRstateChange, CHANGE); // set delay to nightlight default duration on boot (after which json PIRoffSec overides if needed) m_switchOffDelay = (nightlightDelayMins*60000); applyMacro(macroButton); // apply default highlight lighting briHighlight = bri; applyMacro(macroDoublePress); // apply default standby lighting with brightness briStandby = bri; } /* * connected() is called every time the WiFi is (re)connected * Use it to initialize network interfaces */ void connected() { } /* * loop() is called continuously. Here you can check for events, read sensors, etc. */ void loop() { if (!updatePIRsensorState()) { handleOffTimer(); } } /* * addToJsonInfo() can be used to add custom entries to the /json/info part of the JSON API. * * Add PIR sensor state and switch off timer duration to jsoninfo */ void addToJsonInfo(JsonObject& root) { //this code adds "u":{"⏲ PIR sensor state":uiDomString} to the info object // the value contains a button to toggle the sensor enabled/disabled JsonObject user = root["u"]; if (user.isNull()) user = root.createNestedObject("u"); JsonArray infoArr = user.createNestedArray("⏲ PIR sensor state"); //name String uiDomString = ""; infoArr.add(uiDomString); //value //this code adds "u":{"⏲ switch off timer":uiDomString} to the info object infoArr = user.createNestedArray("⏲ switch off timer"); //name // off timer if (m_offTimerStart > 0) { uiDomString = ""; unsigned int offSeconds = (m_switchOffDelay - (millis() - m_offTimerStart)) / 1000; if (offSeconds >= 3600) { uiDomString += (offSeconds / 3600); uiDomString += " hours "; offSeconds %= 3600; } if (offSeconds >= 60) { uiDomString += (offSeconds / 60); offSeconds %= 60; } else if (uiDomString.length() > 0){ uiDomString += 0; } if (uiDomString.length() > 0){ uiDomString += " min "; } uiDomString += (offSeconds); infoArr.add(uiDomString + " sec"); } else { infoArr.add("inactive"); } } /* * 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 * Add "PIRenabled" to json state. This can be used to disable/enable the sensor. * Add "PIRoffSec" to json state. This can be used to adjust milliseconds . */ void addToJsonState(JsonObject& root) { root["PIRenabled"] = m_PIRenabled; root["PIRoffSec"] = (m_switchOffDelay / 1000); } /* * 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 * Read "PIRenabled" from json state and switch enable/disable the PIR sensor. * Read "PIRoffSec" from json state and adjust milliseconds . */ void readFromJsonState(JsonObject& root) { if (root["PIRoffSec"] != nullptr) { m_switchOffDelay = (1000 * max(60UL, min(43200UL, root["PIRoffSec"].as()))); } if (root["PIRenabled"] != nullptr) { if (root["PIRenabled"] && !m_PIRenabled) { attachInterrupt(digitalPinToInterrupt(PIRsensorPin), ISR_PIRstateChange, CHANGE); newPIRsensorState(true, true); } else if(m_PIRenabled) { detachInterrupt(PIRsensorPin); } m_PIRenabled = root["PIRenabled"]; } } /* * 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_PIRSWITCH; } //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! }; ================================================ FILE: usermods/PIR_sensor_switch/PIR_sensor_switch.cpp ================================================ #include "wled.h" #ifndef PIR_SENSOR_PIN // compatible with QuinLED-Dig-Uno #ifdef ARDUINO_ARCH_ESP32 #define PIR_SENSOR_PIN 23 // Q4 #else //ESP8266 boards #define PIR_SENSOR_PIN 13 // Q4 (D7 on D1 mini) #endif #endif #ifndef PIR_SENSOR_OFF_SEC #define PIR_SENSOR_OFF_SEC 600 #endif #ifndef PIR_SENSOR_MAX_SENSORS #define PIR_SENSOR_MAX_SENSORS 1 #endif /* * This usermod handles PIR sensor states. * The strip will be switched on and the off timer will be resetted when the sensor goes HIGH. * When the sensor state goes LOW, the off timer is started and when it expires, the strip is switched off. * Maintained by: @blazoncek * * Usermods allow you to add own functionality to WLED more easily * See: https://github.com/wled-dev/WLED/wiki/Add-own-functionality * * v2 usermods are class inheritance based and can (but don't have to) implement more functions, each of them is shown in this example. * Multiple v2 usermods can be added to one compilation easily. */ class PIRsensorSwitch : public Usermod { public: // constructor PIRsensorSwitch() {} // destructor ~PIRsensorSwitch() {} //Enable/Disable the PIR sensor inline void EnablePIRsensor(bool en) { enabled = en; } // Get PIR sensor enabled/disabled state inline bool PIRsensorEnabled() { return enabled; } private: byte prevPreset = 0; byte prevPlaylist = 0; volatile unsigned long offTimerStart = 0; // off timer start time volatile bool PIRtriggered = false; // did PIR trigger? bool initDone = false; // status of initialization unsigned long lastLoop = 0; bool sensorPinState[PIR_SENSOR_MAX_SENSORS] = {LOW}; // current PIR sensor pin state // configurable parameters #if PIR_SENSOR_PIN < 0 bool enabled = false; // PIR sensor disabled #else bool enabled = true; // PIR sensor enabled #endif int8_t PIRsensorPin[PIR_SENSOR_MAX_SENSORS] = {PIR_SENSOR_PIN}; // PIR sensor pin uint32_t m_switchOffDelay = PIR_SENSOR_OFF_SEC*1000; // delay before switch off after the sensor state goes LOW (10min) uint8_t m_onPreset = 0; // on preset uint8_t m_offPreset = 0; // off preset bool m_nightTimeOnly = false; // flag to indicate that PIR sensor should activate WLED during nighttime only bool m_mqttOnly = false; // flag to send MQTT message only (assuming it is enabled) // flag to enable triggering only if WLED is initially off (LEDs are not on, preventing running effect being overwritten by PIR) bool m_offOnly = false; bool m_offMode = offMode; bool m_override = false; // Home Assistant bool HomeAssistantDiscovery = false; // is HA discovery turned on int16_t idx = -1; // Domoticz virtual switch idx // strings to reduce flash memory usage (used more than twice) static const char _name[]; static const char _switchOffDelay[]; static const char _enabled[]; static const char _onPreset[]; static const char _offPreset[]; static const char _nightTime[]; static const char _mqttOnly[]; static const char _offOnly[]; static const char _haDiscovery[]; static const char _override[]; static const char _domoticzIDX[]; /** * check if it is daytime * if sunrise/sunset is not defined (no NTP or lat/lon) default to nighttime */ static bool isDayTime(); /** * switch strip on/off */ void switchStrip(bool switchOn); void publishMqtt(bool switchOn); // Create an MQTT Binary Sensor for Home Assistant Discovery purposes, this includes a pointer to the topic that is published to in the Loop. void publishHomeAssistantAutodiscovery(); /** * Read and update PIR sensor state. * Initialize/reset switch off timer */ bool updatePIRsensorState(); /** * switch off the strip if the delay has elapsed */ bool handleOffTimer(); public: //Functions called by WLED /** * 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() override; /** * connected() is called every time the WiFi is (re)connected * Use it to initialize network interfaces */ //void connected(); /** * onMqttConnect() is called when MQTT connection is established */ void onMqttConnect(bool sessionPresent) override; /** * loop() is called continuously. Here you can check for events, read sensors, etc. */ void loop() override; /** * addToJsonInfo() can be used to add custom entries to the /json/info part of the JSON API. * * Add PIR sensor state and switch off timer duration to jsoninfo */ void addToJsonInfo(JsonObject &root) override; /** * onStateChanged() is used to detect WLED state change */ void onStateChange(uint8_t mode) override; /** * 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) override; /** * provide the changeable values */ void addToConfig(JsonObject &root) override; /** * provide UI information and allow extending UI options */ void appendConfigData() override; /** * restore the changeable values * readFromConfig() is called before setup() to populate properties from values stored in cfg.json * * The function should return true if configuration was successfully loaded or false if there was no configuration. */ bool readFromConfig(JsonObject &root) override; /** * 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() override { return USERMOD_ID_PIRSWITCH; } }; // strings to reduce flash memory usage (used more than twice) const char PIRsensorSwitch::_name[] PROGMEM = "PIRsensorSwitch"; const char PIRsensorSwitch::_enabled[] PROGMEM = "PIRenabled"; const char PIRsensorSwitch::_switchOffDelay[] PROGMEM = "PIRoffSec"; const char PIRsensorSwitch::_onPreset[] PROGMEM = "on-preset"; const char PIRsensorSwitch::_offPreset[] PROGMEM = "off-preset"; const char PIRsensorSwitch::_nightTime[] PROGMEM = "nighttime-only"; const char PIRsensorSwitch::_mqttOnly[] PROGMEM = "mqtt-only"; const char PIRsensorSwitch::_offOnly[] PROGMEM = "off-only"; const char PIRsensorSwitch::_haDiscovery[] PROGMEM = "HA-discovery"; const char PIRsensorSwitch::_override[] PROGMEM = "override"; const char PIRsensorSwitch::_domoticzIDX[] PROGMEM = "domoticz-idx"; bool PIRsensorSwitch::isDayTime() { updateLocalTime(); uint8_t hr = hour(localTime); uint8_t mi = minute(localTime); if (sunrise && sunset) { if (hour(sunrise)
hr) { return true; } else { if (hour(sunrise)==hr && minute(sunrise)mi) { return true; } } } return false; } void PIRsensorSwitch::switchStrip(bool switchOn) { if (m_offOnly && bri && (switchOn || (!PIRtriggered && !switchOn))) return; //if lights on and off only, do nothing if (PIRtriggered && switchOn) return; //if already on and triggered before, do nothing PIRtriggered = switchOn; DEBUG_PRINT(F("PIR: strip=")); DEBUG_PRINTLN(switchOn?"on":"off"); if (switchOn) { if (m_onPreset) { if (currentPlaylist>0 && !offMode) { prevPlaylist = currentPlaylist; unloadPlaylist(); } else if (currentPreset>0 && !offMode) { prevPreset = currentPreset; } else { saveTemporaryPreset(); prevPlaylist = 0; prevPreset = 255; } applyPreset(m_onPreset, CALL_MODE_BUTTON_PRESET); return; } // preset not assigned if (bri == 0) { bri = briLast; stateUpdated(CALL_MODE_BUTTON); } } else { if (m_offPreset) { applyPreset(m_offPreset, CALL_MODE_BUTTON_PRESET); return; } else if (prevPlaylist) { if (currentPreset==m_onPreset || currentPlaylist==m_onPreset) applyPreset(prevPlaylist, CALL_MODE_BUTTON_PRESET); prevPlaylist = 0; return; } else if (prevPreset) { if (prevPreset<255) { if (currentPreset==m_onPreset || currentPlaylist==m_onPreset) applyPreset(prevPreset, CALL_MODE_BUTTON_PRESET); } else { if (currentPreset==m_onPreset || currentPlaylist==m_onPreset) applyTemporaryPreset(); } prevPreset = 0; return; } // preset not assigned if (bri != 0) { briLast = bri; bri = 0; stateUpdated(CALL_MODE_BUTTON); } } } void PIRsensorSwitch::publishMqtt(bool switchOn) { #ifndef WLED_DISABLE_MQTT //Check if MQTT Connected, otherwise it will crash the 8266 if (WLED_MQTT_CONNECTED) { char buf[128]; sprintf_P(buf, PSTR("%s/motion"), mqttDeviceTopic); //max length: 33 + 7 = 40 mqtt->publish(buf, 0, false, switchOn?"on":"off"); // Domoticz formatted message if (idx > 0) { StaticJsonDocument <128> msg; msg[F("idx")] = idx; msg[F("RSSI")] = WiFi.RSSI(); msg[F("command")] = F("switchlight"); msg[F("switchcmd")] = switchOn ? F("On") : F("Off"); serializeJson(msg, buf, 128); mqtt->publish("domoticz/in", 0, false, buf); } } #endif } void PIRsensorSwitch::publishHomeAssistantAutodiscovery() { #ifndef WLED_DISABLE_MQTT if (WLED_MQTT_CONNECTED) { StaticJsonDocument<600> doc; char uid[24], json_str[1024], buf[128]; sprintf_P(buf, PSTR("%s Motion"), serverDescription); //max length: 33 + 7 = 40 doc[F("name")] = buf; sprintf_P(buf, PSTR("%s/motion"), mqttDeviceTopic); //max length: 33 + 7 = 40 doc[F("stat_t")] = buf; doc[F("pl_on")] = "on"; doc[F("pl_off")] = "off"; sprintf_P(uid, PSTR("%s_motion"), escapedMac.c_str()); doc[F("uniq_id")] = uid; doc[F("dev_cla")] = F("motion"); doc[F("exp_aft")] = 1800; JsonObject device = doc.createNestedObject(F("device")); // attach the sensor to the same device device[F("name")] = serverDescription; device[F("ids")] = String(F("wled-sensor-")) + mqttClientID; device[F("mf")] = F(WLED_BRAND); device[F("mdl")] = F(WLED_PRODUCT_NAME); device[F("sw")] = versionString; sprintf_P(buf, PSTR("homeassistant/binary_sensor/%s/config"), uid); DEBUG_PRINTLN(buf); size_t payload_size = serializeJson(doc, json_str); DEBUG_PRINTLN(json_str); mqtt->publish(buf, 0, true, json_str, payload_size); // do we really need to retain? } #endif } bool PIRsensorSwitch::updatePIRsensorState() { bool stateChanged = false; bool allOff = true; for (int i = 0; i < PIR_SENSOR_MAX_SENSORS; i++) { if (PIRsensorPin[i] < 0) continue; bool pinState = digitalRead(PIRsensorPin[i]); if (pinState != sensorPinState[i]) { sensorPinState[i] = pinState; // change previous state stateChanged = true; if (sensorPinState[i] == HIGH) { offTimerStart = 0; allOff = false; if (!m_mqttOnly && (!m_nightTimeOnly || (m_nightTimeOnly && !isDayTime()))) switchStrip(true); } } } if (stateChanged) { publishMqtt(!allOff); // start switch off timer if (allOff) offTimerStart = millis(); } return stateChanged; } bool PIRsensorSwitch::handleOffTimer() { if (offTimerStart > 0 && millis() - offTimerStart > m_switchOffDelay) { offTimerStart = 0; if (!m_mqttOnly && (!m_nightTimeOnly || (m_nightTimeOnly && !isDayTime()) || PIRtriggered)) switchStrip(false); return true; } return false; } //Functions called by WLED void PIRsensorSwitch::setup() { for (int i = 0; i < PIR_SENSOR_MAX_SENSORS; i++) { sensorPinState[i] = LOW; if (PIRsensorPin[i] < 0) continue; // pin retrieved from cfg.json (readFromConfig()) prior to running setup() if (PinManager::allocatePin(PIRsensorPin[i], false, PinOwner::UM_PIR)) { // PIR Sensor mode INPUT_PULLDOWN #ifdef ESP8266 pinMode(PIRsensorPin[i], PIRsensorPin[i]==16 ? INPUT_PULLDOWN_16 : INPUT_PULLUP); // ESP8266 has INPUT_PULLDOWN on GPIO16 only #else pinMode(PIRsensorPin[i], INPUT_PULLDOWN); #endif sensorPinState[i] = digitalRead(PIRsensorPin[i]); } else { DEBUG_PRINT(F("PIRSensorSwitch pin ")); DEBUG_PRINTLN(i); DEBUG_PRINTLN(F(" allocation failed.")); PIRsensorPin[i] = -1; // allocation failed } } initDone = true; } void PIRsensorSwitch::onMqttConnect(bool sessionPresent) { if (HomeAssistantDiscovery) { publishHomeAssistantAutodiscovery(); } } void PIRsensorSwitch::loop() { // only check sensors 5x/s if (!enabled || millis() - lastLoop < 200) return; lastLoop = millis(); if (!updatePIRsensorState()) { handleOffTimer(); } } void PIRsensorSwitch::addToJsonInfo(JsonObject &root) { JsonObject user = root["u"]; if (user.isNull()) user = root.createNestedObject("u"); bool state = LOW; for (int i = 0; i < PIR_SENSOR_MAX_SENSORS; i++) if (PIRsensorPin[i] >= 0) state |= sensorPinState[i]; JsonArray infoArr = user.createNestedArray(FPSTR(_name)); String uiDomString; if (enabled) { if (offTimerStart > 0) { uiDomString = ""; unsigned int offSeconds = (m_switchOffDelay - (millis() - offTimerStart)) / 1000; if (offSeconds >= 3600) { uiDomString += (offSeconds / 3600); uiDomString += F("h "); offSeconds %= 3600; } if (offSeconds >= 60) { uiDomString += (offSeconds / 60); offSeconds %= 60; } else if (uiDomString.length() > 0) { uiDomString += 0; } if (uiDomString.length() > 0) { uiDomString += F("min "); } uiDomString += (offSeconds); infoArr.add(uiDomString + F("s")); } else { infoArr.add(state ? F("sensor on") : F("inactive")); } } else { infoArr.add(F("disabled")); } uiDomString = F(" "); infoArr.add(uiDomString); if (enabled) { JsonObject sensor = root[F("sensor")]; if (sensor.isNull()) sensor = root.createNestedObject(F("sensor")); sensor[F("motion")] = state || offTimerStart>0 ? true : false; } } void PIRsensorSwitch::onStateChange(uint8_t mode) { if (!initDone) return; DEBUG_PRINT(F("PIR: offTimerStart=")); DEBUG_PRINTLN(offTimerStart); if (m_override && PIRtriggered && offTimerStart) { // debounce // checking PIRtriggered and offTimerStart will prevent cancellation upon On trigger DEBUG_PRINTLN(F("PIR: Canceled.")); offTimerStart = 0; PIRtriggered = false; } } void PIRsensorSwitch::readFromJsonState(JsonObject &root) { if (!initDone) return; // prevent crash on boot applyPreset() JsonObject usermod = root[FPSTR(_name)]; if (!usermod.isNull()) { if (usermod[FPSTR(_enabled)].is()) { enabled = usermod[FPSTR(_enabled)].as(); } } } void PIRsensorSwitch::addToConfig(JsonObject &root) { JsonObject top = root.createNestedObject(FPSTR(_name)); top[FPSTR(_enabled)] = enabled; top[FPSTR(_switchOffDelay)] = m_switchOffDelay / 1000; JsonArray pinArray = top.createNestedArray("pin"); for (int i = 0; i < PIR_SENSOR_MAX_SENSORS; i++) pinArray.add(PIRsensorPin[i]); top[FPSTR(_onPreset)] = m_onPreset; top[FPSTR(_offPreset)] = m_offPreset; top[FPSTR(_nightTime)] = m_nightTimeOnly; top[FPSTR(_mqttOnly)] = m_mqttOnly; top[FPSTR(_offOnly)] = m_offOnly; top[FPSTR(_override)] = m_override; top[FPSTR(_haDiscovery)] = HomeAssistantDiscovery; top[FPSTR(_domoticzIDX)] = idx; DEBUG_PRINTLN(F("PIR config saved.")); } void PIRsensorSwitch::appendConfigData() { oappend(F("addInfo('PIRsensorSwitch:HA-discovery',1,'HA=Home Assistant');")); // 0 is field type, 1 is actual field oappend(F("addInfo('PIRsensorSwitch:override',1,'Cancel timer on change');")); // 0 is field type, 1 is actual field for (int i = 0; i < PIR_SENSOR_MAX_SENSORS; i++) { char str[128]; sprintf_P(str, PSTR("addInfo('PIRsensorSwitch:pin[]',%d,'','#%d');"), i, i); oappend(str); } } bool PIRsensorSwitch::readFromConfig(JsonObject &root) { int8_t oldPin[PIR_SENSOR_MAX_SENSORS]; for (int i = 0; i < PIR_SENSOR_MAX_SENSORS; i++) { oldPin[i] = PIRsensorPin[i]; PIRsensorPin[i] = -1; } DEBUG_PRINT(FPSTR(_name)); JsonObject top = root[FPSTR(_name)]; if (top.isNull()) { DEBUG_PRINTLN(F(": No config found. (Using defaults.)")); return false; } JsonArray pins = top["pin"]; if (!pins.isNull()) { for (size_t i = 0; i < PIR_SENSOR_MAX_SENSORS; i++) if (i < pins.size()) PIRsensorPin[i] = pins[i] | PIRsensorPin[i]; } else { PIRsensorPin[0] = top["pin"] | oldPin[0]; } enabled = top[FPSTR(_enabled)] | enabled; m_switchOffDelay = (top[FPSTR(_switchOffDelay)] | m_switchOffDelay/1000) * 1000; m_onPreset = top[FPSTR(_onPreset)] | m_onPreset; m_onPreset = max(0,min(250,(int)m_onPreset)); m_offPreset = top[FPSTR(_offPreset)] | m_offPreset; m_offPreset = max(0,min(250,(int)m_offPreset)); m_nightTimeOnly = top[FPSTR(_nightTime)] | m_nightTimeOnly; m_mqttOnly = top[FPSTR(_mqttOnly)] | m_mqttOnly; m_offOnly = top[FPSTR(_offOnly)] | m_offOnly; m_override = top[FPSTR(_override)] | m_override; HomeAssistantDiscovery = top[FPSTR(_haDiscovery)] | HomeAssistantDiscovery; idx = top[FPSTR(_domoticzIDX)] | idx; if (!initDone) { // reading config prior to setup() DEBUG_PRINTLN(F(" config loaded.")); } else { for (int i = 0; i < PIR_SENSOR_MAX_SENSORS; i++) if (oldPin[i] >= 0) PinManager::deallocatePin(oldPin[i], PinOwner::UM_PIR); setup(); DEBUG_PRINTLN(F(" config (re)loaded.")); } // use "return !top["newestParameter"].isNull();" when updating Usermod with new features return !(pins.isNull() || pins.size() != PIR_SENSOR_MAX_SENSORS); } static PIRsensorSwitch pir_sensor_switch; REGISTER_USERMOD(pir_sensor_switch); ================================================ FILE: usermods/PIR_sensor_switch/library.json ================================================ { "name": "PIR_sensor_switch", "build": { "libArchive": false } } ================================================ FILE: usermods/PIR_sensor_switch/readme.md ================================================ # PIR sensor switch This usermod-v2 modification allows the connection of a PIR sensor to switch on the LED strip when motion is detected. The switch-off occurs ten minutes after no more motion is detected. _Story:_ I use the PIR Sensor to automatically turn on the WLED analog clock in my home office room when I am there. The LED strip is switched [using a relay](https://kno.wled.ge/features/relay-control/) to keep the power consumption low when it is switched off. ## Web interface The info page in the web interface shows the remaining time of the off timer. Usermod can also be temporarily disbled/enabled from the info page by clicking PIR button. ## Sensor connection My setup uses an HC-SR501 or HC-SR602 sensor, an HC-SR505 should also work. The usermod uses GPIO13 (D1 mini pin D7) by default for the sensor signal, but can be changed in the Usermod settings page. [This example page](http://www.esp8266learning.com/wemos-mini-pir-sensor-example.php) describes how to connect the sensor. Use the potentiometers on the sensor to set the time delay to the minimum and the sensitivity to about half, or slightly above. You can also use usermod's off timer instead of sensor's. In such case rotate the potentiometer to its shortest time possible (or use SR602 which lacks such potentiometer). ## Usermod installation **NOTE:** Usermod has been included in master branch of WLED so it can be compiled in directly just by defining `-D USERMOD_PIRSWITCH` and optionally `-D PIR_SENSOR_PIN=16` to override default pin. You can also change the default off time by adding `-D PIR_SENSOR_OFF_SEC=30`. ## API to enable/disable the PIR sensor from outside. For example from another usermod To query or change the PIR sensor state the methods `bool PIRsensorEnabled()` and `void EnablePIRsensor(bool enable)` are available. When the PIR sensor state changes an MQTT message is broadcasted with topic `wled/deviceMAC/motion` and message `on` or `off`. Usermod can also be configured to send just the MQTT message but not change WLED state using settings page as well as responding to motion only at night (assuming NTP and latitude/longitude are set to determine sunrise/sunset times). ### There are two options to get access to the usermod instance _1._ Include `usermod_PIR_sensor_switch.h` **before** you include other usermods in `usermods_list.cpp' or _2._ Use `#include "usermod_PIR_sensor_switch.h"` at the top of the `usermod.h` where you need it. **Example usermod.h :** ```cpp #include "wled.h" #include "usermod_PIR_sensor_switch.h" class MyUsermod : public Usermod { //... void togglePIRSensor() { #ifdef USERMOD_PIR_SENSOR_SWITCH PIRsensorSwitch *PIRsensor = (PIRsensorSwitch::*) UsermodManager::lookup(USERMOD_ID_PIRSWITCH); if (PIRsensor != nullptr) { PIRsensor->EnablePIRsensor(!PIRsensor->PIRsensorEnabled()); } #endif } //... }; ``` ### Configuration options Usermod can be configured via the Usermods settings page. * `PIRenabled` - enable/disable usermod * `pin` - dynamically change GPIO pin where PIR sensor is attached to ESP * `PIRoffSec` - number of seconds after PIR sensor deactivates when usermod triggers Off preset (or turns WLED off) * `on-preset` - preset triggered when PIR activates (if this is 0 it will just turn WLED on) * `off-preset` - preset triggered when PIR deactivates (if this is 0 it will just turn WLED off) * `nighttime-only` - enable triggering only between sunset and sunrise (you will need to set up _NTP_, _Lat_ & _Lon_ in Time & Macro settings) * `mqtt-only` - send only MQTT messages, do not interact with WLED * `off-only` - only trigger presets or turn WLED on/off if WLED is not already on (displaying effect) * `notifications` - enable or disable sending notifications to other WLED instances using Sync button * `HA-discovery` - enable automatic discovery in Home Assistant * `override` - override PIR input when WLED state is changed using UI * `domoticz-idx` - Domoticz virtual switch ID (used with MQTT `domoticz/in`) Have fun - @gegu & @blazoncek ## Change log 2021-04 * Adaptation for runtime configuration. 2021-11 * Added information about dynamic configuration options * Added option to temporary enable/disable usermod from WLED UI (Info dialog) 2022-11 * Added compile time option for off timer. * Added Home Assistant autodiscovery MQTT broadcast. * Updated info on compiling. 2023-?? * Override option * Domoticz virtual switch ID (used with MQTT `domoticz/in`) 2024-02 * Added compile time option to expand number of PIR sensors (they are logically ORed) `-D PIR_SENSOR_MAX_SENSORS=3` ================================================ FILE: usermods/PS_Comet/PS_Comet.cpp ================================================ #include "wled.h" #include "FXparticleSystem.h" unsigned long nextCometCreationTime = 0; #define FX_FALLBACK_STATIC { SEGMENT.fill(SEGCOLOR(0)); return; } // Use UINT32_MAX - 1 for the "no comet" case so we can add 1 later and not have it overflow #define NULL_INDEX UINT32_MAX - 1 /////////////////////// // Effect Function // /////////////////////// void mode_pscomet() { ParticleSystem2D *PartSys = nullptr; uint32_t i; if (SEGMENT.call == 0) { // Initialization // Try to allocate one comet for every column if (!initParticleSystem2D(PartSys, SEGMENT.vWidth())) { FX_FALLBACK_STATIC; // Allocation failed or not 2D } PartSys->setMotionBlur(170); // Enable motion blur PartSys->setParticleSize(0); // Allow small comets to be a single pixel wide } else { PartSys = reinterpret_cast(SEGENV.data); // If not first call, use existing data } if (PartSys == nullptr || SEGMENT.vHeight() < 2 || SEGMENT.vWidth() < 2) { FX_FALLBACK_STATIC; } PartSys->updateSystem(); // Update system properties (dimensions and data pointers) auto has_fallen_off_screen = [PartSys](uint32_t particleIndex) { return particleIndex < PartSys->numSources ? PartSys->sources[particleIndex].source.y < PartSys->maxY * -1 : true; }; // This will be SEGMENT.vWidth() unless the particle system had insufficient memory uint32_t numComets = PartSys->numSources; // Pick a random column for a new comet to spawn, but reset it to null if it's not time yet or there's already a // comet nearby uint32_t chosenIndex = hw_random(numComets); if ( strip.now < nextCometCreationTime || !has_fallen_off_screen(chosenIndex - 1) || !has_fallen_off_screen(chosenIndex) || !has_fallen_off_screen(chosenIndex + 1) ) { chosenIndex = NULL_INDEX; } else { uint16_t cometFrequencyDelay = 2040 - (SEGMENT.intensity << 3); nextCometCreationTime = strip.now + cometFrequencyDelay + hw_random16(cometFrequencyDelay); } uint8_t canLargeCometSpawn = // Slider 3 determines % of large comets with extra particle sources on their sides SEGMENT.custom1 > hw_random8(254) && chosenIndex != 0 && chosenIndex != numComets - 1; uint8_t fallingSpeed = 1 + (SEGMENT.speed >> 2); // Update the comets for (i = 0; i < numComets; i++) { auto& source = PartSys->sources[i]; auto& sourceParticle = source.source; if (!has_fallen_off_screen(i)) { // Active comets fall downwards and emit flames sourceParticle.y -= fallingSpeed; source.vy = (SEGMENT.speed >> 5) - fallingSpeed; // Emitting speed (upwards) PartSys->flameEmit(PartSys->sources[i]); continue; } bool isChosenComet = i == chosenIndex; bool isChosenSideComet = canLargeCometSpawn && (i == chosenIndex - 1 || i == chosenIndex + 1); // Chosen comets respawn at the top if (isChosenComet || isChosenSideComet) { // Map the comet index into an output pixel index sourceParticle.x = i * PartSys->maxX / (SEGMENT.vWidth() - 1); // Spawn a bit above the top to avoid popping into view sourceParticle.y = PartSys->maxY + (2 * fallingSpeed); if (isChosenComet) { // Slider 4 controls comet length via particle lifetime and fire intensity adjustments source.maxLife = 16 + (SEGMENT.custom2 >> 2); source.minLife = source.maxLife >> 1; sourceParticle.ttl = 16 - (SEGMENT.custom2 >> 4); } else { // Side comets have fixed length source.maxLife = 18; source.minLife = 14; sourceParticle.ttl = 16; // Shift side comets up by 1 pixel sourceParticle.y += 2 * PartSys->maxY / (SEGMENT.vHeight() - 1); } } } // Slider 4 controls comet length via particle lifetime and fire intensity adjustments PartSys->updateFire(max(255U - SEGMENT.custom2, 45U)); } static const char _data_FX_MODE_PSCOMET[] PROGMEM = "PS Comet@Falling Speed,Comet Frequency,Large Comet Probability,Comet Length;;!;2;pal=35,sx=128,ix=255,c1=32,c2=128"; ///////////////////// // UserMod Class // ///////////////////// class PSCometUsermod : public Usermod { public: void setup() override { strip.addEffect(255, &mode_pscomet, _data_FX_MODE_PSCOMET); } void loop() override {} }; static PSCometUsermod ps_comet; REGISTER_USERMOD(ps_comet); ================================================ FILE: usermods/PS_Comet/README.md ================================================ ## Description A 2D falling comet effect similar to "Matrix" but with a fire particle simulation to enhance the comet trail visuals. Works with custom color palettes, defaulting to "Fire". Supports "small" and "large" comets which are 1px and 3px wide respectively. Demo: [https://imgur.com/a/i1v5WAy](https://imgur.com/a/i1v5WAy) ## Installation To activate the usermod, add the following line to your platformio_override.ini ```ini custom_usermods = ps_comet ``` Or if you are already using a usermod, append ps_comet to the list ```ini custom_usermods = audioreactive ps_comet ``` You should now see "PS Comet" appear in your effect list. ## Parameters 1. **Falling Speed** sets how fast the comets fall 2. **Comet Frequency** determines how many comets are on screen at a time 3. **Large Comet Probability** determines how often large 3px wide comets spawn 4. **Comet Length** sets how far comet trails stretch vertically ================================================ FILE: usermods/PS_Comet/library.json ================================================ { "name": "PS Comet", "build": { "libArchive": false } } ================================================ FILE: usermods/PWM_fan/PWM_fan.cpp ================================================ #include "wled.h" #if defined(USERMOD_DALLASTEMPERATURE) #include "UsermodTemperature.h" #elif defined(USERMOD_SHT) #include "ShtUsermod.h" #else #error The "PWM fan" usermod requires "Dallas Temeprature" or "SHT" usermod to function properly. #endif // PWM & tacho code curtesy of @KlausMu // https://github.com/KlausMu/esp32-fan-controller/tree/main/src // adapted for WLED usermod by @blazoncek #ifndef TACHO_PIN #define TACHO_PIN -1 #endif #ifndef PWM_PIN #define PWM_PIN -1 #endif // tacho counter static volatile unsigned long counter_rpm = 0; // Interrupt counting every rotation of the fan // https://desire.giesecke.tk/index.php/2018/01/30/change-global-variables-from-isr/ static void IRAM_ATTR rpm_fan() { counter_rpm++; } class PWMFanUsermod : public Usermod { private: bool initDone = false; bool enabled = true; unsigned long msLastTachoMeasurement = 0; uint16_t last_rpm = 0; #ifdef ARDUINO_ARCH_ESP32 uint8_t pwmChannel = 255; #endif bool lockFan = false; #ifdef USERMOD_DALLASTEMPERATURE UsermodTemperature* tempUM; #elif defined(USERMOD_SHT) ShtUsermod* tempUM; #endif // configurable parameters int8_t tachoPin = TACHO_PIN; int8_t pwmPin = PWM_PIN; uint8_t tachoUpdateSec = 30; float targetTemperature = 35.0; uint8_t minPWMValuePct = 0; uint8_t maxPWMValuePct = 100; uint8_t numberOfInterrupsInOneSingleRotation = 2; // Number of interrupts ESP32 sees on tacho signal on a single fan rotation. All the fans I've seen trigger two interrups. uint8_t pwmValuePct = 0; // constant values static const uint8_t _pwmMaxValue = 255; static const uint8_t _pwmMaxStepCount = 7; float _pwmTempStepSize = 0.5f; // strings to reduce flash memory usage (used more than twice) static const char _name[]; static const char _enabled[]; static const char _tachoPin[]; static const char _pwmPin[]; static const char _temperature[]; static const char _tachoUpdateSec[]; static const char _minPWMValuePct[]; static const char _maxPWMValuePct[]; static const char _IRQperRotation[]; static const char _speed[]; static const char _lock[]; void initTacho(void) { if (tachoPin < 0 || !PinManager::allocatePin(tachoPin, false, PinOwner::UM_Unspecified)){ tachoPin = -1; return; } pinMode(tachoPin, INPUT); digitalWrite(tachoPin, HIGH); attachInterrupt(digitalPinToInterrupt(tachoPin), rpm_fan, FALLING); DEBUG_PRINTLN(F("Tacho sucessfully initialized.")); } void deinitTacho(void) { if (tachoPin < 0) return; detachInterrupt(digitalPinToInterrupt(tachoPin)); PinManager::deallocatePin(tachoPin, PinOwner::UM_Unspecified); tachoPin = -1; } void updateTacho(void) { // store milliseconds when tacho was measured the last time msLastTachoMeasurement = millis(); if (tachoPin < 0) return; // start of tacho measurement // detach interrupt while calculating rpm detachInterrupt(digitalPinToInterrupt(tachoPin)); // calculate rpm last_rpm = (counter_rpm * 60) / numberOfInterrupsInOneSingleRotation; last_rpm /= tachoUpdateSec; // reset counter counter_rpm = 0; // attach interrupt again attachInterrupt(digitalPinToInterrupt(tachoPin), rpm_fan, FALLING); } // https://randomnerdtutorials.com/esp32-pwm-arduino-ide/ void initPWMfan(void) { if (pwmPin < 0 || !PinManager::allocatePin(pwmPin, true, PinOwner::UM_Unspecified)) { enabled = false; pwmPin = -1; return; } #ifdef ESP8266 analogWriteRange(255); analogWriteFreq(WLED_PWM_FREQ); #else pwmChannel = PinManager::allocateLedc(1); if (pwmChannel == 255) { //no more free LEDC channels deinitPWMfan(); return; } // configure LED PWM functionalitites ledcSetup(pwmChannel, 25000, 8); // attach the channel to the GPIO to be controlled ledcAttachPin(pwmPin, pwmChannel); #endif DEBUG_PRINTLN(F("Fan PWM sucessfully initialized.")); } void deinitPWMfan(void) { if (pwmPin < 0) return; PinManager::deallocatePin(pwmPin, PinOwner::UM_Unspecified); #ifdef ARDUINO_ARCH_ESP32 PinManager::deallocateLedc(pwmChannel, 1); #endif pwmPin = -1; } void updateFanSpeed(uint8_t pwmValue){ if (!enabled || pwmPin < 0) return; #ifdef ESP8266 analogWrite(pwmPin, pwmValue); #else ledcWrite(pwmChannel, pwmValue); #endif } float getActualTemperature(void) { #if defined(USERMOD_DALLASTEMPERATURE) || defined(USERMOD_SHT) if (tempUM != nullptr) return tempUM->getTemperatureC(); #endif return -127.0f; } void setFanPWMbasedOnTemperature(void) { float temp = getActualTemperature(); // dividing minPercent and maxPercent into equal pwmvalue sizes int pwmStepSize = ((maxPWMValuePct - minPWMValuePct) * _pwmMaxValue) / (_pwmMaxStepCount*100); int pwmStep = calculatePwmStep(temp - targetTemperature); // minimum based on full speed - not entered MaxPercent int pwmMinimumValue = (minPWMValuePct * _pwmMaxValue) / 100; updateFanSpeed(pwmMinimumValue + pwmStep*pwmStepSize); } uint8_t calculatePwmStep(float diffTemp){ if ((diffTemp == NAN) || (diffTemp <= -100.0)) { DEBUG_PRINTLN(F("WARNING: no temperature value available. Cannot do temperature control. Will set PWM fan to 255.")); return _pwmMaxStepCount; } if(diffTemp <=0){ return 0; } int calculatedStep = (diffTemp / _pwmTempStepSize)+1; // anything greater than max stepcount gets max return (uint8_t)min((int)_pwmMaxStepCount,calculatedStep); } public: // gets called once at boot. Do all initialization that doesn't depend on // network here void setup() override { #ifdef USERMOD_DALLASTEMPERATURE // This Usermod requires Temperature usermod tempUM = (UsermodTemperature*) UsermodManager::lookup(USERMOD_ID_TEMPERATURE); #elif defined(USERMOD_SHT) tempUM = (ShtUsermod*) UsermodManager::lookup(USERMOD_ID_SHT); #endif initTacho(); initPWMfan(); updateFanSpeed((minPWMValuePct * 255) / 100); // inital fan speed initDone = true; } // gets called every time WiFi is (re-)connected. Initialize own network // interfaces here void connected() override {} /* * Da loop. */ void loop() override { if (!enabled || strip.isUpdating()) return; unsigned long now = millis(); if ((now - msLastTachoMeasurement) < (tachoUpdateSec * 1000)) return; updateTacho(); if (!lockFan) setFanPWMbasedOnTemperature(); } /* * 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) override { JsonObject user = root["u"]; if (user.isNull()) user = root.createNestedObject("u"); JsonArray infoArr = user.createNestedArray(FPSTR(_name)); String uiDomString = F(""); infoArr.add(uiDomString); if (enabled) { JsonArray infoArr = user.createNestedArray(F("Manual")); String uiDomString = F("
"); // infoArr.add(uiDomString); JsonArray data = user.createNestedArray(F("Speed")); if (tachoPin >= 0) { data.add(last_rpm); data.add(F("rpm")); } else { if (lockFan) data.add(F("locked")); else data.add(F("auto")); } } } /* * 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) override { if (!initDone) return; // prevent crash on boot applyPreset() JsonObject usermod = root[FPSTR(_name)]; if (!usermod.isNull()) { if (usermod[FPSTR(_enabled)].is()) { enabled = usermod[FPSTR(_enabled)].as(); if (!enabled) updateFanSpeed(0); } if (enabled && !usermod[FPSTR(_speed)].isNull() && usermod[FPSTR(_speed)].is()) { pwmValuePct = usermod[FPSTR(_speed)].as(); updateFanSpeed((constrain(pwmValuePct,0,100) * 255) / 100); if (pwmValuePct) lockFan = true; } if (enabled && !usermod[FPSTR(_lock)].isNull() && usermod[FPSTR(_lock)].is()) { lockFan = usermod[FPSTR(_lock)].as(); } } } /* * 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) override { JsonObject top = root.createNestedObject(FPSTR(_name)); // usermodname top[FPSTR(_enabled)] = enabled; top[FPSTR(_pwmPin)] = pwmPin; top[FPSTR(_tachoPin)] = tachoPin; top[FPSTR(_tachoUpdateSec)] = tachoUpdateSec; top[FPSTR(_temperature)] = targetTemperature; top[FPSTR(_minPWMValuePct)] = minPWMValuePct; top[FPSTR(_maxPWMValuePct)] = maxPWMValuePct; top[FPSTR(_IRQperRotation)] = numberOfInterrupsInOneSingleRotation; DEBUG_PRINTLN(F("Autosave config saved.")); } /* * 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 :) * * The function should return true if configuration was successfully loaded or false if there was no configuration. */ bool readFromConfig(JsonObject& root) override { int8_t newTachoPin = tachoPin; int8_t newPwmPin = pwmPin; JsonObject top = root[FPSTR(_name)]; DEBUG_PRINT(FPSTR(_name)); if (top.isNull()) { DEBUG_PRINTLN(F(": No config found. (Using defaults.)")); return false; } enabled = top[FPSTR(_enabled)] | enabled; newTachoPin = top[FPSTR(_tachoPin)] | newTachoPin; newPwmPin = top[FPSTR(_pwmPin)] | newPwmPin; tachoUpdateSec = top[FPSTR(_tachoUpdateSec)] | tachoUpdateSec; tachoUpdateSec = (uint8_t) max(1,(int)tachoUpdateSec); // bounds checking targetTemperature = top[FPSTR(_temperature)] | targetTemperature; minPWMValuePct = top[FPSTR(_minPWMValuePct)] | minPWMValuePct; minPWMValuePct = (uint8_t) min(100,max(0,(int)minPWMValuePct)); // bounds checking maxPWMValuePct = top[FPSTR(_maxPWMValuePct)] | maxPWMValuePct; maxPWMValuePct = (uint8_t) min(100,max((int)minPWMValuePct,(int)maxPWMValuePct)); // bounds checking numberOfInterrupsInOneSingleRotation = top[FPSTR(_IRQperRotation)] | numberOfInterrupsInOneSingleRotation; numberOfInterrupsInOneSingleRotation = (uint8_t) max(1,(int)numberOfInterrupsInOneSingleRotation); // bounds checking if (!initDone) { // first run: reading from cfg.json tachoPin = newTachoPin; pwmPin = newPwmPin; DEBUG_PRINTLN(F(" config loaded.")); } else { DEBUG_PRINTLN(F(" config (re)loaded.")); // changing paramters from settings page if (tachoPin != newTachoPin || pwmPin != newPwmPin) { DEBUG_PRINTLN(F("Re-init pins.")); // deallocate pin and release interrupts deinitTacho(); deinitPWMfan(); tachoPin = newTachoPin; pwmPin = newPwmPin; // initialise setup(); } } // use "return !top["newestParameter"].isNull();" when updating Usermod with new features return !top[FPSTR(_IRQperRotation)].isNull(); } /* * 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() override { return USERMOD_ID_PWM_FAN; } }; // strings to reduce flash memory usage (used more than twice) const char PWMFanUsermod::_name[] PROGMEM = "PWM-fan"; const char PWMFanUsermod::_enabled[] PROGMEM = "enabled"; const char PWMFanUsermod::_tachoPin[] PROGMEM = "tacho-pin"; const char PWMFanUsermod::_pwmPin[] PROGMEM = "PWM-pin"; const char PWMFanUsermod::_temperature[] PROGMEM = "target-temp-C"; const char PWMFanUsermod::_tachoUpdateSec[] PROGMEM = "tacho-update-s"; const char PWMFanUsermod::_minPWMValuePct[] PROGMEM = "min-PWM-percent"; const char PWMFanUsermod::_maxPWMValuePct[] PROGMEM = "max-PWM-percent"; const char PWMFanUsermod::_IRQperRotation[] PROGMEM = "IRQs-per-rotation"; const char PWMFanUsermod::_speed[] PROGMEM = "speed"; const char PWMFanUsermod::_lock[] PROGMEM = "lock"; static PWMFanUsermod pwm_fan; REGISTER_USERMOD(pwm_fan); ================================================ FILE: usermods/PWM_fan/library.json ================================================ { "name": "PWM_fan", "build": { "libArchive": false, "extraScript": "setup_deps.py" } } ================================================ FILE: usermods/PWM_fan/readme.md ================================================ # PWM fan v2 Usermod to to control PWM fan with RPM feedback and temperature control This usermod requires the Dallas Temperature usermod to obtain temperature information. If it's not available, the fan will run at 100% speed. If the fan does not have _tachometer_ (RPM) output you can set the _tachometer-pin_ to -1 to disable that feature. You can also set the threshold temperature at which fan runs at lowest speed. If the measured temperature is 3°C greater than the threshold temperature, the fan will run at 100%. If the _tachometer_ is supported, the current speed (in RPM) will be displayed on the WLED Info page. ## Installation Add the `PWM_fan` to `custom_usermods` in your `platformio.ini` (or `platformio_override.ini`) You will also need `Temperature` or `sht`. ### Define Your Options All of the parameters are configured during run-time using Usermods settings page. This includes: * PWM output pin (can be configured at compile time `-D PWM_PIN=xx`) * tachometer input pin (can be configured at compile time `-D TACHO_PIN=xx`) * sampling frequency in seconds * threshold temperature in degrees Celsius _NOTE:_ You may also need to tweak Dallas Temperature usermod sampling frequency to match PWM fan sampling frequency. ### PlatformIO requirements No special requirements. ## Control PWM fan speed using JSON API e.g. you can use `{"PWM-fan":{"speed":30,"lock":true}}` to lock fan speed to 30 percent of maximum. (replace 30 with an arbitrary value between 0 and 100) If you include `speed` property you can set fan speed as a percentage (%) of maximum speed. If you include `lock` property you can lock (_true_) or unlock (_false_) the fan speed. If the fan speed is unlocked, it will revert to temperature controlled speed on the next update cycle. Once fan speed is locked it will remain so until it is unlocked by the next API call. ## Change Log 2021-10 * First public release 2022-05 * Added JSON API call to allow changing of speed ================================================ FILE: usermods/PWM_fan/setup_deps.py ================================================ from platformio.package.meta import PackageSpec Import('env') libs = [PackageSpec(lib).name for lib in env.GetProjectOption("lib_deps",[])] # Check for dependencies if "Temperature" in libs: env.Append(CPPDEFINES=[("USERMOD_DALLASTEMPERATURE")]) elif "sht" in libs: env.Append(CPPDEFINES=[("USERMOD_SHT")]) elif "PWM_fan" in libs: # The script can be run if this module was previously selected raise RuntimeError("PWM_fan usermod requires Temperature or sht to be enabled") ================================================ FILE: usermods/RTC/RTC.cpp ================================================ #include "src/dependencies/time/DS1307RTC.h" #include "wled.h" //Connect DS1307 to standard I2C pins (ESP32: GPIO 21 (SDA)/GPIO 22 (SCL)) class RTCUsermod : public Usermod { private: unsigned long lastTime = 0; bool disabled = false; public: void setup() { if (i2c_scl<0 || i2c_sda<0) { disabled = true; return; } RTC.begin(); time_t rtcTime = RTC.get(); if (rtcTime) { toki.setTime(rtcTime,TOKI_NO_MS_ACCURACY,TOKI_TS_RTC); updateLocalTime(); } else { if (!RTC.chipPresent()) disabled = true; //don't waste time if H/W error } } void loop() { if (disabled || strip.isUpdating()) return; if (toki.isTick()) { time_t t = toki.second(); if (t != RTC.get()) RTC.set(t); //set RTC to NTP/UI-provided value } } /* * 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) * I highly recommend checking out the basics of ArduinoJson serialization and deserialization in order to use custom settings! */ // void addToConfig(JsonObject& root) // { // JsonObject top = root.createNestedObject("RTC"); // JsonArray pins = top.createNestedArray("pin"); // pins.add(i2c_scl); // pins.add(i2c_sda); // } uint16_t getId() { return USERMOD_ID_RTC; } }; static RTCUsermod rtc; REGISTER_USERMOD(rtc); ================================================ FILE: usermods/RTC/library.json ================================================ { "name": "RTC", "build": { "libArchive": false } } ================================================ FILE: usermods/RTC/readme.md ================================================ # DS1307/DS3231 Real time clock Gets the time from I2C RTC module on boot. This allows clock operation if WiFi is not available. The stored time is updated each time NTP is synced. ## Installation Add the build flag `-D USERMOD_RTC` to your platformio environment. ================================================ FILE: usermods/RelayBlinds/index.htm ================================================ Blinds
================================================ FILE: usermods/RelayBlinds/presets.json ================================================ {"0":{},"2":{"n":"▲","win":"U0=2"},"1":{"n":"▼","win":"U0=1"}} ================================================ FILE: usermods/RelayBlinds/readme.md ================================================ # RelayBlinds usermod This simple usermod toggles two relay pins momentarily (defaults to 500ms) when `userVar0` is set. e.g. can be used to "push" the buttons of a window blinds motor controller. v1 usermod. Please replace usermod.cpp in the `wled00` directory with the one in this file. You may upload `index.htm` to `[WLED-IP]/edit` to replace the default lighting UI with a simple Up/Down button one. A simple `presets.json` file is available. This makes the relay actions controllable via two presets to facilitate control e.g. the default UI or Alexa. ================================================ FILE: usermods/RelayBlinds/usermod.cpp ================================================ #include "wled.h" //Use userVar0 and userVar1 (API calls &U0=,&U1=, uint16_t) //gets called once at boot. Do all initialization that doesn't depend on network here void userSetup() { } //gets called every time WiFi is (re-)connected. Initialize own network interfaces here void userConnected() { } /* * Physical IO */ #define PIN_UP_RELAY 4 #define PIN_DN_RELAY 5 #define PIN_ON_TIME 500 bool upActive = false, upActiveBefore = false, downActive = false, downActiveBefore = false; unsigned long upStartTime = 0, downStartTime = 0; void handleRelay() { //up and down relays if (userVar0) { upActive = true; if (userVar0 == 1) { upActive = false; downActive = true; } userVar0 = 0; } if (upActive) { if(!upActiveBefore) { pinMode(PIN_UP_RELAY, OUTPUT); digitalWrite(PIN_UP_RELAY, LOW); upActiveBefore = true; upStartTime = millis(); DEBUG_PRINTLN(F("UPA")); } if (millis()- upStartTime > PIN_ON_TIME) { upActive = false; DEBUG_PRINTLN(F("UPN")); } } else if (upActiveBefore) { pinMode(PIN_UP_RELAY, INPUT); upActiveBefore = false; } if (downActive) { if(!downActiveBefore) { pinMode(PIN_DN_RELAY, OUTPUT); digitalWrite(PIN_DN_RELAY, LOW); downActiveBefore = true; downStartTime = millis(); } if (millis()- downStartTime > PIN_ON_TIME) { downActive = false; } } else if (downActiveBefore) { pinMode(PIN_DN_RELAY, INPUT); downActiveBefore = false; } } //loop. You can use "if (WLED_CONNECTED)" to check for successful connection void userLoop() { handleRelay(); } ================================================ FILE: usermods/SN_Photoresistor/SN_Photoresistor.cpp ================================================ #include "wled.h" #include "SN_Photoresistor.h" //Pin defaults for QuinLed Dig-Uno (A0) #ifndef PHOTORESISTOR_PIN #define PHOTORESISTOR_PIN A0 #endif static bool checkBoundSensor(float newValue, float prevValue, float maxDiff) { return isnan(prevValue) || newValue <= prevValue - maxDiff || newValue >= prevValue + maxDiff; } uint16_t Usermod_SN_Photoresistor::getLuminance() { // http://forum.arduino.cc/index.php?topic=37555.0 // https://forum.arduino.cc/index.php?topic=185158.0 float volts = analogRead(PHOTORESISTOR_PIN) * (referenceVoltage / adcPrecision); float amps = volts / resistorValue; float lux = amps * 1000000 * 2.0; lastMeasurement = millis(); getLuminanceComplete = true; return uint16_t(lux); } void Usermod_SN_Photoresistor::setup() { // set pinmode pinMode(PHOTORESISTOR_PIN, INPUT); } void Usermod_SN_Photoresistor::loop() { if (disabled || strip.isUpdating()) return; unsigned long now = millis(); // check to see if we are due for taking a measurement // lastMeasurement will not be updated until the conversion // is complete the the reading is finished if (now - lastMeasurement < readingInterval) { return; } uint16_t currentLDRValue = getLuminance(); if (checkBoundSensor(currentLDRValue, lastLDRValue, offset)) { lastLDRValue = currentLDRValue; #ifndef WLED_DISABLE_MQTT if (WLED_MQTT_CONNECTED) { char subuf[45]; strcpy(subuf, mqttDeviceTopic); strcat_P(subuf, PSTR("/luminance")); mqtt->publish(subuf, 0, true, String(lastLDRValue).c_str()); } else { DEBUG_PRINTLN(F("Missing MQTT connection. Not publishing data")); } } #endif } void Usermod_SN_Photoresistor::addToJsonInfo(JsonObject &root) { JsonObject user = root[F("u")]; if (user.isNull()) user = root.createNestedObject(F("u")); JsonArray lux = user.createNestedArray(F("Luminance")); if (!getLuminanceComplete) { // if we haven't read the sensor yet, let the user know // that we are still waiting for the first measurement lux.add((USERMOD_SN_PHOTORESISTOR_FIRST_MEASUREMENT_AT - millis()) / 1000); lux.add(F(" sec until read")); return; } lux.add(lastLDRValue); lux.add(F(" lux")); } /** * addToConfig() (called from set.cpp) stores persistent properties to cfg.json */ void Usermod_SN_Photoresistor::addToConfig(JsonObject &root) { // we add JSON object. JsonObject top = root.createNestedObject(FPSTR(_name)); // usermodname top[FPSTR(_enabled)] = !disabled; top[FPSTR(_readInterval)] = readingInterval / 1000; top[FPSTR(_referenceVoltage)] = referenceVoltage; top[FPSTR(_resistorValue)] = resistorValue; top[FPSTR(_adcPrecision)] = adcPrecision; top[FPSTR(_offset)] = offset; DEBUG_PRINTLN(F("Photoresistor config saved.")); } /** * readFromConfig() is called before setup() to populate properties from values stored in cfg.json */ bool Usermod_SN_Photoresistor::readFromConfig(JsonObject &root) { // we look for JSON object. JsonObject top = root[FPSTR(_name)]; if (top.isNull()) { DEBUG_PRINT(FPSTR(_name)); DEBUG_PRINTLN(F(": No config found. (Using defaults.)")); return false; } disabled = !(top[FPSTR(_enabled)] | !disabled); readingInterval = (top[FPSTR(_readInterval)] | readingInterval/1000) * 1000; // convert to ms referenceVoltage = top[FPSTR(_referenceVoltage)] | referenceVoltage; resistorValue = top[FPSTR(_resistorValue)] | resistorValue; adcPrecision = top[FPSTR(_adcPrecision)] | adcPrecision; offset = top[FPSTR(_offset)] | offset; DEBUG_PRINT(FPSTR(_name)); DEBUG_PRINTLN(F(" config (re)loaded.")); // use "return !top["newestParameter"].isNull();" when updating Usermod with new features return true; } // strings to reduce flash memory usage (used more than twice) const char Usermod_SN_Photoresistor::_name[] PROGMEM = "Photoresistor"; const char Usermod_SN_Photoresistor::_enabled[] PROGMEM = "enabled"; const char Usermod_SN_Photoresistor::_readInterval[] PROGMEM = "read-interval-s"; const char Usermod_SN_Photoresistor::_referenceVoltage[] PROGMEM = "supplied-voltage"; const char Usermod_SN_Photoresistor::_resistorValue[] PROGMEM = "resistor-value"; const char Usermod_SN_Photoresistor::_adcPrecision[] PROGMEM = "adc-precision"; const char Usermod_SN_Photoresistor::_offset[] PROGMEM = "offset"; static Usermod_SN_Photoresistor sn_photoresistor; REGISTER_USERMOD(sn_photoresistor); ================================================ FILE: usermods/SN_Photoresistor/SN_Photoresistor.h ================================================ #pragma once #include "wled.h" // the frequency to check photoresistor, 10 seconds #ifndef USERMOD_SN_PHOTORESISTOR_MEASUREMENT_INTERVAL #define USERMOD_SN_PHOTORESISTOR_MEASUREMENT_INTERVAL 10000 #endif // how many seconds after boot to take first measurement, 10 seconds #ifndef USERMOD_SN_PHOTORESISTOR_FIRST_MEASUREMENT_AT #define USERMOD_SN_PHOTORESISTOR_FIRST_MEASUREMENT_AT 10000 #endif // supplied voltage #ifndef USERMOD_SN_PHOTORESISTOR_REFERENCE_VOLTAGE #define USERMOD_SN_PHOTORESISTOR_REFERENCE_VOLTAGE 5 #endif // 10 bits #ifndef USERMOD_SN_PHOTORESISTOR_ADC_PRECISION #define USERMOD_SN_PHOTORESISTOR_ADC_PRECISION 1024.0f #endif // resistor size 10K hms #ifndef USERMOD_SN_PHOTORESISTOR_RESISTOR_VALUE #define USERMOD_SN_PHOTORESISTOR_RESISTOR_VALUE 10000.0f #endif // only report if difference grater than offset value #ifndef USERMOD_SN_PHOTORESISTOR_OFFSET_VALUE #define USERMOD_SN_PHOTORESISTOR_OFFSET_VALUE 5 #endif class Usermod_SN_Photoresistor : public Usermod { private: float referenceVoltage = USERMOD_SN_PHOTORESISTOR_REFERENCE_VOLTAGE; float resistorValue = USERMOD_SN_PHOTORESISTOR_RESISTOR_VALUE; float adcPrecision = USERMOD_SN_PHOTORESISTOR_ADC_PRECISION; int8_t offset = USERMOD_SN_PHOTORESISTOR_OFFSET_VALUE; unsigned long readingInterval = USERMOD_SN_PHOTORESISTOR_MEASUREMENT_INTERVAL; // set last reading as "40 sec before boot", so first reading is taken after 20 sec unsigned long lastMeasurement = UINT32_MAX - (USERMOD_SN_PHOTORESISTOR_MEASUREMENT_INTERVAL - USERMOD_SN_PHOTORESISTOR_FIRST_MEASUREMENT_AT); // flag to indicate we have finished the first getTemperature call // allows this library to report to the user how long until the first // measurement bool getLuminanceComplete = false; uint16_t lastLDRValue = 65535; // flag set at startup bool disabled = false; // strings to reduce flash memory usage (used more than twice) static const char _name[]; static const char _enabled[]; static const char _readInterval[]; static const char _referenceVoltage[]; static const char _resistorValue[]; static const char _adcPrecision[]; static const char _offset[]; uint16_t getLuminance(); public: void setup(); void loop(); uint16_t getLastLDRValue() { return lastLDRValue; } void addToJsonInfo(JsonObject &root); uint16_t getId() { return USERMOD_ID_SN_PHOTORESISTOR; } /** * addToConfig() (called from set.cpp) stores persistent properties to cfg.json */ void addToConfig(JsonObject &root); /** * readFromConfig() is called before setup() to populate properties from values stored in cfg.json */ bool readFromConfig(JsonObject &root); }; ================================================ FILE: usermods/SN_Photoresistor/library.json ================================================ { "name": "SN_Photoresistor", "build": { "libArchive": false } } ================================================ FILE: usermods/SN_Photoresistor/readme.md ================================================ # SN_Photoresistor usermod This usermod will read from an attached photoresistor sensor like the KY-018. The luminance is displayed in both the Info section of the web UI as well as published to the `/luminance` MQTT topic, if enabled. ## Installation Copy the example `platformio_override.ini` to the root directory. This file should be placed in the same directory as `platformio.ini`. ### Define Your Options * `USERMOD_SN_PHOTORESISTOR` - Enables this user mod. wled00\usermods_list.cpp * `USERMOD_SN_PHOTORESISTOR_MEASUREMENT_INTERVAL` - Number of milliseconds between measurements. Defaults to 60000 ms * `USERMOD_SN_PHOTORESISTOR_FIRST_MEASUREMENT_AT` - Number of milliseconds after boot to take first measurement. Defaults to 20000 ms * `USERMOD_SN_PHOTORESISTOR_REFERENCE_VOLTAGE` - Voltage supplied to the sensor. Defaults to 5v * `USERMOD_SN_PHOTORESISTOR_ADC_PRECISION` - ADC precision. Defaults to 10 bits * `USERMOD_SN_PHOTORESISTOR_RESISTOR_VALUE` - Resistor size, defaults to 10000.0 (10K Ohms) * `USERMOD_SN_PHOTORESISTOR_OFFSET_VALUE` - Offset value to report on. Defaults to 25 All parameters can be configured at runtime via the Usermods settings page. ## Project link * [QuinLED-Dig-Uno](https://quinled.info/2018/09/15/quinled-dig-uno/) - Project link ### PlatformIO requirements If you are using `platformio_override.ini`, you should be able to refresh the task list and see your custom task, for example `env:usermod_sn_photoresistor_d1_mini`. ## Change Log ================================================ FILE: usermods/ST7789_display/README.md ================================================ # Using the ST7789 TFT IPS 240x240 pixel color display with ESP32 boards This usermod enables display of the following: * Current date and time; * Network SSID; * IP address; * WiFi signal strength; * Brightness; * Selected effect; * Selected palette; * Effect speed and intensity; * Estimated current in mA; ## Hardware *** ![Hardware](images/ST7789_Guide.jpg) ## Library used [Bodmer/TFT_eSPI](https://github.com/Bodmer/TFT_eSPI) ## Setup *** ### Platformio.ini changes In the `platformio.ini` file, you must change the environment setup to build for just the esp32dev platform as follows: Add the following lines to section: ```ini default_envs = esp32dev build_flags = ${common.build_flags_esp32} -D USERMOD_ST7789_DISPLAY -DUSER_SETUP_LOADED=1 -DST7789_DRIVER=1 -DTFT_WIDTH=240 -DTFT_HEIGHT=240 -DCGRAM_OFFSET=1 -DTFT_MOSI=21 -DTFT_SCLK=22 -DTFT_DC=27 -DTFT_RST=26 -DTFT_BL=14 -DLOAD_GLCD=1 ;optional for WROVER ;-DCONFIG_SPIRAM_SUPPORT=1 ``` Save the `platformio.ini` file. Once saved, the required library files should be automatically downloaded for modifications in a later step. ### TFT_eSPI Library Adjustments If you are not using PlatformIO, you need to modify a file in the `TFT_eSPI` library. If you followed the directions to modify and save the `platformio.ini` file above, the `Setup24_ST7789.h` file can be found in the `/.pio/libdeps/esp32dev/TFT_eSPI/User_Setups/` folder. Edit `Setup_ST7789.h` file and uncomment and change GPIO pin numbers in lines containing `TFT_MOSI`, `TFT_SCLK`, `TFT_RST`, `TFT_DC`. Modify the `User_Setup_Select.h` by uncommenting the line containing `#include ` and commenting out the line containing `#include `. If your display uses the backlight enable pin, add this definition: #define TFT_BL with backlight enable GPIO number. ================================================ FILE: usermods/ST7789_display/ST7789_display.cpp ================================================ // Credits to @mrVanboy, @gwaland and my dearest friend @westward // Also for @spiff72 for usermod TTGO-T-Display // 210217 #include "wled.h" #include #include #ifndef USER_SETUP_LOADED #ifndef ST7789_DRIVER #error Please define ST7789_DRIVER #endif #ifndef TFT_WIDTH #error Please define TFT_WIDTH #endif #ifndef TFT_HEIGHT #error Please define TFT_HEIGHT #endif #ifndef TFT_DC #error Please define TFT_DC #endif #ifndef TFT_RST #error Please define TFT_RST #endif #ifndef TFT_CS #error Please define TFT_CS #endif #ifndef LOAD_GLCD #error Please define LOAD_GLCD #endif #endif #ifndef TFT_BL #define TFT_BL -1 #endif #define USERMOD_ID_ST7789_DISPLAY 97 TFT_eSPI tft = TFT_eSPI(TFT_WIDTH, TFT_HEIGHT); // Invoke custom library // Extra char (+1) for null #define LINE_BUFFER_SIZE 20 // How often we are redrawing screen #define USER_LOOP_REFRESH_RATE_MS 1000 extern int getSignalQuality(int rssi); //class name. Use something descriptive and leave the ": public Usermod" part :) class St7789DisplayUsermod : public Usermod { private: //Private class members. You can declare variables and functions only accessible to your usermod here unsigned long lastTime = 0; bool enabled = true; bool displayTurnedOff = false; long lastRedraw = 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 knownMode = 0; uint8_t knownPalette = 0; uint8_t knownEffectSpeed = 0; uint8_t knownEffectIntensity = 0; uint8_t knownMinute = 99; uint8_t knownHour = 99; const uint8_t tftcharwidth = 19; // Number of chars that fit on screen with text size set to 2 long lastUpdate = 0; void center(String &line, uint8_t width) { int len = line.length(); if (len0; i--) line = ' ' + line; for (byte i=line.length(); i 12) { showHour -= 12; isAM = false; } else { isAM = true; } } sprintf_P(lineBuffer, PSTR("%2d:%02d"), (useAMPM ? showHour : hourCurrent), minuteCurrent); tft.setTextColor(TFT_WHITE); tft.setTextSize(4); tft.setCursor(60, 24); tft.print(lineBuffer); tft.setTextSize(2); tft.setCursor(186, 24); //sprintf_P(lineBuffer, PSTR("%02d"), secondCurrent); if (useAMPM) tft.print(isAM ? "AM" : "PM"); //else tft.print(lineBuffer); } public: //Functions called by WLED /* * 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() override { PinManagerPinType spiPins[] = { { spi_mosi, true }, { spi_miso, false}, { spi_sclk, true } }; if (!PinManager::allocateMultiplePins(spiPins, 3, PinOwner::HW_SPI)) { enabled = false; return; } PinManagerPinType displayPins[] = { { TFT_CS, true}, { TFT_DC, true}, { TFT_RST, true }, { TFT_BL, true } }; if (!PinManager::allocateMultiplePins(displayPins, sizeof(displayPins)/sizeof(PinManagerPinType), PinOwner::UM_FourLineDisplay)) { PinManager::deallocateMultiplePins(spiPins, 3, PinOwner::HW_SPI); enabled = false; return; } tft.init(); tft.setRotation(0); //Rotation here is set up for the text to be readable with the port on the left. Use 1 to flip. tft.fillScreen(TFT_BLACK); tft.setTextColor(TFT_RED); tft.setCursor(60, 100); tft.setTextDatum(MC_DATUM); tft.setTextSize(2); tft.print("Loading..."); if (TFT_BL >= 0) { pinMode(TFT_BL, OUTPUT); // Set backlight pin to output mode digitalWrite(TFT_BL, HIGH); // Turn backlight on. } } /* * connected() is called every time the WiFi is (re)connected * Use it to initialize network interfaces */ void connected() override { //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() override { char buff[LINE_BUFFER_SIZE]; // Check if we time interval for redrawing passes. if (millis() - lastUpdate < USER_LOOP_REFRESH_RATE_MS) { return; } lastUpdate = millis(); // Turn off display after 5 minutes with no change. if (!displayTurnedOff && millis() - lastRedraw > 5*60*1000) { if (TFT_BL >= 0) digitalWrite(TFT_BL, LOW); // Turn backlight off. displayTurnedOff = true; } // Check if values which are shown on display changed from the last time. if ((((apActive) ? String(apSSID) : WiFi.SSID()) != knownSsid) || (knownIp != (apActive ? IPAddress(4, 3, 2, 1) : Network.localIP())) || (knownBrightness != bri) || (knownEffectSpeed != strip.getMainSegment().speed) || (knownEffectIntensity != strip.getMainSegment().intensity) || (knownMode != strip.getMainSegment().mode) || (knownPalette != strip.getMainSegment().palette)) { needRedraw = true; } if (!needRedraw) { return; } needRedraw = false; if (displayTurnedOff) { digitalWrite(TFT_BL, HIGH); // Turn backlight on. displayTurnedOff = false; } lastRedraw = millis(); // 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.getMainSegment().mode; knownPalette = strip.getMainSegment().palette; knownEffectSpeed = strip.getMainSegment().speed; knownEffectIntensity = strip.getMainSegment().intensity; tft.fillScreen(TFT_BLACK); showTime(); tft.setTextSize(2); // Wifi name tft.setTextColor(TFT_GREEN); tft.setCursor(0, 60); String line = knownSsid.substring(0, tftcharwidth-1); // Print `~` char to indicate that SSID is longer, than our display if (knownSsid.length() > tftcharwidth) line = line.substring(0, tftcharwidth-1) + '~'; center(line, tftcharwidth); tft.print(line.c_str()); // Print AP IP and password in AP mode or knownIP if AP not active. if (apActive) { tft.setCursor(0, 84); tft.print("AP IP: "); tft.print(knownIp); tft.setCursor(0,108); tft.print("AP Pass:"); tft.print(apPass); } else { tft.setCursor(0, 84); line = knownIp.toString(); center(line, tftcharwidth); tft.print(line.c_str()); // percent brightness tft.setCursor(0, 120); tft.setTextColor(TFT_WHITE); tft.print("Bri: "); tft.print((((int)bri*100)/255)); tft.print("%"); // signal quality tft.setCursor(124,120); tft.print("Sig: "); if (getSignalQuality(WiFi.RSSI()) < 10) { tft.setTextColor(TFT_RED); } else if (getSignalQuality(WiFi.RSSI()) < 25) { tft.setTextColor(TFT_ORANGE); } else { tft.setTextColor(TFT_GREEN); } tft.print(getSignalQuality(WiFi.RSSI())); tft.setTextColor(TFT_WHITE); tft.print("%"); } // mode name tft.setTextColor(TFT_CYAN); tft.setCursor(0, 144); char lineBuffer[tftcharwidth+1]; extractModeName(knownMode, JSON_mode_names, lineBuffer, tftcharwidth); tft.print(lineBuffer); // palette name tft.setTextColor(TFT_YELLOW); tft.setCursor(0, 168); extractModeName(knownPalette, JSON_palette_names, lineBuffer, tftcharwidth); tft.print(lineBuffer); tft.setCursor(0, 192); tft.setTextColor(TFT_SILVER); sprintf_P(buff, PSTR("FX Spd:%3d Int:%3d"), effectSpeed, effectIntensity); tft.print(buff); // Fifth row with estimated mA usage tft.setTextColor(TFT_SILVER); tft.setCursor(0, 216); // Print estimated milliamp usage (must specify the LED type in LED prefs for this to be a reasonable estimate). tft.print("Current: "); tft.setTextColor(TFT_ORANGE); tft.print(BusManager::currentMilliamps()); tft.print("mA"); } /* * 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) override { JsonObject user = root["u"]; if (user.isNull()) user = root.createNestedObject("u"); JsonArray lightArr = user.createNestedArray("ST7789"); //name lightArr.add(enabled?F("installed"):F("disabled")); //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) override { //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) override { //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!")); } /* * 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) override { JsonObject top = root.createNestedObject("ST7789"); JsonArray pins = top.createNestedArray("pin"); pins.add(TFT_CS); pins.add(TFT_DC); pins.add(TFT_RST); pins.add(TFT_BL); //top["great"] = userVar0; //save this var persistently whenever settings are saved } void appendConfigData() override { oappend(F("addInfo('ST7789:pin[]',0,'','SPI CS');")); oappend(F("addInfo('ST7789:pin[]',1,'','SPI DC');")); oappend(F("addInfo('ST7789:pin[]',2,'','SPI RST');")); oappend(F("addInfo('ST7789:pin[]',3,'','SPI BL');")); } /* * 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 :) */ bool readFromConfig(JsonObject& root) override { //JsonObject top = root["top"]; //userVar0 = top["great"] | 42; //The value right of the pipe "|" is the default value in case your setting was not present in cfg.json (e.g. first boot) return true; } /* * 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() override { return USERMOD_ID_ST7789_DISPLAY; } //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! }; static name. st7789_display; REGISTER_USERMOD(st7789_display); ================================================ FILE: usermods/ST7789_display/library.json.disabled ================================================ { "name:": "ST7789_display", "build": { "libArchive": false } } ================================================ FILE: usermods/Si7021_MQTT_HA/Si7021_MQTT_HA.cpp ================================================ // this is remixed from usermod_v2_SensorsToMqtt.h (sensors_to_mqtt usermod) // and usermod_multi_relay.h (multi_relay usermod) #include "wled.h" #include #include // EnvironmentCalculations::HeatIndex(), ::DewPoint(), ::AbsoluteHumidity() #ifdef WLED_DISABLE_MQTT #error "This user mod requires MQTT to be enabled." #endif static Adafruit_Si7021 si7021; class Si7021_MQTT_HA : public Usermod { private: bool sensorInitialized = false; bool mqttInitialized = false; float sensorTemperature = 0; float sensorHumidity = 0; float sensorHeatIndex = 0; float sensorDewPoint = 0; float sensorAbsoluteHumidity= 0; String mqttTemperatureTopic = ""; String mqttHumidityTopic = ""; String mqttHeatIndexTopic = ""; String mqttDewPointTopic = ""; String mqttAbsoluteHumidityTopic = ""; unsigned long nextMeasure = 0; bool enabled = false; bool haAutoDiscovery = true; bool sendAdditionalSensors = true; // strings to reduce flash memory usage (used more than twice) static const char _name[]; static const char _enabled[]; static const char _sendAdditionalSensors[]; static const char _haAutoDiscovery[]; void _initializeSensor() { sensorInitialized = si7021.begin(); Serial.printf("Si7021_MQTT_HA: sensorInitialized = %d\n", sensorInitialized); } void _initializeMqtt() { mqttTemperatureTopic = String(mqttDeviceTopic) + "/si7021_temperature"; mqttHumidityTopic = String(mqttDeviceTopic) + "/si7021_humidity"; mqttHeatIndexTopic = String(mqttDeviceTopic) + "/si7021_heat_index"; mqttDewPointTopic = String(mqttDeviceTopic) + "/si7021_dew_point"; mqttAbsoluteHumidityTopic = String(mqttDeviceTopic) + "/si7021_absolute_humidity"; // Update and publish sensor data _updateSensorData(); _publishSensorData(); if (haAutoDiscovery) { _publishHAMqttSensor("temperature", "Temperature", mqttTemperatureTopic, "temperature", "°C"); _publishHAMqttSensor("humidity", "Humidity", mqttHumidityTopic, "humidity", "%"); if (sendAdditionalSensors) { _publishHAMqttSensor("heat_index", "Heat Index", mqttHeatIndexTopic, "temperature", "°C"); _publishHAMqttSensor("dew_point", "Dew Point", mqttDewPointTopic, "", "°C"); _publishHAMqttSensor("absolute_humidity", "Absolute Humidity", mqttAbsoluteHumidityTopic, "", "g/m³"); } } mqttInitialized = true; } void _publishHAMqttSensor( const String &name, const String &friendly_name, const String &state_topic, const String &deviceClass, const String &unitOfMeasurement) { if (WLED_MQTT_CONNECTED) { String topic = String("homeassistant/sensor/") + mqttClientID + "/" + name + "/config"; StaticJsonDocument<300> doc; doc["name"] = String(serverDescription) + " " + friendly_name; doc["state_topic"] = state_topic; doc["unique_id"] = String(mqttClientID) + name; if (unitOfMeasurement != "") doc["unit_of_measurement"] = unitOfMeasurement; if (deviceClass != "") doc["device_class"] = deviceClass; doc["expire_after"] = 1800; JsonObject device = doc.createNestedObject("device"); // attach the sensor to the same device device["name"] = String(serverDescription); device["model"] = F(WLED_PRODUCT_NAME); device["manufacturer"] = F(WLED_BRAND); device["identifiers"] = String("wled-") + String(serverDescription); device["sw_version"] = VERSION; String payload; serializeJson(doc, payload); mqtt->publish(topic.c_str(), 0, true, payload.c_str()); } } void _updateSensorData() { sensorTemperature = si7021.readTemperature(); sensorHumidity = si7021.readHumidity(); // Serial.print("Si7021_MQTT_HA: Temperature: "); // Serial.print(sensorTemperature, 2); // Serial.print("\tHumidity: "); // Serial.print(sensorHumidity, 2); if (sendAdditionalSensors) { EnvironmentCalculations::TempUnit envTempUnit(EnvironmentCalculations::TempUnit_Celsius); sensorHeatIndex = EnvironmentCalculations::HeatIndex(sensorTemperature, sensorHumidity, envTempUnit); sensorDewPoint = EnvironmentCalculations::DewPoint(sensorTemperature, sensorHumidity, envTempUnit); sensorAbsoluteHumidity = EnvironmentCalculations::AbsoluteHumidity(sensorTemperature, sensorHumidity, envTempUnit); // Serial.print("\tHeat Index: "); // Serial.print(sensorHeatIndex, 2); // Serial.print("\tDew Point: "); // Serial.print(sensorDewPoint, 2); // Serial.print("\tAbsolute Humidity: "); // Serial.println(sensorAbsoluteHumidity, 2); } // else // Serial.println(""); } void _publishSensorData() { if (WLED_MQTT_CONNECTED) { mqtt->publish(mqttTemperatureTopic.c_str(), 0, false, String(sensorTemperature).c_str()); mqtt->publish(mqttHumidityTopic.c_str(), 0, false, String(sensorHumidity).c_str()); if (sendAdditionalSensors) { mqtt->publish(mqttHeatIndexTopic.c_str(), 0, false, String(sensorHeatIndex).c_str()); mqtt->publish(mqttDewPointTopic.c_str(), 0, false, String(sensorDewPoint).c_str()); mqtt->publish(mqttAbsoluteHumidityTopic.c_str(), 0, false, String(sensorAbsoluteHumidity).c_str()); } } } public: void addToConfig(JsonObject& root) { JsonObject top = root.createNestedObject(FPSTR(_name)); top[FPSTR(_enabled)] = enabled; top[FPSTR(_sendAdditionalSensors)] = sendAdditionalSensors; top[FPSTR(_haAutoDiscovery)] = haAutoDiscovery; } bool readFromConfig(JsonObject& root) { JsonObject top = root[FPSTR(_name)]; bool configComplete = !top.isNull(); configComplete &= getJsonValue(top[FPSTR(_enabled)], enabled); configComplete &= getJsonValue(top[FPSTR(_sendAdditionalSensors)], sendAdditionalSensors); configComplete &= getJsonValue(top[FPSTR(_haAutoDiscovery)], haAutoDiscovery); return configComplete; } void onMqttConnect(bool sessionPresent) { if (mqttDeviceTopic[0] != 0) _initializeMqtt(); } void setup() { if (enabled) { Serial.println("Si7021_MQTT_HA: Starting!"); Serial.println("Si7021_MQTT_HA: Initializing sensors.. "); _initializeSensor(); } } // gets called every time WiFi is (re-)connected. void connected() { nextMeasure = millis() + 5000; // Schedule next measure in 5 seconds } void loop() { yield(); if (!enabled || strip.isUpdating()) return; // !sensorFound || unsigned long tempTimer = millis(); if (tempTimer > nextMeasure) { nextMeasure = tempTimer + 60000; // Schedule next measure in 60 seconds if (!sensorInitialized) { Serial.println("Si7021_MQTT_HA: Error! Sensors not initialized in loop()!"); _initializeSensor(); return; // lets try again next loop } if (WLED_MQTT_CONNECTED) { if (!mqttInitialized) _initializeMqtt(); // Update and publish sensor data _updateSensorData(); _publishSensorData(); } else { Serial.println("Si7021_MQTT_HA: Missing MQTT connection. Not publishing data"); mqttInitialized = false; } } } uint16_t getId() { return USERMOD_ID_SI7021_MQTT_HA; } }; // strings to reduce flash memory usage (used more than twice) const char Si7021_MQTT_HA::_name[] PROGMEM = "Si7021 MQTT (Home Assistant)"; const char Si7021_MQTT_HA::_enabled[] PROGMEM = "enabled"; const char Si7021_MQTT_HA::_sendAdditionalSensors[] PROGMEM = "Send Dew Point, Abs. Humidity and Heat Index"; const char Si7021_MQTT_HA::_haAutoDiscovery[] PROGMEM = "Home Assistant MQTT Auto-Discovery"; static Si7021_MQTT_HA si7021_mqtt_ha; REGISTER_USERMOD(si7021_mqtt_ha); ================================================ FILE: usermods/Si7021_MQTT_HA/library.json ================================================ { "name": "Si7021_MQTT_HA", "build": { "libArchive": false }, "dependencies": { "finitespace/BME280":"3.0.0", "adafruit/Adafruit Si7021 Library" : "1.5.3", "SPI":"*", "adafruit/Adafruit BusIO": "1.17.1" } } ================================================ FILE: usermods/Si7021_MQTT_HA/readme.md ================================================ # Si7021 to MQTT (with Home Assistant Auto Discovery) usermod This usermod implements support for [Si7021 I²C temperature and humidity sensors](https://www.silabs.com/documents/public/data-sheets/Si7021-A20.pdf). As of this writing, the sensor data will *not* be shown on the WLED UI, but it _is_ published via MQTT to WLED's "built-in" MQTT device topic. ``` temperature: $mqttDeviceTopic/si7021_temperature humidity: $mqttDeviceTopic/si7021_humidity ``` The following sensors can also be published: ``` heat_index: $mqttDeviceTopic/si7021_heat_index dew_point: $mqttDeviceTopic/si7021_dew_point absolute_humidity: $mqttDeviceTopic/si7021_absolute_humidity ``` Sensor data will be updated/sent every 60 seconds. This usermod also supports Home Assistant Auto Discovery. ## Settings via Usermod Setup - `enabled`: Enables this usermod - `Send Dew Point, Abs. Humidity and Heat Index`: Enables additional sensors - `Home Assistant MQTT Auto-Discovery`: Enables Home Assistant Auto Discovery # Installation ## Hardware Attach the Si7021 sensor to the I²C interface. Default PINs ESP32: ``` SCL_PIN = 22; SDA_PIN = 21; ``` Default PINs ESP8266: ``` SCL_PIN = 5; SDA_PIN = 4; ``` ## Software Add `Si7021_MQTT_HA` to custom_usermods # Credits - Aircoookie for making WLED - Other usermod creators for example code (`sensors_to_mqtt` and `multi_relay` especially) - You, for reading this ================================================ FILE: usermods/TTGO-T-Display/README.md ================================================ # TTGO T-Display ESP32 with 240x135 TFT via SPI with TFT_eSPI This usermod enables use of the TTGO 240x135 T-Display ESP32 module for controlling WLED and showing the following information: * Current SSID * IP address, if obtained * If connected to a network, current brightness percentage is shown * In AP mode, AP, IP and password are shown * Current effect * Current palette * Estimated current in mA (NOTE: for this to be a reasonable value, the correct LED type must be specified in the LED Prefs section) Button pin is mapped to the onboard button adjacent to the reset button of the TTGO T-Display board. I have designed a 3D printed case around this board and an ["ElectroCookie"](https://amzn.to/2WCNeeA) project board, a [level shifter](https://amzn.to/3hbKu18), a [buck regulator](https://amzn.to/3mLMy0W), and a DC [power jack](https://amzn.to/3phj9NZ). I use 12V WS2815 LED strips for my projects, and power them with 12V power supplies. The regulator supplies 5V for the ESP module and the level shifter. If there is any interest in this case which elevates the board and display on custom extended standoffs to place the screen at the top of the enclosure (with accessible buttons), let me know, and I will post the STL files. It is a bit tricky to get the height correct, so I also designed a one-time use 3D printed solder fixture to set the board in the right location and at the correct height for the housing. (It is one-time use because it has to be cut off after soldering to be able to remove it). I didn't think the effort to make it in multiple pieces was worthwhile. Based on a rework of the ssd1306_i2c_oled_u8g2 usermod from the WLED repo. ## Hardware ![Hardware](assets/ttgo_hardware1.png) ![Hardware](assets/ttgo-tdisplay-enclosure1a.png) ![Hardware](assets/ttgo-tdisplay-enclosure2a.png) ![Hardware](assets/ttgo-tdisplay-enclosure3a.png) ![Hardware](assets/ttgo-tdisplay-enclosure3a.png) ## Github reference for TTGO-Tdisplay * [TTGO T-Display](https://github.com/Xinyuan-LilyGO/TTGO-T-Display) ## Requirements Functionality checked with: * TTGO T-Display * PlatformIO * Group of 4 individual Neopixels from Adafruit and several full strings of 12v WS2815 LEDs. * The hardware design shown above should be limited to shorter strings. For larger strings, I use a different setup with a dedicated 12v power supply and power them directly from said supply (in addition to dropping the 12v to 5v with a buck regulator for the ESP module and level shifter). ## Setup Needed: * As with all usermods, copy the usermod.cpp file from the TTGO-T-Display usermod folder to the wled00 folder (replacing the default usermod.cpp file). ## Platformio Requirements ### Platformio.ini changes Under the root folder of the project, in the `platformio.ini` file, uncomment the `TFT_eSPI` line within the [common] section, under `lib_deps`: ```ini # platformio.ini ... [common] ... lib_deps = ... #For use of the TTGO T-Display ESP32 Module with integrated TFT display uncomment the following line #TFT_eSPI ... ``` In the `platformio.ini` file, you must change the environment setup to build for just the esp32dev platform as follows: Comment out the line described below: ```ini # Release binaries ; default_envs = nodemcuv2, esp8266_2m, esp01_1m_full, esp32dev, esp32_eth, esp32s2_saola, esp32c3 ``` and uncomment the following line in the 'Single binaries' section: ```ini default_envs = esp32dev ``` Save the `platformio.ini` file. Once saved, the required library files should be automatically downloaded for modifications in a later step. ### Platformio_overrides.ini (added) Copy the `platformio_overrides.ini` file which is contained in the `usermods/TTGO-T-Display/` folder into the root of your project folder. This file contains an override that remaps the button pin of WLED to use the on-board button to the right of the USB-C connector (when viewed with the port oriented downward - see hardware photo). ### TFT_eSPI Library Adjustments (board selection) You need to modify a file in the `TFT_eSPI` library to select the correct board. If you followed the directions to modify and save the `platformio.ini` file above, the `User_Setup_Select.h` file can be found in the `/.pio/libdeps/esp32dev/TFT_eSPI_ID1559` folder. Modify the `User_Setup_Select.h` file as follows: * Comment out the following line (which is the 'default' setup file): ```ini //#include // Default setup is root library folder ``` * Uncomment the following line (which points to the setup file for the TTGO T-Display): ```ini #include // Setup file for ESP32 and TTGO T-Display ST7789V SPI bus TFT ``` Build the file. If you see a failure like this: ```ini xtensa-esp32-elf-g++: error: wled00\wled00.ino.cpp: No such file or directory xtensa-esp32-elf-g++: fatal error: no input files ``` try building again. Sometimes this happens on the first build attempt and subsequent attempts build correctly. ## Arduino IDE - UNTESTED ================================================ FILE: usermods/TTGO-T-Display/usermod.cpp ================================================ /* * This file allows you to add own functionality to WLED more easily * See: https://github.com/wled-dev/WLED/wiki/Add-own-functionality * EEPROM bytes 2750+ are reserved for your custom use case. (if you extend #define EEPSIZE in const.h) * bytes 2400+ are currently unused, but might be used for future wled features */ /* * Pin 2 of the TTGO T-Display serves as the data line for the LED string. * Pin 35 is set up as the button pin in the platformio_overrides.ini file. * The button can be set up via the macros section in the web interface. * I use the button to cycle between presets. * The Pin 35 button is the one on the RIGHT side of the USB-C port on the board, * when the port is oriented downwards. See readme.md file for photo. * The display is set up to turn off after 5 minutes, and turns on automatically * when a change in the dipslayed info is detected (within a 5 second interval). */ //Use userVar0 and userVar1 (API calls &U0=,&U1=, uint16_t) #include "wled.h" #include #include #include "WiFi.h" #include #ifndef TFT_DISPOFF #define TFT_DISPOFF 0x28 #endif #ifndef TFT_SLPIN #define TFT_SLPIN 0x10 #endif #define TFT_MOSI 19 #define TFT_SCLK 18 #define TFT_CS 5 #define TFT_DC 16 #define TFT_RST 23 #define TFT_BL 4 // Display backlight control pin #define ADC_EN 14 // Used for enabling battery voltage measurements - not used in this program TFT_eSPI tft = TFT_eSPI(135, 240); // Invoke custom library //gets called once at boot. Do all initialization that doesn't depend on network here void userSetup() { Serial.begin(115200); Serial.println("Start"); tft.init(); tft.setRotation(3); //Rotation here is set up for the text to be readable with the port on the left. Use 1 to flip. tft.fillScreen(TFT_BLACK); tft.setTextSize(2); tft.setTextColor(TFT_WHITE); tft.setCursor(1, 10); tft.setTextDatum(MC_DATUM); tft.setTextSize(3); tft.print("Loading..."); if (TFT_BL > 0) { // TFT_BL has been set in the TFT_eSPI library in the User Setup file TTGO_T_Display.h pinMode(TFT_BL, OUTPUT); // Set backlight pin to output mode digitalWrite(TFT_BL, HIGH); // Turn backlight on. } // tft.setRotation(3); } // gets called every time WiFi is (re-)connected. Initialize own network // interfaces here void userConnected() {} // 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 knownMode = 0; uint8_t knownPalette = 0; uint8_t tftcharwidth = 19; // Number of chars that fit on screen with text size set to 2 long lastUpdate = 0; long lastRedraw = 0; bool displayTurnedOff = false; // How often we are redrawing screen #define USER_LOOP_REFRESH_RATE_MS 5000 void userLoop() { // Check if we time interval for redrawing passes. if (millis() - lastUpdate < USER_LOOP_REFRESH_RATE_MS) { return; } lastUpdate = millis(); // Turn off display after 5 minutes with no change. if(!displayTurnedOff && millis() - lastRedraw > 5*60*1000) { digitalWrite(TFT_BL, LOW); // Turn backlight off. displayTurnedOff = true; } // Check if values which are shown on display changed from the last time. 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 (knownMode != strip.getMainSegment().mode) { needRedraw = true; } else if (knownPalette != strip.getMainSegment().palette) { needRedraw = true; } if (!needRedraw) { return; } needRedraw = false; if (displayTurnedOff) { digitalWrite(TFT_BL, TFT_BACKLIGHT_ON); // Turn backlight on. displayTurnedOff = false; } lastRedraw = millis(); // 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.getMainSegment().mode; knownPalette = strip.getMainSegment().palette; tft.fillScreen(TFT_BLACK); tft.setTextSize(2); // First row with Wifi name tft.setCursor(1, 1); tft.print(knownSsid.substring(0, tftcharwidth > 1 ? tftcharwidth - 1 : 0)); // Print `~` char to indicate that SSID is longer than our display if (knownSsid.length() > tftcharwidth) tft.print("~"); // Second row with AP IP and Password or IP tft.setTextSize(2); tft.setCursor(1, 24); // Print AP IP and password in AP mode or knownIP if AP not active. // if (apActive && bri == 0) // tft.print(apPass); // else // tft.print(knownIp); if (apActive) { tft.print("AP IP: "); tft.print(knownIp); tft.setCursor(1,46); tft.print("AP Pass:"); tft.print(apPass); } else { tft.print("IP: "); tft.print(knownIp); tft.setCursor(1,46); //tft.print("Signal Strength: "); //tft.print(i.wifi.signal); tft.print("Brightness: "); tft.print(((float(bri)/255)*100)); tft.print("%"); } // Third row with mode name tft.setCursor(1, 68); char lineBuffer[tftcharwidth+1]; extractModeName(knownMode, JSON_mode_names, lineBuffer, tftcharwidth); tft.print(lineBuffer); // Fourth row with palette name tft.setCursor(1, 90); extractModeName(knownPalette, JSON_palette_names, lineBuffer, tftcharwidth); tft.print(lineBuffer); // Fifth row with estimated mA usage tft.setCursor(1, 112); // Print estimated milliamp usage (must specify the LED type in LED prefs for this to be a reasonable estimate). tft.print(strip.currentMilliamps); tft.print("mA (estimated)"); } ================================================ FILE: usermods/Temperature/Temperature.cpp ================================================ #include "UsermodTemperature.h" static void mode_temperature(); //Dallas sensor quick (& dirty) reading. Credit to - Author: Peter Scargill, August 17th, 2013 float UsermodTemperature::readDallas() { byte data[9]; int16_t result; // raw data from sensor float retVal = -127.0f; if (oneWire->reset()) { // if reset() fails there are no OneWire devices oneWire->skip(); // skip ROM oneWire->write(0xBE); // read (temperature) from EEPROM oneWire->read_bytes(data, 9); // first 2 bytes contain temperature #ifdef WLED_DEBUG if (OneWire::crc8(data,8) != data[8]) { DEBUG_PRINTLN(F("CRC error reading temperature.")); for (unsigned i=0; i < 9; i++) DEBUG_PRINTF_P(PSTR("0x%02X "), data[i]); DEBUG_PRINT(F(" => ")); DEBUG_PRINTF_P(PSTR("0x%02X\n"), OneWire::crc8(data,8)); } #endif switch(sensorFound) { case 0x10: // DS18S20 has 9-bit precision - 1-bit fraction part result = (data[1] << 8) | data[0]; retVal = float(result) * 0.5f; break; case 0x22: // DS1822 case 0x28: // DS18B20 case 0x3B: // DS1825 case 0x42: // DS28EA00 // 12-bit precision - 4-bit fraction part result = (data[1] << 8) | data[0]; // Clear LSBs to match desired precision (9/10/11-bit) rounding towards negative infinity result &= 0xFFFF << (3 - (resolution & 3)); retVal = float(result) * 0.0625f; // 2^(-4) break; } } for (unsigned i=1; i<9; i++) data[0] &= data[i]; return data[0]==0xFF ? -127.0f : retVal; } void UsermodTemperature::requestTemperatures() { DEBUG_PRINTLN(F("Requesting temperature.")); oneWire->reset(); oneWire->skip(); // skip ROM oneWire->write(0x44,parasite); // request new temperature reading if (parasite && parasitePin >=0 ) digitalWrite(parasitePin, HIGH); // has to happen within 10us (open MOSFET) lastTemperaturesRequest = millis(); waitingForConversion = true; } void UsermodTemperature::readTemperature() { if (parasite && parasitePin >=0 ) digitalWrite(parasitePin, LOW); // deactivate power (close MOSFET) temperature = readDallas(); lastMeasurement = millis(); waitingForConversion = false; //DEBUG_PRINTF_P(PSTR("Read temperature %2.1f.\n"), temperature); // does not work properly on 8266 DEBUG_PRINT(F("Read temperature ")); DEBUG_PRINTLN(temperature); } bool UsermodTemperature::findSensor() { DEBUG_PRINTLN(F("Searching for sensor...")); uint8_t deviceAddress[8] = {0,0,0,0,0,0,0,0}; // find out if we have DS18xxx sensor attached oneWire->reset_search(); delay(10); while (oneWire->search(deviceAddress)) { DEBUG_PRINTLN(F("Found something...")); if (oneWire->crc8(deviceAddress, 7) == deviceAddress[7]) { switch (deviceAddress[0]) { case 0x10: // DS18S20 case 0x22: // DS1822 case 0x28: // DS18B20 case 0x3B: // DS1825 case 0x42: // DS28EA00 DEBUG_PRINTLN(F("Sensor found.")); sensorFound = deviceAddress[0]; DEBUG_PRINTF_P(PSTR("0x%02X\n"), sensorFound); return true; } } } DEBUG_PRINTLN(F("Sensor NOT found.")); return false; } #ifndef WLED_DISABLE_MQTT void UsermodTemperature::publishHomeAssistantAutodiscovery() { if (!WLED_MQTT_CONNECTED) return; char json_str[1024], buf[128]; size_t payload_size; StaticJsonDocument<1024> json; sprintf_P(buf, PSTR("%s Temperature"), serverDescription); json[F("name")] = buf; strcpy(buf, mqttDeviceTopic); strcat_P(buf, _Temperature); json[F("state_topic")] = buf; json[F("device_class")] = FPSTR(_temperature); json[F("unique_id")] = escapedMac.c_str(); json[F("unit_of_measurement")] = F("°C"); payload_size = serializeJson(json, json_str); sprintf_P(buf, PSTR("homeassistant/sensor/%s/config"), escapedMac.c_str()); mqtt->publish(buf, 0, true, json_str, payload_size); HApublished = true; } #endif void UsermodTemperature::setup() { int retries = 10; sensorFound = 0; temperature = -127.0f; // default to -127, DS18B20 only goes down to -50C if (enabled) { // config says we are enabled DEBUG_PRINTLN(F("Allocating temperature pin...")); // pin retrieved from cfg.json (readFromConfig()) prior to running setup() if (temperaturePin >= 0 && PinManager::allocatePin(temperaturePin, true, PinOwner::UM_Temperature)) { oneWire = new OneWire(temperaturePin); if (oneWire->reset()) { while (!findSensor() && retries--) { delay(25); // try to find sensor } } if (parasite && PinManager::allocatePin(parasitePin, true, PinOwner::UM_Temperature)) { pinMode(parasitePin, OUTPUT); digitalWrite(parasitePin, LOW); // deactivate power (close MOSFET) } else { parasitePin = -1; } } else { if (temperaturePin >= 0) { DEBUG_PRINTLN(F("Temperature pin allocation failed.")); } temperaturePin = -1; // allocation failed } if (sensorFound && !initDone) strip.addEffect(255, &mode_temperature, _data_fx); } lastMeasurement = millis() - readingInterval + 10000; initDone = true; } void UsermodTemperature::loop() { if (!enabled || !sensorFound || strip.isUpdating()) return; static uint8_t errorCount = 0; unsigned long now = millis(); // check to see if we are due for taking a measurement // lastMeasurement will not be updated until the conversion // is complete the the reading is finished if (now - lastMeasurement < readingInterval) return; // we are due for a measurement, if we are not already waiting // for a conversion to complete, then make a new request for temps if (!waitingForConversion) { requestTemperatures(); return; } // we were waiting for a conversion to complete, have we waited log enough? if (now - lastTemperaturesRequest >= 750 /* 93.75ms per the datasheet but can be up to 750ms */) { readTemperature(); if (getTemperatureC() < -100.0f) { if (++errorCount > 10) sensorFound = 0; lastMeasurement = now - readingInterval + 300; // force new measurement in 300ms return; } errorCount = 0; #ifndef WLED_DISABLE_MQTT if (WLED_MQTT_CONNECTED) { char subuf[128]; strcpy(subuf, mqttDeviceTopic); if (temperature > -100.0f) { // dont publish super low temperature as the graph will get messed up // the DallasTemperature library returns -127C or -196.6F when problem // reading the sensor strcat_P(subuf, _Temperature); mqtt->publish(subuf, 0, false, String(getTemperatureC()).c_str()); strcat_P(subuf, PSTR("_f")); mqtt->publish(subuf, 0, false, String(getTemperatureF()).c_str()); if (idx > 0) { StaticJsonDocument <128> msg; msg[F("idx")] = idx; msg[F("RSSI")] = WiFi.RSSI(); msg[F("nvalue")] = 0; msg[F("svalue")] = String(getTemperatureC()); serializeJson(msg, subuf, 127); mqtt->publish("domoticz/in", 0, false, subuf); } } else { // publish something else to indicate status? } } #endif } } /** * connected() is called every time the WiFi is (re)connected * Use it to initialize network interfaces */ //void UsermodTemperature::connected() {} #ifndef WLED_DISABLE_MQTT /** * subscribe to MQTT topic if needed */ void UsermodTemperature::onMqttConnect(bool sessionPresent) { //(re)subscribe to required topics //char subuf[64]; if (mqttDeviceTopic[0] != 0) { publishHomeAssistantAutodiscovery(); } } #endif /* * 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 UsermodTemperature::addToJsonInfo(JsonObject& root) { // dont add temperature to info if we are disabled if (!enabled) return; JsonObject user = root["u"]; if (user.isNull()) user = root.createNestedObject("u"); JsonArray temp = user.createNestedArray(FPSTR(_name)); if (temperature <= -100.0f) { temp.add(0); temp.add(F(" Sensor Error!")); return; } temp.add(getTemperature()); temp.add(getTemperatureUnit()); JsonObject sensor = root[FPSTR(_sensor)]; if (sensor.isNull()) sensor = root.createNestedObject(FPSTR(_sensor)); temp = sensor.createNestedArray(FPSTR(_temperature)); temp.add(getTemperature()); temp.add(getTemperatureUnit()); } /** * 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 UsermodTemperature::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 * Read "_" from json state and and change settings (i.e. GPIO pin) used. */ //void UsermodTemperature::readFromJsonState(JsonObject &root) { // if (!initDone) return; // prevent crash on boot applyPreset() //} /** * addToConfig() (called from set.cpp) stores persistent properties to cfg.json */ void UsermodTemperature::addToConfig(JsonObject &root) { // we add JSON object: {"Temperature": {"pin": 0, "degC": true}} JsonObject top = root.createNestedObject(FPSTR(_name)); // usermodname top[FPSTR(_enabled)] = enabled; top["pin"] = temperaturePin; // usermodparam top[F("degC")] = degC; // usermodparam top[FPSTR(_readInterval)] = readingInterval / 1000; top[FPSTR(_parasite)] = parasite; top[FPSTR(_parasitePin)] = parasitePin; top[FPSTR(_domoticzIDX)] = idx; top[FPSTR(_resolution)] = resolution; DEBUG_PRINTLN(F("Temperature config saved.")); } /** * readFromConfig() is called before setup() to populate properties from values stored in cfg.json * * The function should return true if configuration was successfully loaded or false if there was no configuration. */ bool UsermodTemperature::readFromConfig(JsonObject &root) { // we look for JSON object: {"Temperature": {"pin": 0, "degC": true}} int8_t newTemperaturePin = temperaturePin; DEBUG_PRINT(FPSTR(_name)); JsonObject top = root[FPSTR(_name)]; if (top.isNull()) { DEBUG_PRINTLN(F(": No config found. (Using defaults.)")); return false; } enabled = top[FPSTR(_enabled)] | enabled; newTemperaturePin = top["pin"] | newTemperaturePin; degC = top[F("degC")] | degC; readingInterval = top[FPSTR(_readInterval)] | readingInterval/1000; readingInterval = min(120,max(10,(int)readingInterval)) * 1000; // convert to ms parasite = top[FPSTR(_parasite)] | parasite; parasitePin = top[FPSTR(_parasitePin)] | parasitePin; idx = top[FPSTR(_domoticzIDX)] | idx; resolution = top[FPSTR(_resolution)] | resolution; if (!initDone) { // first run: reading from cfg.json temperaturePin = newTemperaturePin; DEBUG_PRINTLN(F(" config loaded.")); } else { DEBUG_PRINTLN(F(" config (re)loaded.")); // changing paramters from settings page if (newTemperaturePin != temperaturePin) { DEBUG_PRINTLN(F("Re-init temperature.")); // deallocate pin and release memory delete oneWire; PinManager::deallocatePin(temperaturePin, PinOwner::UM_Temperature); temperaturePin = newTemperaturePin; PinManager::deallocatePin(parasitePin, PinOwner::UM_Temperature); // initialise setup(); } } // use "return !top["newestParameter"].isNull();" when updating Usermod with new features return !top[FPSTR(_resolution)].isNull(); } void UsermodTemperature::appendConfigData() { oappend(F("addInfo('")); oappend(String(FPSTR(_name)).c_str()); oappend(F(":")); oappend(String(FPSTR(_parasite)).c_str()); oappend(F("',1,'(if no Vcc connected)');")); // 0 is field type, 1 is actual field oappend(F("addInfo('")); oappend(String(FPSTR(_name)).c_str()); oappend(F(":")); oappend(String(FPSTR(_parasitePin)).c_str()); oappend(F("',1,'(for external MOSFET)');")); // 0 is field type, 1 is actual field oappend(F("dd=addDD('")); oappend(String(FPSTR(_name)).c_str()); oappend(F("','")); oappend(String(FPSTR(_resolution)).c_str()); oappend(F("');")); oappend(F("addO(dd,'0.5 °C (9-bit)',0);")); oappend(F("addO(dd,'0.25°C (10-bit)',1);")); oappend(F("addO(dd,'0.125°C (11-bit)',2);")); oappend(F("addO(dd,'0.0625°C (12-bit)',3);")); oappend(F("addInfo('")); oappend(String(FPSTR(_name)).c_str()); oappend(F(":")); oappend(String(FPSTR(_resolution)).c_str()); oappend(F("',1,'(ignored on DS18S20)');")); // 0 is field type, 1 is actual field } float UsermodTemperature::getTemperature() { return degC ? getTemperatureC() : getTemperatureF(); } const char *UsermodTemperature::getTemperatureUnit() { return degC ? "°C" : "°F"; } UsermodTemperature* UsermodTemperature::_instance = nullptr; // strings to reduce flash memory usage (used more than twice) const char UsermodTemperature::_name[] PROGMEM = "Temperature"; const char UsermodTemperature::_enabled[] PROGMEM = "enabled"; const char UsermodTemperature::_readInterval[] PROGMEM = "read-interval-s"; const char UsermodTemperature::_parasite[] PROGMEM = "parasite-pwr"; const char UsermodTemperature::_parasitePin[] PROGMEM = "parasite-pwr-pin"; const char UsermodTemperature::_domoticzIDX[] PROGMEM = "domoticz-idx"; const char UsermodTemperature::_resolution[] PROGMEM = "resolution"; const char UsermodTemperature::_sensor[] PROGMEM = "sensor"; const char UsermodTemperature::_temperature[] PROGMEM = "temperature"; const char UsermodTemperature::_Temperature[] PROGMEM = "/temperature"; const char UsermodTemperature::_data_fx[] PROGMEM = "Temperature@Min,Max;;!;01;pal=54,sx=255,ix=0"; static void mode_temperature() { float low = roundf(mapf((float)SEGMENT.speed, 0.f, 255.f, -150.f, 150.f)); // default: 15°C, range: -15°C to 15°C float high = roundf(mapf((float)SEGMENT.intensity, 0.f, 255.f, 300.f, 600.f)); // default: 30°C, range 30°C to 60°C float temp = constrain(UsermodTemperature::getInstance()->getTemperatureC()*10.f, low, high); // get a little better resolution (*10) unsigned i = map(roundf(temp), (unsigned)low, (unsigned)high, 0, 248); SEGMENT.fill(SEGMENT.color_from_palette(i, false, false, 255)); } static UsermodTemperature temperature; REGISTER_USERMOD(temperature); ================================================ FILE: usermods/Temperature/UsermodTemperature.h ================================================ #pragma once #include "wled.h" #include "OneWire.h" //Pin defaults for QuinLed Dig-Uno if not overriden #ifndef TEMPERATURE_PIN #ifdef ARDUINO_ARCH_ESP32 #define TEMPERATURE_PIN 18 #else //ESP8266 boards #define TEMPERATURE_PIN 14 #endif #endif // the frequency to check temperature, 1 minute #ifndef USERMOD_DALLASTEMPERATURE_MEASUREMENT_INTERVAL #define USERMOD_DALLASTEMPERATURE_MEASUREMENT_INTERVAL 60000 #endif class UsermodTemperature : public Usermod { private: bool initDone = false; OneWire *oneWire; // GPIO pin used for sensor (with a default compile-time fallback) int8_t temperaturePin = TEMPERATURE_PIN; // measurement unit (true==°C, false==°F) bool degC = true; // using parasite power on the sensor bool parasite = false; int8_t parasitePin = -1; // how often do we read from sensor? unsigned long readingInterval = USERMOD_DALLASTEMPERATURE_MEASUREMENT_INTERVAL; // set last reading as "40 sec before boot", so first reading is taken after 20 sec unsigned long lastMeasurement = UINT32_MAX - USERMOD_DALLASTEMPERATURE_MEASUREMENT_INTERVAL; // last time requestTemperatures was called // used to determine when we can read the sensors temperature // we have to wait at least 93.75 ms after requestTemperatures() is called unsigned long lastTemperaturesRequest; float temperature; // indicates requestTemperatures has been called but the sensor measurement is not complete bool waitingForConversion = false; // flag set at startup if DS18B20 sensor not found, avoids trying to keep getting // temperature if flashed to a board without a sensor attached byte sensorFound; bool enabled = true; bool HApublished = false; int16_t idx = -1; // Domoticz virtual sensor idx uint8_t resolution = 0; // 9bits=0, 10bits=1, 11bits=2, 12bits=3 // strings to reduce flash memory usage (used more than twice) static const char _name[]; static const char _enabled[]; static const char _readInterval[]; static const char _parasite[]; static const char _parasitePin[]; static const char _domoticzIDX[]; static const char _resolution[]; static const char _sensor[]; static const char _temperature[]; static const char _Temperature[]; static const char _data_fx[]; //Dallas sensor quick (& dirty) reading. Credit to - Author: Peter Scargill, August 17th, 2013 float readDallas(); void requestTemperatures(); void readTemperature(); bool findSensor(); #ifndef WLED_DISABLE_MQTT void publishHomeAssistantAutodiscovery(); #endif static UsermodTemperature* _instance; // to overcome nonstatic getTemperatureC() method and avoid UsermodManager::lookup(USERMOD_ID_TEMPERATURE); public: UsermodTemperature() { _instance = this; } static UsermodTemperature *getInstance() { return UsermodTemperature::_instance; } /* * API calls te enable data exchange between WLED modules */ inline float getTemperatureC() { return temperature; } inline float getTemperatureF() { return temperature * 1.8f + 32.0f; } float getTemperature(); const char *getTemperatureUnit(); uint16_t getId() override { return USERMOD_ID_TEMPERATURE; } void setup() override; void loop() override; //void connected() override; #ifndef WLED_DISABLE_MQTT void onMqttConnect(bool sessionPresent) override; #endif //void onUpdateBegin(bool init) override; //bool handleButton(uint8_t b) override; //void handleOverlayDraw() override; void addToJsonInfo(JsonObject& root) override; //void addToJsonState(JsonObject &root) override; //void readFromJsonState(JsonObject &root) override; void addToConfig(JsonObject &root) override; bool readFromConfig(JsonObject &root) override; void appendConfigData() override; }; ================================================ FILE: usermods/Temperature/library.json ================================================ { "name": "Temperature", "build": { "libArchive": false}, "dependencies": { "paulstoffregen/OneWire":"~2.3.8" } } ================================================ FILE: usermods/Temperature/readme.md ================================================ # Temperature usermod Based on the excellent `QuinLED_Dig_Uno_Temp_MQTT` usermod by srg74 and 400killer! Reads an attached DS18B20 temperature sensor (as available on the QuinLED Dig-Uno) Temperature is displayed in both the Info section of the web UI as well as published to the `/temperature` MQTT topic, if enabled. May be expanded with support for different sensor types in the future. If temperature sensor is not detected during boot, this usermod will be disabled. Maintained by @blazoncek ## Installation Add `Temperature` to `custom_usermods` in your platformio_override.ini. Example **platformio_override.ini**: ```ini [env:usermod_temperature_esp32dev] extends = env:esp32dev custom_usermods = ${env:esp32dev.custom_usermods} Temperature ``` ### Define Your Options * `USERMOD_DALLASTEMPERATURE_MEASUREMENT_INTERVAL` - number of milliseconds between measurements, defaults to 60000 ms (60s) All parameters can be configured at runtime via the Usermods settings page, including pin, temperature in degrees Celsius or Fahrenheit and measurement interval. ## Project link * [QuinLED-Dig-Uno](https://quinled.info/2018/09/15/quinled-dig-uno/) - Project link * [Srg74-WLED-Wemos-shield](https://github.com/srg74/WLED-wemos-shield) - another great DIY WLED board ## Change Log 2020-09-12 * Changed to use async non-blocking implementation * Do not report erroneous low temperatures to MQTT * Disable plugin if temperature sensor not detected * Report the number of seconds until the first read in the info screen instead of sensor error 2021-04 * Adaptation for runtime configuration. 2023-05 * Rewrite to conform to newer recommendations. * Recommended @blazoncek fork of OneWire for ESP32 to avoid Sensor error 2024-09 * Update OneWire to version 2.3.8, which includes stickbreaker's and garyd9's ESP32 fixes: blazoncek's fork is no longer needed ================================================ FILE: usermods/TetrisAI_v2/TetrisAI_v2.cpp ================================================ #include "wled.h" #include "FX.h" #include "fcn_declare.h" #include "tetrisaigame.h" // By: muebau bool noFlashOnClear = false; typedef struct TetrisAI_data { unsigned long lastTime = 0; unsigned long clearingStartTime = 0; TetrisAIGame tetris; uint8_t intelligence; uint8_t rotate; bool showNext; bool showBorder; uint8_t colorOffset; uint8_t colorInc; uint8_t mistaceCountdown; uint16_t segcols; uint16_t segrows; uint16_t segOffsetX; uint16_t segOffsetY; uint16_t effectWidth; uint16_t effectHeight; } tetrisai_data; void drawGrid(TetrisAIGame* tetris, TetrisAI_data* tetrisai_data) { SEGMENT.fill(SEGCOLOR(1)); //GRID for (auto index_y = 4; index_y < tetris->grid.height; index_y++) { bool isRowClearing = tetris->grid.gridBW.clearingRows[index_y]; for (auto index_x = 0; index_x < tetris->grid.width; index_x++) { CRGB color; uint8_t gridPixel = *tetris->grid.getPixel(index_x, index_y); if (isRowClearing) { if (noFlashOnClear) { color = CRGB::Gray; } else { //flash color white and black every 200ms color = (strip.now % 200) < 150 ? CRGB::Gray : CRGB::Black; } } else if (gridPixel == 0) { //BG color color = SEGCOLOR(1); } //game over animation else if (gridPixel == 254) { //use fg color = SEGCOLOR(0); } else { //spread the color over the whole palette uint8_t colorIndex = gridPixel * 32; colorIndex += tetrisai_data->colorOffset; color = ColorFromPalette(SEGPALETTE, colorIndex, 255, NOBLEND); } SEGMENT.setPixelColorXY(tetrisai_data->segOffsetX + index_x, tetrisai_data->segOffsetY + index_y - 4, color); } } tetrisai_data->colorOffset += tetrisai_data->colorInc; //NEXT PIECE AREA if (tetrisai_data->showNext) { //BORDER if (tetrisai_data->showBorder) { //draw a line 6 pixels from right with the border color for (auto index_y = 0; index_y < tetrisai_data->effectHeight; index_y++) { SEGMENT.setPixelColorXY(tetrisai_data->segOffsetX + tetrisai_data->effectWidth - 6, tetrisai_data->segOffsetY + index_y, SEGCOLOR(2)); } } //NEXT PIECE int piecesOffsetX = tetrisai_data->effectWidth - 4; int piecesOffsetY = 1; for (uint8_t nextPieceIdx = 1; nextPieceIdx < tetris->nLookAhead; nextPieceIdx++) { uint8_t pieceNbrOffsetY = (nextPieceIdx - 1) * 5; Piece piece(tetris->bag.piecesQueue[nextPieceIdx]); for (uint8_t pieceY = 0; pieceY < piece.getRotation().height; pieceY++) { for (uint8_t pieceX = 0; pieceX < piece.getRotation().width; pieceX++) { if (piece.getPixel(pieceX, pieceY)) { uint8_t colIdx = ((piece.pieceData->colorIndex * 32) + tetrisai_data->colorOffset); SEGMENT.setPixelColorXY(tetrisai_data->segOffsetX + piecesOffsetX + pieceX, tetrisai_data->segOffsetY + piecesOffsetY + pieceNbrOffsetY + pieceY, ColorFromPalette(SEGPALETTE, colIdx, 255, NOBLEND)); } } } } } } //////////////////////////// // 2D Tetris AI // //////////////////////////// void mode_2DTetrisAI() { if (!strip.isMatrix || !SEGENV.allocateData(sizeof(tetrisai_data))) { // not a 2D set-up SEGMENT.fill(SEGCOLOR(0)); return; } TetrisAI_data* tetrisai_data = reinterpret_cast(SEGENV.data); const uint16_t cols = SEGMENT.virtualWidth(); const uint16_t rows = SEGMENT.virtualHeight(); //range 0 - 1024ms => 1024/255 ~ 4 uint16_t msDelayMove = 1024 - (4 * SEGMENT.speed); int16_t msDelayGameOver = msDelayMove / 4; //range 0 - 2 (not including current) uint8_t nLookAhead = SEGMENT.intensity ? (SEGMENT.intensity >> 7) + 2 : 1; //range 0 - 16 tetrisai_data->colorInc = SEGMENT.custom2 >> 4; if (tetrisai_data->tetris.nLookAhead != nLookAhead || tetrisai_data->segcols != cols || tetrisai_data->segrows != rows || tetrisai_data->showNext != SEGMENT.check1 || tetrisai_data->showBorder != SEGMENT.check2 ) { tetrisai_data->segcols = cols; tetrisai_data->segrows = rows; tetrisai_data->showNext = SEGMENT.check1; tetrisai_data->showBorder = SEGMENT.check2; //not more than 32 columns and 255 rows as this is the limit of this implementation uint8_t gridWidth = cols > 32 ? 32 : cols; uint8_t gridHeight = rows > 255 ? 255 : rows; tetrisai_data->effectWidth = 0; tetrisai_data->effectHeight = 0; // do we need space for the 'next' section? if (tetrisai_data->showNext) { //does it get to tight? if (gridWidth + 5 > cols) { // yes, so make the grid smaller // make space for the piece and one pixel of space gridWidth = (gridWidth - ((gridWidth + 5) - cols)); } tetrisai_data->effectWidth += 5; // do we need space for a border? if (tetrisai_data->showBorder) { if (gridWidth + 5 + 1 > cols) { gridWidth -= 1; } tetrisai_data->effectWidth += 1; } } tetrisai_data->effectWidth += gridWidth; tetrisai_data->effectHeight += gridHeight; tetrisai_data->segOffsetX = cols > tetrisai_data->effectWidth ? ((cols - tetrisai_data->effectWidth) / 2) : 0; tetrisai_data->segOffsetY = rows > tetrisai_data->effectHeight ? ((rows - tetrisai_data->effectHeight) / 2) : 0; tetrisai_data->tetris = TetrisAIGame(gridWidth, gridHeight, nLookAhead, piecesData, numPieces); tetrisai_data->tetris.state = TetrisAIGame::States::INIT; tetrisai_data->clearingStartTime = 0; SEGMENT.fill(SEGCOLOR(1)); } if (tetrisai_data->intelligence != SEGMENT.custom1) { tetrisai_data->intelligence = SEGMENT.custom1; float dui = 0.2f - (0.2f * (tetrisai_data->intelligence / 255.0f)); tetrisai_data->tetris.ai.aHeight = -0.510066f + dui; tetrisai_data->tetris.ai.fullLines = 0.760666f - dui; tetrisai_data->tetris.ai.holes = -0.35663f + dui; tetrisai_data->tetris.ai.bumpiness = -0.184483f + dui; } //end line clearing flashing effect if needed if (tetrisai_data->tetris.grid.gridBW.hasClearingRows()) { if (tetrisai_data->clearingStartTime == 0) { tetrisai_data->clearingStartTime = strip.now; } if (strip.now - tetrisai_data->clearingStartTime > 750) { tetrisai_data->tetris.grid.gridBW.clearedLinesReadyForRemoval = true; tetrisai_data->tetris.grid.cleanupFullLines(); tetrisai_data->clearingStartTime = 0; } drawGrid(&tetrisai_data->tetris, tetrisai_data); } else if (tetrisai_data->tetris.state == TetrisAIGame::ANIMATE_MOVE) { if (strip.now - tetrisai_data->lastTime > msDelayMove) { drawGrid(&tetrisai_data->tetris, tetrisai_data); tetrisai_data->lastTime = strip.now; tetrisai_data->tetris.poll(); } } else if (tetrisai_data->tetris.state == TetrisAIGame::ANIMATE_GAME_OVER) { if (strip.now - tetrisai_data->lastTime > msDelayGameOver) { drawGrid(&tetrisai_data->tetris, tetrisai_data); tetrisai_data->lastTime = strip.now; tetrisai_data->tetris.poll(); } } else if (tetrisai_data->tetris.state == TetrisAIGame::FIND_BEST_MOVE) { if (SEGMENT.check3) { if(tetrisai_data->mistaceCountdown == 0) { tetrisai_data->tetris.ai.findWorstMove = true; tetrisai_data->tetris.poll(); tetrisai_data->tetris.ai.findWorstMove = false; tetrisai_data->mistaceCountdown = SEGMENT.custom3; } tetrisai_data->mistaceCountdown--; } tetrisai_data->tetris.poll(); } else { tetrisai_data->tetris.poll(); } } // mode_2DTetrisAI() static const char _data_FX_MODE_2DTETRISAI[] PROGMEM = "Tetris AI@!,Look ahead,Intelligence,Rotate color,Mistake free,Show next,Border,Mistakes;Game Over,!,Border;!;2;sx=127,ix=64,c1=255,c2=0,c3=31,o1=1,o2=1,o3=0,pal=11"; class TetrisAIUsermod : public Usermod { private: static const char _name[]; public: void setup() { strip.addEffect(255, &mode_2DTetrisAI, _data_FX_MODE_2DTETRISAI); } void addToConfig(JsonObject& root) override { JsonObject top = root.createNestedObject(FPSTR(_name)); top["noFlashOnClear"] = noFlashOnClear; } bool readFromConfig(JsonObject& root) override { JsonObject top = root[FPSTR(_name)]; bool configComplete = !top.isNull(); configComplete &= getJsonValue(top["noFlashOnClear"], noFlashOnClear); return configComplete; } void loop() { } uint16_t getId() { return USERMOD_ID_TETRISAI; } }; const char TetrisAIUsermod::_name[] PROGMEM = "TetrisAI_v2"; static TetrisAIUsermod tetrisai_v2; REGISTER_USERMOD(tetrisai_v2); ================================================ FILE: usermods/TetrisAI_v2/gridbw.h ================================================ /****************************************************************************** * @file : gridbw.h * @brief : contains the tetris grid as binary so black and white version ****************************************************************************** * @attention * * Copyright (c) muebau 2023 * All rights reserved. * ****************************************************************************** */ #ifndef __GRIDBW_H__ #define __GRIDBW_H__ #include #include "pieces.h" using namespace std; class GridBW { private: public: uint8_t width; uint8_t height; std::vector pixels; // When a row fills, we mark it here first so it can flash before being // fully removed. std::vector clearingRows; // True when a line clearing flashing effect is over and we're ready to // fully clean up the lines bool clearedLinesReadyForRemoval = false; GridBW(uint8_t width, uint8_t height): width(width), height(height), pixels(height), clearingRows(height) { if (width > 32) { this->width = 32; } } void placePiece(Piece* piece, uint8_t x, uint8_t y) { for (uint8_t row = 4 - piece->getRotation().height; row < 4; row++) { pixels[y + (row - (4 - piece->getRotation().height))] |= piece->getGridRow(x, row, width); } } void erasePiece(Piece* piece, uint8_t x, uint8_t y) { for (uint8_t row = 4 - piece->getRotation().height; row < 4; row++) { pixels[y + (row - (4 - piece->getRotation().height))] &= ~piece->getGridRow(x, row, width); } } bool noCollision(Piece* piece, uint8_t x, uint8_t y) { //if it touches a wall it is a collision if (x > (this->width - piece->getRotation().width) || y > this->height - piece->getRotation().height) { return false; } for (uint8_t row = 4 - piece->getRotation().height; row < 4; row++) { if (piece->getGridRow(x, row, width) & pixels[y + (row - (4 - piece->getRotation().height))]) { return false; } } return true; } void findLandingPosition(Piece* piece) { // move down until the piece bumps into some occupied pixels or the 'wall' while (noCollision(piece, piece->x, piece->landingY)) { piece->landingY++; } //at this point the positon is 'in the wall' or 'over some occupied pixel' //so the previous position was the last correct one (clamped to 0 as minimum). piece->landingY = piece->landingY > 0 ? piece->landingY - 1 : 0; } bool hasClearingRows() { for (bool rowClearing : clearingRows) { if (rowClearing) { return true; } } return false; } void cleanupFullLines() { // Skip cleanup if there are rows clearing if (hasClearingRows() && !clearedLinesReadyForRemoval) { return; } uint8_t offset = 0; bool doneRemovingClearedLines = false; //from "height - 1" to "0", so from bottom row to top for (uint8_t row = height; row-- > 0; ) { //full line? if (isLineFull(row)) { if (clearedLinesReadyForRemoval) { offset++; pixels[row] = 0x0; doneRemovingClearedLines = true; } else { clearingRows[row] = true; } continue; } if (offset > 0) { pixels[row + offset] = pixels[row]; pixels[row] = 0x0; } } if (doneRemovingClearedLines) { clearingRows.assign(height, false); clearedLinesReadyForRemoval = false; } } bool isLineFull(uint8_t y) { return pixels[y] == (width >= 32 ? UINT32_MAX : (1U << width) - 1); } bool isLineReadyForRemoval(uint8_t y) { return clearedLinesReadyForRemoval && isLineFull(y); } void reset() { if (width > 32) { width = 32; } pixels.clear(); pixels.resize(height); clearingRows.assign(height, false); clearedLinesReadyForRemoval = false; } }; #endif /* __GRIDBW_H__ */ ================================================ FILE: usermods/TetrisAI_v2/gridcolor.h ================================================ /****************************************************************************** * @file : gridcolor.h * @brief : contains the tetris grid as 8bit indexed color version ****************************************************************************** * @attention * * Copyright (c) muebau 2023 * All rights reserved. * ****************************************************************************** */ #ifndef __GRIDCOLOR_H__ #define __GRIDCOLOR_H__ #include #include #include #include "gridbw.h" #include "gridcolor.h" using namespace std; class GridColor { private: public: uint8_t width; uint8_t height; GridBW gridBW; std::vector pixels; GridColor(uint8_t width, uint8_t height): width(width), height(height), gridBW(width, height), pixels(width* height) {} void clear() { for (uint8_t y = 0; y < height; y++) { gridBW.pixels[y] = 0x0; for (int8_t x = 0; x < width; x++) { *getPixel(x, y) = 0; } } } void placePiece(Piece* piece, uint8_t x, uint8_t y) { for (uint8_t pieceY = 0; pieceY < piece->getRotation().height; pieceY++) { for (uint8_t pieceX = 0; pieceX < piece->getRotation().width; pieceX++) { if (piece->getPixel(pieceX, pieceY)) { *getPixel(x + pieceX, y + pieceY) = piece->pieceData->colorIndex; } } } } void erasePiece(Piece* piece, uint8_t x, uint8_t y) { for (uint8_t pieceY = 0; pieceY < piece->getRotation().height; pieceY++) { for (uint8_t pieceX = 0; pieceX < piece->getRotation().width; pieceX++) { if (piece->getPixel(pieceX, pieceY)) { *getPixel(x + pieceX, y + pieceY) = 0; } } } } void cleanupFullLines() { uint8_t offset = 0; //from "height - 1" to "0", so from bottom row to top for (uint8_t y = height; y-- > 0; ) { if (gridBW.isLineReadyForRemoval(y)) { offset++; for (uint8_t x = 0; x < width; x++) { pixels[y * width + x] = 0; } continue; } if (offset > 0) { if (gridBW.pixels[y]) { for (uint8_t x = 0; x < width; x++) { pixels[(y + offset) * width + x] = pixels[y * width + x]; pixels[y * width + x] = 0; } } } } gridBW.cleanupFullLines(); } uint8_t* getPixel(uint8_t x, uint8_t y) { return &pixels[y * width + x]; } void sync() { for (uint8_t y = 0; y < height; y++) { gridBW.pixels[y] = 0x0; for (int8_t x = 0; x < width; x++) { gridBW.pixels[y] <<= 1; if (*getPixel(x, y) != 0) { gridBW.pixels[y] |= 0x1; } } } } void reset() { gridBW.reset(); pixels.clear(); pixels.resize(width* height); clear(); } }; #endif /* __GRIDCOLOR_H__ */ ================================================ FILE: usermods/TetrisAI_v2/library.json ================================================ { "name": "TetrisAI_v2", "build": { "libArchive": false } } ================================================ FILE: usermods/TetrisAI_v2/pieces.h ================================================ /****************************************************************************** * @file : pieces.h * @brief : contains the tetris pieces with their colors indecies ****************************************************************************** * @attention * * Copyright (c) muebau 2022 * All rights reserved. * ****************************************************************************** */ #ifndef __PIECES_H__ #define __PIECES_H__ #include #include #include #include #include #define numPieces 7 struct PieceRotation { uint8_t width; uint8_t height; uint16_t rows; }; struct PieceData { uint8_t rotCount; uint8_t colorIndex; PieceRotation rotations[4]; }; PieceData piecesData[numPieces] = { // I { 2, 1, { { 1, 4, 0b0001000100010001}, { 4, 1, 0b0000000000001111} } }, // O { 1, 2, { { 2, 2, 0b0000000000110011} } }, // Z { 2, 3, { { 3, 2, 0b0000000001100011}, { 2, 3, 0b0000000100110010} } }, // S { 2, 4, { { 3, 2, 0b0000000000110110}, { 2, 3, 0b0000001000110001} } }, // L { 4, 5, { { 2, 3, 0b0000001000100011}, { 3, 2, 0b0000000001110100}, { 2, 3, 0b0000001100010001}, { 3, 2, 0b0000000000010111} } }, // J { 4, 6, { { 2, 3, 0b0000000100010011}, { 3, 2, 0b0000000001000111}, { 2, 3, 0b0000001100100010}, { 3, 2, 0b0000000001110001} } }, // T { 4, 7, { { 3, 2, 0b0000000001110010}, { 2, 3, 0b0000000100110001}, { 3, 2, 0b0000000000100111}, { 2, 3, 0b0000001000110010} } }, }; class Piece { private: public: uint8_t x; uint8_t y; PieceData* pieceData; uint8_t rotation; uint8_t landingY; Piece(uint8_t pieceIndex = 0): x(0), y(0), rotation(0), landingY(0) { this->pieceData = &piecesData[pieceIndex]; } void reset() { this->rotation = 0; this->x = 0; this->y = 0; this->landingY = 0; } uint32_t getGridRow(uint8_t x, uint8_t y, uint8_t width) { if (x < width) { //shift the row with the "top-left" position to the "x" position auto shiftx = (width - 1) - x; auto topleftx = (getRotation().width - 1); auto finalShift = shiftx - topleftx; auto row = getRow(y); auto finalResult = row << finalShift; return finalResult; } return 0xffffffff; } uint8_t getRow(uint8_t y) { if (y < 4) { return (getRotation().rows >> (12 - (4 * y))) & 0xf; } return 0xf; } bool getPixel(uint8_t x, uint8_t y) { if(x > getRotation().width - 1 || y > getRotation().height - 1 ) { return false; } if (x < 4 && y < 4) { return (getRow((4 - getRotation().height) + y) >> (3 - ((4 - getRotation().width) + x))) & 0x1; } return false; } PieceRotation getRotation() { return this->pieceData->rotations[rotation]; } }; #endif /* __PIECES_H__ */ ================================================ FILE: usermods/TetrisAI_v2/rating.h ================================================ /****************************************************************************** * @file : rating.h * @brief : contains the tetris rating of a grid ****************************************************************************** * @attention * * Copyright (c) muebau 2022 * All rights reserved. * ****************************************************************************** */ #ifndef __RATING_H__ #define __RATING_H__ #include #include #include #include #include #include "rating.h" using namespace std; class Rating { private: public: uint8_t minHeight; uint8_t maxHeight; uint16_t holes; uint8_t fullLines; uint16_t bumpiness; uint16_t aggregatedHeight; float score; uint8_t width; std::vector lineHights; Rating(uint8_t width): width(width), lineHights(width) { reset(); } void reset() { this->minHeight = 0; this->maxHeight = 0; for (uint8_t line = 0; line < this->width; line++) { this->lineHights[line] = 0; } this->holes = 0; this->fullLines = 0; this->bumpiness = 0; this->aggregatedHeight = 0; this->score = -FLT_MAX; } }; #endif /* __RATING_H__ */ ================================================ FILE: usermods/TetrisAI_v2/readme.md ================================================ # Tetris AI effect usermod This usermod adds a self-playing Tetris game as an 'effect'. The mod requires version 0.14 or higher as it relies on matrix support. The effect was tested on an ESP32 4MB with a WS2812B 16x16 matrix. PHOTOSENSITIVE EPILEPSY WARNING: By default the effect features a flashing animation on line clear. This can be disabled from the usermod settings page in WLED. ## Installation To activate the usermod, add the following line to your platformio_override.ini `custom_usermods = tetrisai_v2` The effect will then become available under the name 'Tetris AI'. If you are running out of flash memory, use a different memory layout (e.g. [WLED_ESP32_4MB_256KB_FS.csv](https://github.com/wled-dev/WLED/blob/main/tools/WLED_ESP32_4MB_256KB_FS.csv)). If needed simply add to `platformio_override.ini`: ```ini board_build.partitions = tools/WLED_ESP32_4MB_256KB_FS.csv ``` ## Usage It is best to set the background color to black 🖤, the border color to light grey 🤍, the game over color (foreground) to dark grey 🩶, and color palette to 'Rainbow' 🌈. ### Sliders and boxes #### Sliders * speed: speed the game plays * look ahead: how many pieces is the AI allowed to know the next pieces (0 - 2) * intelligence: how good the AI will play * Rotate color: make the colors shift (rotate) every few moves * Mistakes free: how many good moves between mistakes (if enabled) #### Checkboxes * show next: if true, a space of 5 pixels from the right will be used to show the next pieces. Otherwise the whole segment is used for the grid. * show border: if true an additional column of 1 pixel is used to draw a border between the grid and the next pieces * mistakes: if true, the worst decision will be made every few moves instead of the best (see above). ## Best results If the speed is set to be a little bit faster than a good human could play with maximal intelligence and very few mistakes it makes people furious/happy at a party 😉. ## Limits The game grid is limited to a maximum width of 32 and a maximum height of 255 due to the internal structure of the code. The canvas of the effect will be centred in the segment if the segment exceeds the maximum width or height. ================================================ FILE: usermods/TetrisAI_v2/tetrisai.h ================================================ /****************************************************************************** * @file : ai.h * @brief : contains the heuristic ****************************************************************************** * @attention * * Copyright (c) muebau 2023 * All rights reserved. * ****************************************************************************** */ #ifndef __AI_H__ #define __AI_H__ #include "gridbw.h" #include "rating.h" using namespace std; class TetrisAI { private: public: float aHeight; float fullLines; float holes; float bumpiness; bool findWorstMove = false; uint8_t countOnes(uint32_t vector) { uint8_t count = 0; while (vector) { vector &= (vector - 1); count++; } return count; } void updateRating(GridBW grid, Rating* rating) { rating->minHeight = 0; rating->maxHeight = 0; rating->holes = 0; rating->fullLines = 0; rating->bumpiness = 0; rating->aggregatedHeight = 0; fill(rating->lineHights.begin(), rating->lineHights.end(), 0); uint32_t columnvector = 0x0; uint32_t lastcolumnvector = 0x0; for (uint8_t row = 0; row < grid.height; row++) { columnvector |= grid.pixels[row]; //first (highest) column makes it if (rating->maxHeight == 0 && columnvector) { rating->maxHeight = grid.height - row; } //if column vector is full we found the minimal height (or it stays zero) if (rating->minHeight == 0 && (columnvector == (uint32_t)((1 << grid.width) - 1))) { rating->minHeight = grid.height - row; } //line full if all ones in mask :-) if (grid.isLineReadyForRemoval(row)) { rating->fullLines++; } //holes are basically a XOR with the "full" columns rating->holes += countOnes(columnvector ^ grid.pixels[row]); //calculate the difference (XOR) between the current column vector and the last one uint32_t columnDelta = columnvector ^ lastcolumnvector; //process every new column uint8_t index = 0; while (columnDelta) { //if this is a new column if (columnDelta & 0x1) { //update hight of this column rating->lineHights[(grid.width - 1) - index] = grid.height - row; // update aggregatedHeight rating->aggregatedHeight += grid.height - row; } index++; columnDelta >>= 1; } lastcolumnvector = columnvector; } //compare every two columns to get the difference and add them up for (uint8_t column = 1; column < grid.width; column++) { rating->bumpiness += abs(rating->lineHights[column - 1] - rating->lineHights[column]); } rating->score = (aHeight * (rating->aggregatedHeight)) + (fullLines * (rating->fullLines)) + (holes * (rating->holes)) + (bumpiness * (rating->bumpiness)); } TetrisAI(): TetrisAI(-0.510066f, 0.760666f, -0.35663f, -0.184483f) {} TetrisAI(float aHeight, float fullLines, float holes, float bumpiness): aHeight(aHeight), fullLines(fullLines), holes(holes), bumpiness(bumpiness) {} void findBestMove(GridBW grid, Piece *piece) { vector pieces = {*piece}; findBestMove(grid, &pieces); *piece = pieces[0]; } void findBestMove(GridBW grid, std::vector *pieces) { findBestMove(grid, pieces->begin(), pieces->end()); } void findBestMove(GridBW grid, std::vector::iterator start, std::vector::iterator end) { Rating bestRating(grid.width); findBestMove(grid, start, end, &bestRating); } void findBestMove(GridBW grid, std::vector::iterator start, std::vector::iterator end, Rating* bestRating) { grid.cleanupFullLines(); Rating curRating(grid.width); Rating deeperRating(grid.width); Piece piece = *start; // for every rotation of the piece for (piece.rotation = 0; piece.rotation < piece.pieceData->rotCount; piece.rotation++) { // put piece to top left corner piece.x = 0; piece.y = 0; //test for every column for (piece.x = 0; piece.x <= grid.width - piece.getRotation().width; piece.x++) { //todo optimise by the use of the previous grids height piece.landingY = 0; //will set landingY to final position grid.findLandingPosition(&piece); // draw piece grid.placePiece(&piece, piece.x, piece.landingY); if(start == end - 1) { //at the deepest level updateRating(grid, &curRating); } else { //go deeper to take another piece into account findBestMove(grid, start + 1, end, &deeperRating); curRating = deeperRating; } // eraese piece grid.erasePiece(&piece, piece.x, piece.landingY); if(findWorstMove) { //init rating for worst if(bestRating->score == -FLT_MAX) { bestRating->score = FLT_MAX; } // update if we found a worse one if (bestRating->score > curRating.score) { *bestRating = curRating; (*start) = piece; } } else { // update if we found a better one if (bestRating->score < curRating.score) { *bestRating = curRating; (*start) = piece; } } } } } }; #endif /* __AI_H__ */ ================================================ FILE: usermods/TetrisAI_v2/tetrisaigame.h ================================================ /****************************************************************************** * @file : tetrisaigame.h * @brief : main tetris functions ****************************************************************************** * @attention * * Copyright (c) muebau 2022 * All rights reserved. * ****************************************************************************** */ #ifndef __TETRISAIGAME_H__ #define __TETRISAIGAME_H__ #include #include #include #include "pieces.h" #include "gridcolor.h" #include "tetrisbag.h" #include "tetrisai.h" using namespace std; class TetrisAIGame { private: bool animateFallOfPiece(Piece* piece, bool skip) { if (skip || piece->y >= piece->landingY) { piece->y = piece->landingY; grid.gridBW.placePiece(piece, piece->x, piece->landingY); grid.placePiece(piece, piece->x, piece->y); return false; } else { // eraese last drawing grid.erasePiece(piece, piece->x, piece->y); //move piece down piece->y++; // draw piece grid.placePiece(piece, piece->x, piece->y); return true; } } public: uint8_t width; uint8_t height; uint8_t nLookAhead; uint8_t nPieces; TetrisBag bag; GridColor grid; TetrisAI ai; Piece curPiece; PieceData* piecesData; enum States { INIT, TEST_GAME_OVER, GET_NEXT_PIECE, FIND_BEST_MOVE, ANIMATE_MOVE, ANIMATE_GAME_OVER } state = INIT; TetrisAIGame(uint8_t width, uint8_t height, uint8_t nLookAhead, PieceData* piecesData, uint8_t nPieces): width(width), height(height), nLookAhead(nLookAhead), nPieces(nPieces), bag(nPieces, 1, nLookAhead), grid(width, height + 4), ai(), piecesData(piecesData) { } void nextPiece() { grid.cleanupFullLines(); bag.queuePiece(); } void findBestMove() { ai.findBestMove(grid.gridBW, &bag.piecesQueue); } bool animateFall(bool skip) { return animateFallOfPiece(&(bag.piecesQueue[0]), skip); } bool isGameOver() { //if there is something in the 4 lines of the hidden area the game is over return grid.gridBW.pixels[0] || grid.gridBW.pixels[1] || grid.gridBW.pixels[2] || grid.gridBW.pixels[3]; } void poll() { switch (state) { case INIT: reset(); state = TEST_GAME_OVER; break; case TEST_GAME_OVER: if (isGameOver()) { state = ANIMATE_GAME_OVER; } else { state = GET_NEXT_PIECE; } break; case GET_NEXT_PIECE: nextPiece(); state = FIND_BEST_MOVE; break; case FIND_BEST_MOVE: findBestMove(); state = ANIMATE_MOVE; break; case ANIMATE_MOVE: if (!animateFall(false)) { state = TEST_GAME_OVER; } break; case ANIMATE_GAME_OVER: static auto curPixel = grid.pixels.size(); grid.pixels[curPixel] = 254; if (curPixel == 0) { state = INIT; curPixel = grid.pixels.size(); } curPixel--; break; } } void reset() { grid.width = width; grid.height = height + 4; grid.reset(); bag.reset(); } }; #endif /* __TETRISAIGAME_H__ */ ================================================ FILE: usermods/TetrisAI_v2/tetrisbag.h ================================================ /****************************************************************************** * @file : tetrisbag.h * @brief : the tetris implementation of a random piece generator ****************************************************************************** * @attention * * Copyright (c) muebau 2022 * All rights reserved. * ****************************************************************************** */ #ifndef __TETRISBAG_H__ #define __TETRISBAG_H__ #include #include #include "tetrisbag.h" class TetrisBag { private: public: uint8_t nPieces; uint8_t nBagLength; uint8_t queueLength; uint8_t bagIdx; std::vector bag; std::vector piecesQueue; TetrisBag(uint8_t nPieces, uint8_t nBagLength, uint8_t queueLength): nPieces(nPieces), nBagLength(nBagLength), queueLength(queueLength), bag(nPieces * nBagLength), piecesQueue(queueLength) { init(); } void init() { //will shuffle the bag at first use bagIdx = nPieces - 1; for (uint8_t bagIndex = 0; bagIndex < nPieces * nBagLength; bagIndex++) { bag[bagIndex] = bagIndex % nPieces; } //will init the queue for (uint8_t index = 0; index < piecesQueue.size(); index++) { queuePiece(); } } void shuffleBag() { uint8_t temp; uint8_t swapIdx; for (int index = nPieces - 1; index > 0; index--) { //get candidate to swap swapIdx = rand() % index; //swap it! temp = bag[swapIdx]; bag[swapIdx] = bag[index]; bag[index] = temp; } } Piece getNextPiece() { bagIdx++; if (bagIdx >= nPieces) { shuffleBag(); bagIdx = 0; } return Piece(bag[bagIdx]); } void queuePiece() { //move vector to left for (uint8_t i = 1; i < piecesQueue.size(); i++) { piecesQueue[i - 1] = piecesQueue[i]; } piecesQueue[piecesQueue.size() - 1] = getNextPiece(); } void reset() { bag.clear(); bag.resize(nPieces * nBagLength); piecesQueue.clear(); piecesQueue.resize(queueLength); init(); } }; #endif /* __TETRISBAG_H__ */ ================================================ FILE: usermods/VL53L0X_gestures/VL53L0X_gestures.cpp ================================================ /* * That usermod implements support of simple hand gestures with VL53L0X sensor: on/off and brightness correction. * It can be useful for kitchen strips to avoid any touches. * - on/off - just swipe a hand below your sensor ("shortPressAction" is called and can be customized through WLED macros) * - brightness correction - keep your hand below sensor for 1 second to switch to "brightness" mode. Configure brightness by changing distance to the sensor (see parameters below for customization). * * Enabling this usermod: * 1. Attach VL53L0X sensor to i2c pins according to default pins for your board. * 2. Add `-D USERMOD_VL53L0X_GESTURES` to your build flags at platformio.ini (plaformio_override.ini) for needed environment. * In my case, for example: `build_flags = ${env.build_flags} -D USERMOD_VL53L0X_GESTURES` * 3. Add "pololu/VL53L0X" dependency below to `lib_deps` like this: * lib_deps = ${env.lib_deps} * pololu/VL53L0X @ ^1.3.0 */ #include "wled.h" #include #include #ifndef VL53L0X_MAX_RANGE_MM #define VL53L0X_MAX_RANGE_MM 230 // max height in millimeters to react for motions #endif #ifndef VL53L0X_MIN_RANGE_OFFSET #define VL53L0X_MIN_RANGE_OFFSET 60 // minimal range in millimeters that sensor can detect. Used in long motions to correct brightness calculation. #endif #ifndef VL53L0X_DELAY_MS #define VL53L0X_DELAY_MS 100 // how often to get data from sensor #endif #ifndef VL53L0X_LONG_MOTION_DELAY_MS #define VL53L0X_LONG_MOTION_DELAY_MS 1000 // switch onto "long motion" action after this delay #endif class UsermodVL53L0XGestures : public Usermod { private: //Private class members. You can declare variables and functions only accessible to your usermod here unsigned long lastTime = 0; VL53L0X sensor; bool enabled = true; bool wasMotionBefore = false; bool isLongMotion = false; unsigned long motionStartTime = 0; public: void setup() { if (i2c_scl<0 || i2c_sda<0) { enabled = false; return; } sensor.setTimeout(150); if (!sensor.init()) { DEBUG_PRINTLN(F("Failed to detect and initialize VL53L0X sensor!")); } else { sensor.setMeasurementTimingBudget(20000); // set high speed mode } } void loop() { if (!enabled || strip.isUpdating()) return; if (millis() - lastTime > VL53L0X_DELAY_MS) { lastTime = millis(); int range = sensor.readRangeSingleMillimeters(); DEBUG_PRINTF("range: %d, brightness: %d\r\n", range, bri); if (range < VL53L0X_MAX_RANGE_MM) { if (!wasMotionBefore) { motionStartTime = millis(); DEBUG_PRINTF("motionStartTime: %d\r\n", motionStartTime); } wasMotionBefore = true; if (millis() - motionStartTime > VL53L0X_LONG_MOTION_DELAY_MS) //long motion { DEBUG_PRINTF("long motion: %d\r\n", motionStartTime); if (!isLongMotion) { isLongMotion = true; } // set brightness according to range bri = (VL53L0X_MAX_RANGE_MM - max(range, VL53L0X_MIN_RANGE_OFFSET)) * 255 / (VL53L0X_MAX_RANGE_MM - VL53L0X_MIN_RANGE_OFFSET); DEBUG_PRINTF("new brightness: %d", bri); stateUpdated(1); } } else if (wasMotionBefore) { //released if (!isLongMotion) { //short press DEBUG_PRINTLN(F("shortPressAction...")); shortPressAction(); } wasMotionBefore = false; isLongMotion = false; } } } /* * 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) * I highly recommend checking out the basics of ArduinoJson serialization and deserialization in order to use custom settings! */ // void addToConfig(JsonObject& root) // { // JsonObject top = root.createNestedObject("VL53L0x"); // JsonArray pins = top.createNestedArray("pin"); // pins.add(i2c_scl); // pins.add(i2c_sda); // } /* * 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_VL53L0X; } }; static UsermodVL53L0XGestures vl53l0x_gestures; REGISTER_USERMOD(vl53l0x_gestures); ================================================ FILE: usermods/VL53L0X_gestures/library.json ================================================ { "name": "VL53L0X_gestures", "build": { "libArchive": false}, "dependencies": { "pololu/VL53L0X" : "^1.3.0" } } ================================================ FILE: usermods/VL53L0X_gestures/readme.md ================================================ # Description Implements support of simple hand gestures via a VL53L0X sensor: on/off and brightness adjustment. Useful for controlling strips when you want to avoid touching anything. - on/off - swipe your hand below the sensor ("shortPressAction" is called. Can be customized via WLED macros) - brightness adjustment - hold your hand below the sensor for 1 second to switch to "brightness" mode. adjust the brightness by changing the distance between your hand and the sensor (see parameters below for customization). ## Installation 1. Attach VL53L0X sensor to i2c pins according to default pins for your board. 2. Add `-D USERMOD_VL53L0X_GESTURES` to your build flags at platformio.ini (plaformio_override.ini) for needed environment. ================================================ FILE: usermods/Wemos_D1_mini+Wemos32_mini_shield/readme.md ================================================ # Wemos D1 mini and Wemos32 mini shield - Installation of file: Copy and replace file in wled00 directory - For BME280 sensor use usermod_bme280.cpp. Copy to wled00 and rename to usermod.cpp - Added third choice of controller Heltec WiFi-Kit-8. Totally DIY but with OLED display. ## Project repository - [Original repository](https://github.com/srg74/WLED-wemos-shield) - WLED Wemos shield repository - [Wemos shield project Wiki](https://github.com/srg74/WLED-wemos-shield/wiki) - [Precompiled WLED firmware](https://github.com/srg74/WLED-wemos-shield/tree/master/resources/Firmware) ## Features - SSD1306 128x32 or 128x64 I2C OLED display - On screen IP address, SSID and controller status (e.g. ON or OFF, recent effect) - Auto display shutoff for extending display lifetime - Dallas temperature sensor - Reporting temperature to MQTT broker - Relay for saving energy ## Hardware ![Shield](https://github.com/srg74/WLED-wemos-shield/blob/master/resources/Images/Assembly_8.jpg) ## Functionality checked with - Wemos D1 mini original v3.1 and clones - Wemos32 mini - PlatformIO - SSD1306 128x32 I2C OLED display - DS18B20 (temperature sensor) - BME280 (temperature, humidity and pressure sensor) - Push button (N.O. momentary switch) ### Platformio requirements For Dallas sensor uncomment `U8g2@~2.27.3`,`DallasTemperature@~3.8.0`,`OneWire@~2.3.5 under` `[common]` section in `platformio.ini`: ```ini # platformio.ini ... [platformio] ... ; default_envs = esp07 default_envs = d1_mini ... [common] ... lib_deps_external = ... #For use SSD1306 OLED display uncomment following U8g2@~2.27.3 #For Dallas sensor uncomment following 2 lines DallasTemperature@~3.8.0 OneWire@~2.3.5 ... ``` For BME280 sensor uncomment `U8g2@~2.27.3`,`BME280@~3.0.0 under` `[common]` section in `platformio.ini`: ```ini # platformio.ini ... [platformio] ... ; default_envs = esp07 default_envs = d1_mini ... [common] ... lib_deps_external = ... #For use SSD1306 OLED display uncomment following U8g2@~2.27.3 #For BME280 sensor uncomment following BME280@~3.0.0 ... ``` ================================================ FILE: usermods/Wemos_D1_mini+Wemos32_mini_shield/usermod.cpp ================================================ #include "wled.h" #include #include // from https://github.com/olikraus/u8g2/ #include // Dallas temperature sensor //Dallas sensor quick reading. Credit to - Author: Peter Scargill, August 17th, 2013 int16_t Dallas(int x, byte start) { OneWire DallasSensor(x); byte i; byte data[2]; int16_t result; do { DallasSensor.reset(); DallasSensor.write(0xCC); DallasSensor.write(0xBE); for ( i = 0; i < 2; i++) data[i] = DallasSensor.read(); result=(data[1]<<8)|data[0]; result>>=4; if (data[1]&128) result|=61440; if (data[0]&8) ++result; DallasSensor.reset(); DallasSensor.write(0xCC); DallasSensor.write(0x44,1); if (start) delay(1000); } while (start--); return result; } #ifdef ARDUINO_ARCH_ESP32 uint8_t SCL_PIN = 22; uint8_t SDA_PIN = 21; uint8_t DALLAS_PIN =23; #else uint8_t SCL_PIN = 5; uint8_t SDA_PIN = 4; uint8_t DALLAS_PIN =13; // uint8_t RST_PIN = 16; // Un-comment for Heltec WiFi-Kit-8 #endif //The SCL and SDA pins are defined here. //ESP8266 Wemos D1 mini board use SCL=5 SDA=4 while ESP32 Wemos32 mini board use SCL=22 SDA=21 #define U8X8_PIN_SCL SCL_PIN #define U8X8_PIN_SDA SDA_PIN //#define U8X8_PIN_RESET RST_PIN // Un-comment for Heltec WiFi-Kit-8 // Dallas sensor reading timer long temptimer = millis(); long lastMeasure = 0; #define Celsius // Show temperature measurement in Celsius otherwise is in Fahrenheit // 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 // --> First choice of cheap I2C OLED 128X32 0.91" U8X8_SSD1306_128X32_UNIVISION_HW_I2C u8x8(U8X8_PIN_NONE, U8X8_PIN_SCL, U8X8_PIN_SDA); // Pins are Reset, SCL, SDA // --> Second choice of cheap I2C OLED 128X64 0.96" or 1.3" //U8X8_SSD1306_128X64_NONAME_HW_I2C u8x8(U8X8_PIN_NONE, U8X8_PIN_SCL, U8X8_PIN_SDA); // Pins are Reset, SCL, SDA // --> Third choice of Heltec WiFi-Kit-8 OLED 128X32 0.91" //U8X8_SSD1306_128X32_UNIVISION_HW_I2C u8x8(U8X8_PIN_RESET, U8X8_PIN_SCL, U8X8_PIN_SDA); // Constructor for Heltec WiFi-Kit-8 // gets called once at boot. Do all initialization that doesn't depend on network here void userSetup() { //Serial.begin(115200); Dallas (DALLAS_PIN,1); u8x8.begin(); u8x8.setPowerSave(0); u8x8.setFlipMode(1); 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.drawString(0, 0, "Loading..."); } // gets called every time WiFi is (re-)connected. Initialize own network // interfaces here void userConnected() {} // 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 knownMode = 0; uint8_t knownPalette = 0; long lastUpdate = 0; long lastRedraw = 0; bool displayTurnedOff = false; // How often we are redrawing screen #define USER_LOOP_REFRESH_RATE_MS 5000 void userLoop() { //----> Dallas temperature sensor MQTT publishing temptimer = millis(); // Timer to publish new temperature every 60 seconds if (temptimer - lastMeasure > 60000) { lastMeasure = temptimer; #ifndef WLED_DISABLE_MQTT //Check if MQTT Connected, otherwise it will crash the 8266 if (mqtt != nullptr) { // Serial.println(Dallas(DALLAS_PIN,0)); //Gets preferred temperature scale based on selection in definitions section #ifdef Celsius int16_t board_temperature = Dallas(DALLAS_PIN,0); #else int16_t board_temperature = (Dallas(DALLAS_PIN,0)* 1.8 + 32); #endif //Create character string populated with user defined device topic from the UI, and the read temperature. Then publish to MQTT server. String t = String(mqttDeviceTopic); t += "/temperature"; mqtt->publish(t.c_str(), 0, true, String(board_temperature).c_str()); } #endif } // Check if we time interval for redrawing passes. if (millis() - lastUpdate < USER_LOOP_REFRESH_RATE_MS) { return; } lastUpdate = millis(); // Turn off display after 3 minutes with no change. if(!displayTurnedOff && millis() - lastRedraw > 3*60*1000) { u8x8.setPowerSave(1); displayTurnedOff = true; } // Check if values which are shown on display changed from the last time. 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 (knownMode != strip.getMainSegment().mode) { needRedraw = true; } else if (knownPalette != strip.getMainSegment().palette) { needRedraw = true; } if (!needRedraw) { return; } needRedraw = false; if (displayTurnedOff) { u8x8.setPowerSave(0); displayTurnedOff = false; } lastRedraw = millis(); // Update last known values. #ifdef ARDUINO_ARCH_ESP32 knownSsid = WiFi.SSID(); #else knownSsid = apActive ? WiFi.softAPSSID() : WiFi.SSID(); #endif knownIp = apActive ? IPAddress(4, 3, 2, 1) : WiFi.localIP(); knownBrightness = bri; knownMode = strip.getMainSegment().mode; knownPalette = strip.getMainSegment().palette; u8x8.clear(); u8x8.setFont(u8x8_font_chroma48medium8_r); // First row with Wifi name u8x8.setCursor(1, 0); u8x8.print(knownSsid.substring(0, u8x8.getCols() > 1 ? u8x8.getCols() - 2 : 0)); // Print `~` char to indicate that SSID is longer than our display if (knownSsid.length() > u8x8.getCols()) u8x8.print("~"); // Second row with IP or Password u8x8.setCursor(1, 1); // Print password in AP mode and if led is OFF. if (apActive && bri == 0) u8x8.print(apPass); else u8x8.print(knownIp); // Third row with mode name u8x8.setCursor(2, 2); char lineBuffer[17]; extractModeName(knownMode, JSON_mode_names, lineBuffer, 16); u8x8.print(lineBuffer); // Fourth row with palette name u8x8.setCursor(2, 3); extractModeName(knownPalette, JSON_palette_names, lineBuffer, 16); u8x8.print(lineBuffer); u8x8.setFont(u8x8_font_open_iconic_embedded_1x1); u8x8.drawGlyph(0, 0, 80); // wifi icon u8x8.drawGlyph(0, 1, 68); // home icon u8x8.setFont(u8x8_font_open_iconic_weather_2x2); u8x8.drawGlyph(0, 2, 66 + (bri > 0 ? 3 : 0)); // sun/moon icon } ================================================ FILE: usermods/Wemos_D1_mini+Wemos32_mini_shield/usermod_bme280.cpp ================================================ #include "wled.h" #include #include // from https://github.com/olikraus/u8g2/ #include #include //BME280 sensor void UpdateBME280Data(); #define Celsius // Show temperature measurement in Celsius otherwise is in Fahrenheit BME280I2C bme; // Default : forced mode, standby time = 1000 ms // Oversampling = pressure ×1, temperature ×1, humidity ×1, filter off, #ifdef ARDUINO_ARCH_ESP32 //ESP32 boards uint8_t SCL_PIN = 22; uint8_t SDA_PIN = 21; #else //ESP8266 boards uint8_t SCL_PIN = 5; uint8_t SDA_PIN = 4; // uint8_t RST_PIN = 16; // Un-comment for Heltec WiFi-Kit-8 #endif //The SCL and SDA pins are defined here. //ESP8266 Wemos D1 mini board use SCL=5 SDA=4 while ESP32 Wemos32 mini board use SCL=22 SDA=21 #define U8X8_PIN_SCL SCL_PIN #define U8X8_PIN_SDA SDA_PIN //#define U8X8_PIN_RESET RST_PIN // Un-comment for Heltec WiFi-Kit-8 // 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 // --> First choice of cheap I2C OLED 128X32 0.91" U8X8_SSD1306_128X32_UNIVISION_HW_I2C u8x8(U8X8_PIN_NONE, U8X8_PIN_SCL, U8X8_PIN_SDA); // Pins are Reset, SCL, SDA // --> Second choice of cheap I2C OLED 128X64 0.96" or 1.3" //U8X8_SSD1306_128X64_NONAME_HW_I2C u8x8(U8X8_PIN_NONE, U8X8_PIN_SCL, U8X8_PIN_SDA); // Pins are Reset, SCL, SDA // --> Third choice of Heltec WiFi-Kit-8 OLED 128X32 0.91" //U8X8_SSD1306_128X32_UNIVISION_HW_I2C u8x8(U8X8_PIN_RESET, U8X8_PIN_SCL, U8X8_PIN_SDA); // Constructor for Heltec WiFi-Kit-8 // gets called once at boot. Do all initialization that doesn't depend on network here // BME280 sensor timer long tempTimer = millis(); long lastMeasure = 0; float SensorPressure(NAN); float SensorTemperature(NAN); float SensorHumidity(NAN); void userSetup() { u8x8.begin(); u8x8.setPowerSave(0); u8x8.setFlipMode(1); 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.drawString(0, 0, "Loading..."); Wire.begin(SDA_PIN,SCL_PIN); while(!bme.begin()) { Serial.println("Could not find BME280I2C sensor!"); delay(1000); } switch(bme.chipModel()) { case BME280::ChipModel_BME280: Serial.println("Found BME280 sensor! Success."); break; case BME280::ChipModel_BMP280: Serial.println("Found BMP280 sensor! No Humidity available."); break; default: Serial.println("Found UNKNOWN sensor! Error!"); } } // gets called every time WiFi is (re-)connected. Initialize own network // interfaces here void userConnected() {} // 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 knownMode = 0; uint8_t knownPalette = 0; long lastUpdate = 0; long lastRedraw = 0; bool displayTurnedOff = false; // How often we are redrawing screen #define USER_LOOP_REFRESH_RATE_MS 5000 void userLoop() { // BME280 sensor MQTT publishing tempTimer = millis(); // Timer to publish new sensor data every 60 seconds if (tempTimer - lastMeasure > 60000) { lastMeasure = tempTimer; #ifndef WLED_DISABLE_MQTT // Check if MQTT Connected, otherwise it will crash the 8266 if (mqtt != nullptr) { UpdateBME280Data(); float board_temperature = SensorTemperature; float board_pressure = SensorPressure; float board_humidity = SensorHumidity; // Create string populated with user defined device topic from the UI, and the read temperature, humidity and pressure. Then publish to MQTT server. String t = String(mqttDeviceTopic); t += "/temperature"; mqtt->publish(t.c_str(), 0, true, String(board_temperature).c_str()); String p = String(mqttDeviceTopic); p += "/pressure"; mqtt->publish(p.c_str(), 0, true, String(board_pressure).c_str()); String h = String(mqttDeviceTopic); h += "/humidity"; mqtt->publish(h.c_str(), 0, true, String(board_humidity).c_str()); } #endif } // Check if we time interval for redrawing passes. if (millis() - lastUpdate < USER_LOOP_REFRESH_RATE_MS) { return; } lastUpdate = millis(); // Turn off display after 3 minutes with no change. if(!displayTurnedOff && millis() - lastRedraw > 3*60*1000) { u8x8.setPowerSave(1); displayTurnedOff = true; } // Check if values which are shown on display changed from the last time. 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 (knownMode != strip.getMainSegment().mode) { needRedraw = true; } else if (knownPalette != strip.getMainSegment().palette) { needRedraw = true; } if (!needRedraw) { return; } needRedraw = false; if (displayTurnedOff) { u8x8.setPowerSave(0); displayTurnedOff = false; } lastRedraw = millis(); // 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.getMainSegment().mode; knownPalette = strip.getMainSegment().palette; u8x8.clear(); u8x8.setFont(u8x8_font_chroma48medium8_r); // First row with Wifi name u8x8.setCursor(1, 0); u8x8.print(knownSsid.substring(0, u8x8.getCols() > 1 ? u8x8.getCols() - 2 : 0)); // Print `~` char to indicate that SSID is longer, than our display if (knownSsid.length() > u8x8.getCols()) u8x8.print("~"); // Second row with IP or Password u8x8.setCursor(1, 1); // Print password in AP mode and if led is OFF. if (apActive && bri == 0) u8x8.print(apPass); else u8x8.print(knownIp); // Third row with mode name u8x8.setCursor(2, 2); char lineBuffer[17]; extractModeName(knownMode, JSON_mode_names, lineBuffer, 16); u8x8.print(lineBuffer); // Fourth row with palette name u8x8.setCursor(2, 3); extractModeName(knownPalette, JSON_palette_names, lineBuffer, 16); u8x8.print(lineBuffer); u8x8.setFont(u8x8_font_open_iconic_embedded_1x1); u8x8.drawGlyph(0, 0, 80); // wifi icon u8x8.drawGlyph(0, 1, 68); // home icon u8x8.setFont(u8x8_font_open_iconic_weather_2x2); u8x8.drawGlyph(0, 2, 66 + (bri > 0 ? 3 : 0)); // sun/moon icon } void UpdateBME280Data() { float temp(NAN), hum(NAN), pres(NAN); #ifdef Celsius BME280::TempUnit tempUnit(BME280::TempUnit_Celsius); BME280::PresUnit presUnit(BME280::PresUnit_Pa); bme.read(pres, temp, hum, tempUnit, presUnit); #else BME280::TempUnit tempUnit(BME280::TempUnit_Fahrenheit); BME280::PresUnit presUnit(BME280::PresUnit_Pa); bme.read(pres, temp, hum, tempUnit, presUnit); #endif SensorTemperature=temp; SensorHumidity=hum; SensorPressure=pres; } ================================================ FILE: usermods/audioreactive/audio_reactive.cpp ================================================ #include "wled.h" #ifdef ARDUINO_ARCH_ESP32 #include #include #endif #if defined(ARDUINO_ARCH_ESP32) && (defined(WLED_DEBUG) || defined(SR_DEBUG)) #include #endif /* * Usermods allow you to add own functionality to WLED more easily * See: https://github.com/wled-dev/WLED/wiki/Add-own-functionality * * This is an audioreactive v2 usermod. * .... */ #if !defined(FFTTASK_PRIORITY) #define FFTTASK_PRIORITY 1 // standard: looptask prio //#define FFTTASK_PRIORITY 2 // above looptask, below asyc_tcp //#define FFTTASK_PRIORITY 4 // above asyc_tcp #endif // Comment/Uncomment to toggle usb serial debugging // #define MIC_LOGGER // MIC sampling & sound input debugging (serial plotter) // #define FFT_SAMPLING_LOG // FFT result debugging // #define SR_DEBUG // generic SR DEBUG messages #ifdef SR_DEBUG #define DEBUGSR_PRINT(x) DEBUGOUT.print(x) #define DEBUGSR_PRINTLN(x) DEBUGOUT.println(x) #define DEBUGSR_PRINTF(x...) DEBUGOUT.printf(x) #else #define DEBUGSR_PRINT(x) #define DEBUGSR_PRINTLN(x) #define DEBUGSR_PRINTF(x...) #endif #if defined(MIC_LOGGER) || defined(FFT_SAMPLING_LOG) #define PLOT_PRINT(x) DEBUGOUT.print(x) #define PLOT_PRINTLN(x) DEBUGOUT.println(x) #define PLOT_PRINTF(x...) DEBUGOUT.printf(x) #else #define PLOT_PRINT(x) #define PLOT_PRINTLN(x) #define PLOT_PRINTF(x...) #endif #define MAX_PALETTES 3 static volatile bool disableSoundProcessing = false; // if true, sound processing (FFT, filters, AGC) will be suspended. "volatile" as its shared between tasks. static uint8_t audioSyncEnabled = 0; // bit field: bit 0 - send, bit 1 - receive (config value) static bool udpSyncConnected = false; // UDP connection status -> true if connected to multicast group #define NUM_GEQ_CHANNELS 16 // number of frequency channels. Don't change !! // audioreactive variables #ifdef ARDUINO_ARCH_ESP32 #ifndef SR_AGC // Automatic gain control mode #define SR_AGC 0 // default mode = off #endif static float micDataReal = 0.0f; // MicIn data with full 24bit resolution - lowest 8bit after decimal point static float multAgc = 1.0f; // sample * multAgc = sampleAgc. Our AGC multiplier static float sampleAvg = 0.0f; // Smoothed Average sample - sampleAvg < 1 means "quiet" (simple noise gate) static float sampleAgc = 0.0f; // Smoothed AGC sample static uint8_t soundAgc = SR_AGC; // Automatic gain control: 0 - off, 1 - normal, 2 - vivid, 3 - lazy (config value) #endif //static float volumeSmth = 0.0f; // either sampleAvg or sampleAgc depending on soundAgc; smoothed sample static float FFT_MajorPeak = 1.0f; // FFT: strongest (peak) frequency static float FFT_Magnitude = 0.0f; // FFT: volume (magnitude) of peak frequency static bool samplePeak = false; // Boolean flag for peak - used in effects. Responding routine may reset this flag. Auto-reset after strip.getFrameTime() static bool udpSamplePeak = false; // Boolean flag for peak. Set at the same time as samplePeak, but reset by transmitAudioData static unsigned long timeOfPeak = 0; // time of last sample peak detection. static uint8_t fftResult[NUM_GEQ_CHANNELS]= {0};// Our calculated freq. channel result table to be used by effects // TODO: probably best not used by receive nodes //static float agcSensitivity = 128; // AGC sensitivity estimation, based on agc gain (multAgc). calculated by getSensitivity(). range 0..255 // user settable parameters for limitSoundDynamics() #ifdef UM_AUDIOREACTIVE_DYNAMICS_LIMITER_OFF static bool limiterOn = false; // bool: enable / disable dynamics limiter #else static bool limiterOn = true; #endif static uint16_t attackTime = 80; // int: attack time in milliseconds. Default 0.08sec static uint16_t decayTime = 1400; // int: decay time in milliseconds. Default 1.40sec // peak detection #ifdef ARDUINO_ARCH_ESP32 static void detectSamplePeak(void); // peak detection function (needs scaled FFT results in vReal[]) - no used for 8266 receive-only mode #endif static void autoResetPeak(void); // peak auto-reset function static uint8_t maxVol = 31; // (was 10) Reasonable value for constant volume for 'peak detector', as it won't always trigger (deprecated) static uint8_t binNum = 8; // Used to select the bin for FFT based beat detection (deprecated) #ifdef ARDUINO_ARCH_ESP32 // use audio source class (ESP32 specific) #include "audio_source.h" constexpr i2s_port_t I2S_PORT = I2S_NUM_0; // I2S port to use (do not change !) constexpr int BLOCK_SIZE = 128; // I2S buffer size (samples) // globals static uint8_t inputLevel = 128; // UI slider value #ifndef SR_SQUELCH uint8_t soundSquelch = 10; // squelch value for volume reactive routines (config value) #else uint8_t soundSquelch = SR_SQUELCH; // squelch value for volume reactive routines (config value) #endif #ifndef SR_GAIN uint8_t sampleGain = 60; // sample gain (config value) #else uint8_t sampleGain = SR_GAIN; // sample gain (config value) #endif // user settable options for FFTResult scaling static uint8_t FFTScalingMode = 3; // 0 none; 1 optimized logarithmic; 2 optimized linear; 3 optimized square root // // AGC presets // Note: in C++, "const" implies "static" - no need to explicitly declare everything as "static const" // #define AGC_NUM_PRESETS 3 // AGC presets: normal, vivid, lazy const double agcSampleDecay[AGC_NUM_PRESETS] = { 0.9994f, 0.9985f, 0.9997f}; // decay factor for sampleMax, in case the current sample is below sampleMax const float agcZoneLow[AGC_NUM_PRESETS] = { 32, 28, 36}; // low volume emergency zone const float agcZoneHigh[AGC_NUM_PRESETS] = { 240, 240, 248}; // high volume emergency zone const float agcZoneStop[AGC_NUM_PRESETS] = { 336, 448, 304}; // disable AGC integrator if we get above this level const float agcTarget0[AGC_NUM_PRESETS] = { 112, 144, 164}; // first AGC setPoint -> between 40% and 65% const float agcTarget0Up[AGC_NUM_PRESETS] = { 88, 64, 116}; // setpoint switching value (a poor man's bang-bang) const float agcTarget1[AGC_NUM_PRESETS] = { 220, 224, 216}; // second AGC setPoint -> around 85% const double agcFollowFast[AGC_NUM_PRESETS] = { 1/192.f, 1/128.f, 1/256.f}; // quickly follow setpoint - ~0.15 sec const double agcFollowSlow[AGC_NUM_PRESETS] = {1/6144.f,1/4096.f,1/8192.f}; // slowly follow setpoint - ~2-15 secs const double agcControlKp[AGC_NUM_PRESETS] = { 0.6f, 1.5f, 0.65f}; // AGC - PI control, proportional gain parameter const double agcControlKi[AGC_NUM_PRESETS] = { 1.7f, 1.85f, 1.2f}; // AGC - PI control, integral gain parameter const float agcSampleSmooth[AGC_NUM_PRESETS] = { 1/12.f, 1/6.f, 1/16.f}; // smoothing factor for sampleAgc (use rawSampleAgc if you want the non-smoothed value) // AGC presets end static AudioSource *audioSource = nullptr; static bool useBandPassFilter = false; // if true, enables a bandpass filter 80Hz-16Khz to remove noise. Applies before FFT. //////////////////// // Begin FFT Code // //////////////////// // some prototypes, to ensure consistent interfaces static float fftAddAvg(int from, int to); // average of several FFT result bins void FFTcode(void * parameter); // audio processing task: read samples, run FFT, fill GEQ channels from FFT results static void runMicFilter(uint16_t numSamples, float *sampleBuffer); // pre-filtering of raw samples (band-pass) static void postProcessFFTResults(bool noiseGateOpen, int numberOfChannels); // post-processing and post-amp of GEQ channels static TaskHandle_t FFT_Task = nullptr; // Table of multiplication factors so that we can even out the frequency response. static float fftResultPink[NUM_GEQ_CHANNELS] = { 1.70f, 1.71f, 1.73f, 1.78f, 1.68f, 1.56f, 1.55f, 1.63f, 1.79f, 1.62f, 1.80f, 2.06f, 2.47f, 3.35f, 6.83f, 9.55f }; // globals and FFT Output variables shared with animations #if defined(WLED_DEBUG) || defined(SR_DEBUG) static uint64_t fftTime = 0; static uint64_t sampleTime = 0; #endif // FFT Task variables (filtering and post-processing) static float fftCalc[NUM_GEQ_CHANNELS] = {0.0f}; // Try and normalize fftBin values to a max of 4096, so that 4096/16 = 256. static float fftAvg[NUM_GEQ_CHANNELS] = {0.0f}; // Calculated frequency channel results, with smoothing (used if dynamics limiter is ON) #ifdef SR_DEBUG static float fftResultMax[NUM_GEQ_CHANNELS] = {0.0f}; // A table used for testing to determine how our post-processing is working. #endif // audio source parameters and constant constexpr SRate_t SAMPLE_RATE = 22050; // Base sample rate in Hz - 22Khz is a standard rate. Physical sample time -> 23ms //constexpr SRate_t SAMPLE_RATE = 16000; // 16kHz - use if FFTtask takes more than 20ms. Physical sample time -> 32ms //constexpr SRate_t SAMPLE_RATE = 20480; // Base sample rate in Hz - 20Khz is experimental. Physical sample time -> 25ms //constexpr SRate_t SAMPLE_RATE = 10240; // Base sample rate in Hz - previous default. Physical sample time -> 50ms #define FFT_MIN_CYCLE 21 // minimum time before FFT task is repeated. Use with 22Khz sampling //#define FFT_MIN_CYCLE 30 // Use with 16Khz sampling //#define FFT_MIN_CYCLE 23 // minimum time before FFT task is repeated. Use with 20Khz sampling //#define FFT_MIN_CYCLE 46 // minimum time before FFT task is repeated. Use with 10Khz sampling // FFT Constants constexpr uint16_t samplesFFT = 512; // Samples in an FFT batch - This value MUST ALWAYS be a power of 2 constexpr uint16_t samplesFFT_2 = 256; // meaningfull part of FFT results - only the "lower half" contains useful information. // the following are observed values, supported by a bit of "educated guessing" //#define FFT_DOWNSCALE 0.65f // 20kHz - downscaling factor for FFT results - "Flat-Top" window @20Khz, old freq channels #define FFT_DOWNSCALE 0.46f // downscaling factor for FFT results - for "Flat-Top" window @22Khz, new freq channels #define LOG_256 5.54517744f // log(256) // These are the input and output vectors. Input vectors receive computed results from FFT. static float* vReal = nullptr; // FFT sample inputs / freq output - these are our raw result bins static float* vImag = nullptr; // imaginary parts // Create FFT object // lib_deps += https://github.com/kosme/arduinoFFT#develop @ 1.9.2 // these options actually cause slow-downs on all esp32 processors, don't use them. // #define FFT_SPEED_OVER_PRECISION // enables use of reciprocals (1/x etc) - not faster on ESP32 // #define FFT_SQRT_APPROXIMATION // enables "quake3" style inverse sqrt - slower on ESP32 // Below options are forcing ArduinoFFT to use sqrtf() instead of sqrt() // #define sqrt_internal sqrtf // see https://github.com/kosme/arduinoFFT/pull/83 - since v2.0.0 this must be done in build_flags #include // FFT object is created in FFTcode // Helper functions // compute average of several FFT result bins static float fftAddAvg(int from, int to) { float result = 0.0f; for (int i = from; i <= to; i++) { result += vReal[i]; } return result / float(to - from + 1); } // // FFT main task // void FFTcode(void * parameter) { DEBUGSR_PRINT("FFT started on core: "); DEBUGSR_PRINTLN(xPortGetCoreID()); // allocate FFT buffers on first call if (vReal == nullptr) vReal = (float*) calloc(samplesFFT, sizeof(float)); if (vImag == nullptr) vImag = (float*) calloc(samplesFFT, sizeof(float)); if ((vReal == nullptr) || (vImag == nullptr)) { // something went wrong if (vReal) free(vReal); vReal = nullptr; if (vImag) free(vImag); vImag = nullptr; return; } // Create FFT object with weighing factor storage ArduinoFFT FFT = ArduinoFFT( vReal, vImag, samplesFFT, SAMPLE_RATE, true); // see https://www.freertos.org/vtaskdelayuntil.html const TickType_t xFrequency = FFT_MIN_CYCLE * portTICK_PERIOD_MS; TickType_t xLastWakeTime = xTaskGetTickCount(); for(;;) { delay(1); // DO NOT DELETE THIS LINE! It is needed to give the IDLE(0) task enough time and to keep the watchdog happy. // taskYIELD(), yield(), vTaskDelay() and esp_task_wdt_feed() didn't seem to work. // Don't run FFT computing code if we're in Receive mode or in realtime mode if (disableSoundProcessing || (audioSyncEnabled & 0x02)) { vTaskDelayUntil( &xLastWakeTime, xFrequency); // release CPU, and let I2S fill its buffers continue; } #if defined(WLED_DEBUG) || defined(SR_DEBUG) uint64_t start = esp_timer_get_time(); bool haveDoneFFT = false; // indicates if second measurement (FFT time) is valid #endif // get a fresh batch of samples from I2S if (audioSource) audioSource->getSamples(vReal, samplesFFT); memset(vImag, 0, samplesFFT * sizeof(float)); // set imaginary parts to 0 #if defined(WLED_DEBUG) || defined(SR_DEBUG) if (start < esp_timer_get_time()) { // filter out overflows uint64_t sampleTimeInMillis = (esp_timer_get_time() - start +5ULL) / 10ULL; // "+5" to ensure proper rounding sampleTime = (sampleTimeInMillis*3 + sampleTime*7)/10; // smooth } start = esp_timer_get_time(); // start measuring FFT time #endif xLastWakeTime = xTaskGetTickCount(); // update "last unblocked time" for vTaskDelay // band pass filter - can reduce noise floor by a factor of 50 // downside: frequencies below 100Hz will be ignored if (useBandPassFilter) runMicFilter(samplesFFT, vReal); // find highest sample in the batch float maxSample = 0.0f; // max sample from FFT batch for (int i=0; i < samplesFFT; i++) { // pick our our current mic sample - we take the max value from all samples that go into FFT if ((vReal[i] <= (INT16_MAX - 1024)) && (vReal[i] >= (INT16_MIN + 1024))) //skip extreme values - normally these are artefacts if (fabsf((float)vReal[i]) > maxSample) maxSample = fabsf((float)vReal[i]); } // release highest sample to volume reactive effects early - not strictly necessary here - could also be done at the end of the function // early release allows the filters (getSample() and agcAvg()) to work with fresh values - we will have matching gain and noise gate values when we want to process the FFT results. micDataReal = maxSample; #ifdef SR_DEBUG if (true) { // this allows measure FFT runtimes, as it disables the "only when needed" optimization #else if (sampleAvg > 0.25f) { // noise gate open means that FFT results will be used. Don't run FFT if results are not needed. #endif // run FFT (takes 3-5ms on ESP32, ~12ms on ESP32-S2) FFT.dcRemoval(); // remove DC offset FFT.windowing( FFTWindow::Flat_top, FFTDirection::Forward); // Weigh data using "Flat Top" function - better amplitude accuracy //FFT.windowing(FFTWindow::Blackman_Harris, FFTDirection::Forward); // Weigh data using "Blackman- Harris" window - sharp peaks due to excellent sideband rejection FFT.compute( FFTDirection::Forward ); // Compute FFT FFT.complexToMagnitude(); // Compute magnitudes vReal[0] = 0; // The remaining DC offset on the signal produces a strong spike on position 0 that should be eliminated to avoid issues. FFT.majorPeak(&FFT_MajorPeak, &FFT_Magnitude); // let the effects know which freq was most dominant FFT_MajorPeak = constrain(FFT_MajorPeak, 1.0f, 11025.0f); // restrict value to range expected by effects #if defined(WLED_DEBUG) || defined(SR_DEBUG) haveDoneFFT = true; #endif } else { // noise gate closed - only clear results as FFT was skipped. MIC samples are still valid when we do this. memset(vReal, 0, samplesFFT * sizeof(float)); FFT_MajorPeak = 1; FFT_Magnitude = 0.001; } for (int i = 0; i < samplesFFT; i++) { float t = fabsf(vReal[i]); // just to be sure - values in fft bins should be positive any way vReal[i] = t / 16.0f; // Reduce magnitude. Want end result to be scaled linear and ~4096 max. } // for() // mapping of FFT result bins to frequency channels if (fabsf(sampleAvg) > 0.5f) { // noise gate open #if 0 /* This FFT post processing is a DIY endeavour. What we really need is someone with sound engineering expertise to do a great job here AND most importantly, that the animations look GREAT as a result. * * Andrew's updated mapping of 256 bins down to the 16 result bins with Sample Freq = 10240, samplesFFT = 512 and some overlap. * Based on testing, the lowest/Start frequency is 60 Hz (with bin 3) and a highest/End frequency of 5120 Hz in bin 255. * Now, Take the 60Hz and multiply by 1.320367784 to get the next frequency and so on until the end. Then determine the bins. * End frequency = Start frequency * multiplier ^ 16 * Multiplier = (End frequency/ Start frequency) ^ 1/16 * Multiplier = 1.320367784 */ // Range fftCalc[ 0] = fftAddAvg(2,4); // 60 - 100 fftCalc[ 1] = fftAddAvg(4,5); // 80 - 120 fftCalc[ 2] = fftAddAvg(5,7); // 100 - 160 fftCalc[ 3] = fftAddAvg(7,9); // 140 - 200 fftCalc[ 4] = fftAddAvg(9,12); // 180 - 260 fftCalc[ 5] = fftAddAvg(12,16); // 240 - 340 fftCalc[ 6] = fftAddAvg(16,21); // 320 - 440 fftCalc[ 7] = fftAddAvg(21,29); // 420 - 600 fftCalc[ 8] = fftAddAvg(29,37); // 580 - 760 fftCalc[ 9] = fftAddAvg(37,48); // 740 - 980 fftCalc[10] = fftAddAvg(48,64); // 960 - 1300 fftCalc[11] = fftAddAvg(64,84); // 1280 - 1700 fftCalc[12] = fftAddAvg(84,111); // 1680 - 2240 fftCalc[13] = fftAddAvg(111,147); // 2220 - 2960 fftCalc[14] = fftAddAvg(147,194); // 2940 - 3900 fftCalc[15] = fftAddAvg(194,250); // 3880 - 5000 // avoid the last 5 bins, which are usually inaccurate #else /* new mapping, optimized for 22050 Hz by softhack007 */ // bins frequency range if (useBandPassFilter) { // skip frequencies below 100hz fftCalc[ 0] = 0.8f * fftAddAvg(3,4); fftCalc[ 1] = 0.9f * fftAddAvg(4,5); fftCalc[ 2] = fftAddAvg(5,6); fftCalc[ 3] = fftAddAvg(6,7); // don't use the last bins from 206 to 255. fftCalc[15] = fftAddAvg(165,205) * 0.75f; // 40 7106 - 8828 high -- with some damping } else { fftCalc[ 0] = fftAddAvg(1,2); // 1 43 - 86 sub-bass fftCalc[ 1] = fftAddAvg(2,3); // 1 86 - 129 bass fftCalc[ 2] = fftAddAvg(3,5); // 2 129 - 216 bass fftCalc[ 3] = fftAddAvg(5,7); // 2 216 - 301 bass + midrange // don't use the last bins from 216 to 255. They are usually contaminated by aliasing (aka noise) fftCalc[15] = fftAddAvg(165,215) * 0.70f; // 50 7106 - 9259 high -- with some damping } fftCalc[ 4] = fftAddAvg(7,10); // 3 301 - 430 midrange fftCalc[ 5] = fftAddAvg(10,13); // 3 430 - 560 midrange fftCalc[ 6] = fftAddAvg(13,19); // 5 560 - 818 midrange fftCalc[ 7] = fftAddAvg(19,26); // 7 818 - 1120 midrange -- 1Khz should always be the center ! fftCalc[ 8] = fftAddAvg(26,33); // 7 1120 - 1421 midrange fftCalc[ 9] = fftAddAvg(33,44); // 9 1421 - 1895 midrange fftCalc[10] = fftAddAvg(44,56); // 12 1895 - 2412 midrange + high mid fftCalc[11] = fftAddAvg(56,70); // 14 2412 - 3015 high mid fftCalc[12] = fftAddAvg(70,86); // 16 3015 - 3704 high mid fftCalc[13] = fftAddAvg(86,104); // 18 3704 - 4479 high mid fftCalc[14] = fftAddAvg(104,165) * 0.88f; // 61 4479 - 7106 high mid + high -- with slight damping #endif } else { // noise gate closed - just decay old values for (int i=0; i < NUM_GEQ_CHANNELS; i++) { fftCalc[i] *= 0.85f; // decay to zero if (fftCalc[i] < 4.0f) fftCalc[i] = 0.0f; } } // post-processing of frequency channels (pink noise adjustment, AGC, smoothing, scaling) postProcessFFTResults((fabsf(sampleAvg) > 0.25f)? true : false , NUM_GEQ_CHANNELS); #if defined(WLED_DEBUG) || defined(SR_DEBUG) if (haveDoneFFT && (start < esp_timer_get_time())) { // filter out overflows uint64_t fftTimeInMillis = ((esp_timer_get_time() - start) +5ULL) / 10ULL; // "+5" to ensure proper rounding fftTime = (fftTimeInMillis*3 + fftTime*7)/10; // smooth } #endif // run peak detection autoResetPeak(); detectSamplePeak(); #if !defined(I2S_GRAB_ADC1_COMPLETELY) if ((audioSource == nullptr) || (audioSource->getType() != AudioSource::Type_I2SAdc)) // the "delay trick" does not help for analog ADC #endif vTaskDelayUntil( &xLastWakeTime, xFrequency); // release CPU, and let I2S fill its buffers } // for(;;)ever } // FFTcode() task end /////////////////////////// // Pre / Postprocessing // /////////////////////////// static void runMicFilter(uint16_t numSamples, float *sampleBuffer) // pre-filtering of raw samples (band-pass) { // low frequency cutoff parameter - see https://dsp.stackexchange.com/questions/40462/exponential-moving-average-cut-off-frequency //constexpr float alpha = 0.04f; // 150Hz //constexpr float alpha = 0.03f; // 110Hz constexpr float alpha = 0.0225f; // 80hz //constexpr float alpha = 0.01693f;// 60hz // high frequency cutoff parameter //constexpr float beta1 = 0.75f; // 11Khz //constexpr float beta1 = 0.82f; // 15Khz //constexpr float beta1 = 0.8285f; // 18Khz constexpr float beta1 = 0.85f; // 20Khz constexpr float beta2 = (1.0f - beta1) / 2.0f; static float last_vals[2] = { 0.0f }; // FIR high freq cutoff filter static float lowfilt = 0.0f; // IIR low frequency cutoff filter for (int i=0; i < numSamples; i++) { // FIR lowpass, to remove high frequency noise float highFilteredSample; if (i < (numSamples-1)) highFilteredSample = beta1*sampleBuffer[i] + beta2*last_vals[0] + beta2*sampleBuffer[i+1]; // smooth out spikes else highFilteredSample = beta1*sampleBuffer[i] + beta2*last_vals[0] + beta2*last_vals[1]; // special handling for last sample in array last_vals[1] = last_vals[0]; last_vals[0] = sampleBuffer[i]; sampleBuffer[i] = highFilteredSample; // IIR highpass, to remove low frequency noise lowfilt += alpha * (sampleBuffer[i] - lowfilt); sampleBuffer[i] = sampleBuffer[i] - lowfilt; } } static void postProcessFFTResults(bool noiseGateOpen, int numberOfChannels) // post-processing and post-amp of GEQ channels { for (int i=0; i < numberOfChannels; i++) { if (noiseGateOpen) { // noise gate open // Adjustment for frequency curves. fftCalc[i] *= fftResultPink[i]; if (FFTScalingMode > 0) fftCalc[i] *= FFT_DOWNSCALE; // adjustment related to FFT windowing function // Manual linear adjustment of gain using sampleGain adjustment for different input types. fftCalc[i] *= soundAgc ? multAgc : ((float)sampleGain/40.0f * (float)inputLevel/128.0f + 1.0f/16.0f); //apply gain, with inputLevel adjustment if(fftCalc[i] < 0) fftCalc[i] = 0; } // smooth results - rise fast, fall slower if(fftCalc[i] > fftAvg[i]) // rise fast fftAvg[i] = fftCalc[i] *0.75f + 0.25f*fftAvg[i]; // will need approx 2 cycles (50ms) for converging against fftCalc[i] else { // fall slow if (decayTime < 1000) fftAvg[i] = fftCalc[i]*0.22f + 0.78f*fftAvg[i]; // approx 5 cycles (225ms) for falling to zero else if (decayTime < 2000) fftAvg[i] = fftCalc[i]*0.17f + 0.83f*fftAvg[i]; // default - approx 9 cycles (225ms) for falling to zero else if (decayTime < 3000) fftAvg[i] = fftCalc[i]*0.14f + 0.86f*fftAvg[i]; // approx 14 cycles (350ms) for falling to zero else fftAvg[i] = fftCalc[i]*0.1f + 0.9f*fftAvg[i]; // approx 20 cycles (500ms) for falling to zero } // constrain internal vars - just to be sure fftCalc[i] = constrain(fftCalc[i], 0.0f, 1023.0f); fftAvg[i] = constrain(fftAvg[i], 0.0f, 1023.0f); float currentResult; if(limiterOn == true) currentResult = fftAvg[i]; else currentResult = fftCalc[i]; switch (FFTScalingMode) { case 1: // Logarithmic scaling currentResult *= 0.42f; // 42 is the answer ;-) currentResult -= 8.0f; // this skips the lowest row, giving some room for peaks if (currentResult > 1.0f) currentResult = logf(currentResult); // log to base "e", which is the fastest log() function else currentResult = 0.0f; // special handling, because log(1) = 0; log(0) = undefined currentResult *= 0.85f + (float(i)/18.0f); // extra up-scaling for high frequencies currentResult = mapf(currentResult, 0, LOG_256, 0, 255); // map [log(1) ... log(255)] to [0 ... 255] break; case 2: // Linear scaling currentResult *= 0.30f; // needs a bit more damping, get stay below 255 currentResult -= 4.0f; // giving a bit more room for peaks if (currentResult < 1.0f) currentResult = 0.0f; currentResult *= 0.85f + (float(i)/1.8f); // extra up-scaling for high frequencies break; case 3: // square root scaling currentResult *= 0.38f; currentResult -= 6.0f; if (currentResult > 1.0f) currentResult = sqrtf(currentResult); else currentResult = 0.0f; // special handling, because sqrt(0) = undefined currentResult *= 0.85f + (float(i)/4.5f); // extra up-scaling for high frequencies currentResult = mapf(currentResult, 0.0, 16.0, 0.0, 255.0); // map [sqrt(1) ... sqrt(256)] to [0 ... 255] break; case 0: default: // no scaling - leave freq bins as-is currentResult -= 4; // just a bit more room for peaks break; } // Now, let's dump it all into fftResult. Need to do this, otherwise other routines might grab fftResult values prematurely. if (soundAgc > 0) { // apply extra "GEQ Gain" if set by user float post_gain = (float)inputLevel/128.0f; if (post_gain < 1.0f) post_gain = ((post_gain -1.0f) * 0.8f) +1.0f; currentResult *= post_gain; } fftResult[i] = constrain((int)currentResult, 0, 255); } } //////////////////// // Peak detection // //////////////////// // peak detection is called from FFT task when vReal[] contains valid FFT results static void detectSamplePeak(void) { bool havePeak = false; // softhack007: this code continuously triggers while amplitude in the selected bin is above a certain threshold. So it does not detect peaks - it detects high activity in a frequency bin. // Poor man's beat detection by seeing if sample > Average + some value. // This goes through ALL of the 255 bins - but ignores stupid settings // Then we got a peak, else we don't. The peak has to time out on its own in order to support UDP sound sync. if ((sampleAvg > 1) && (maxVol > 0) && (binNum > 4) && (vReal[binNum] > maxVol) && ((millis() - timeOfPeak) > 100)) { havePeak = true; } if (havePeak) { samplePeak = true; timeOfPeak = millis(); udpSamplePeak = true; } } #endif static void autoResetPeak(void) { uint16_t peakDelay = max(uint16_t(50), strip.getFrameTime()); if (millis() - timeOfPeak > peakDelay) { // Auto-reset of samplePeak after at least one complete frame has passed. samplePeak = false; if (audioSyncEnabled == 0) udpSamplePeak = false; // this is normally reset by transmitAudioData } } //////////////////// // usermod class // //////////////////// //class name. Use something descriptive and leave the ": public Usermod" part :) class AudioReactive : public Usermod { private: #ifdef ARDUINO_ARCH_ESP32 #ifndef AUDIOPIN int8_t audioPin = -1; #else int8_t audioPin = AUDIOPIN; #endif #ifndef SR_DMTYPE // I2S mic type uint8_t dmType = 1; // 0=none/disabled/analog; 1=generic I2S #define SR_DMTYPE 1 // default type = I2S #else uint8_t dmType = SR_DMTYPE; #endif #ifndef I2S_SDPIN // aka DOUT int8_t i2ssdPin = 32; #else int8_t i2ssdPin = I2S_SDPIN; #endif #ifndef I2S_WSPIN // aka LRCL int8_t i2swsPin = 15; #else int8_t i2swsPin = I2S_WSPIN; #endif #ifndef I2S_CKPIN // aka BCLK int8_t i2sckPin = 14; /*PDM: set to I2S_PIN_NO_CHANGE*/ #else int8_t i2sckPin = I2S_CKPIN; #endif #ifndef MCLK_PIN int8_t mclkPin = I2S_PIN_NO_CHANGE; /* ESP32: only -1, 0, 1, 3 allowed*/ #else int8_t mclkPin = MCLK_PIN; #endif #endif // new "V2" audiosync struct - 44 Bytes struct __attribute__ ((packed)) audioSyncPacket { // "packed" ensures that there are no additional gaps char header[6]; // 06 Bytes offset 0 uint8_t reserved1[2]; // 02 Bytes, offset 6 - gap required by the compiler - not used yet float sampleRaw; // 04 Bytes offset 8 - either "sampleRaw" or "rawSampleAgc" depending on soundAgc setting float sampleSmth; // 04 Bytes offset 12 - either "sampleAvg" or "sampleAgc" depending on soundAgc setting uint8_t samplePeak; // 01 Bytes offset 16 - 0 no peak; >=1 peak detected. In future, this will also provide peak Magnitude uint8_t reserved2; // 01 Bytes offset 17 - for future extensions - not used yet uint8_t fftResult[16]; // 16 Bytes offset 18 uint16_t reserved3; // 02 Bytes, offset 34 - gap required by the compiler - not used yet float FFT_Magnitude; // 04 Bytes offset 36 float FFT_MajorPeak; // 04 Bytes offset 40 }; // old "V1" audiosync struct - 83 Bytes payload, 88 bytes total (with padding added by compiler) - for backwards compatibility struct audioSyncPacket_v1 { char header[6]; // 06 Bytes uint8_t myVals[32]; // 32 Bytes int sampleAgc; // 04 Bytes int sampleRaw; // 04 Bytes float sampleAvg; // 04 Bytes bool samplePeak; // 01 Bytes uint8_t fftResult[16]; // 16 Bytes double FFT_Magnitude; // 08 Bytes double FFT_MajorPeak; // 08 Bytes }; #define UDPSOUND_MAX_PACKET 88 // max packet size for audiosync // set your config variables to their boot default value (this can also be done in readFromConfig() or a constructor if you prefer) #ifdef UM_AUDIOREACTIVE_ENABLE bool enabled = true; #else bool enabled = false; #endif bool initDone = false; bool addPalettes = false; int8_t palettes = 0; // variables for UDP sound sync WiFiUDP fftUdp; // UDP object for sound sync (from WiFi UDP, not Async UDP!) unsigned long lastTime = 0; // last time of running UDP Microphone Sync const uint16_t delayMs = 10; // I don't want to sample too often and overload WLED uint16_t audioSyncPort= 11988;// default port for UDP sound sync bool updateIsRunning = false; // true during OTA. #ifdef ARDUINO_ARCH_ESP32 // used for AGC int last_soundAgc = -1; // used to detect AGC mode change (for resetting AGC internal error buffers) double control_integrated = 0.0; // persistent across calls to agcAvg(); "integrator control" = accumulated error // variables used by getSample() and agcAvg() int16_t micIn = 0; // Current sample starts with negative values and large values, which is why it's 16 bit signed double sampleMax = 0.0; // Max sample over a few seconds. Needed for AGC controller. double micLev = 0.0; // Used to convert returned value to have '0' as minimum. A leveller float expAdjF = 0.0f; // Used for exponential filter. float sampleReal = 0.0f; // "sampleRaw" as float, to provide bits that are lost otherwise (before amplification by sampleGain or inputLevel). Needed for AGC. int16_t sampleRaw = 0; // Current sample. Must only be updated ONCE!!! (amplified mic value by sampleGain and inputLevel) int16_t rawSampleAgc = 0; // not smoothed AGC sample #endif // variables used in effects float volumeSmth = 0.0f; // either sampleAvg or sampleAgc depending on soundAgc; smoothed sample int16_t volumeRaw = 0; // either sampleRaw or rawSampleAgc depending on soundAgc float my_magnitude =0.0f; // FFT_Magnitude, scaled by multAgc // used to feed "Info" Page unsigned long last_UDPTime = 0; // time of last valid UDP sound sync datapacket int receivedFormat = 0; // last received UDP sound sync format - 0=none, 1=v1 (0.13.x), 2=v2 (0.14.x) float maxSample5sec = 0.0f; // max sample (after AGC) in last 5 seconds unsigned long sampleMaxTimer = 0; // last time maxSample5sec was reset #define CYCLE_SAMPLEMAX 3500 // time window for merasuring // strings to reduce flash memory usage (used more than twice) static const char _name[]; static const char _enabled[]; static const char _config[]; static const char _dynamics[]; static const char _frequency[]; static const char _inputLvl[]; #if defined(ARDUINO_ARCH_ESP32) && !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) && !defined(CONFIG_IDF_TARGET_ESP32S3) static const char _analogmic[]; #endif static const char _digitalmic[]; static const char _addPalettes[]; static const char UDP_SYNC_HEADER[]; static const char UDP_SYNC_HEADER_v1[]; // private methods void removeAudioPalettes(void); void createAudioPalettes(void); CRGB getCRGBForBand(int x, int pal); void fillAudioPalettes(void); //////////////////// // Debug support // //////////////////// void logAudio() { if (disableSoundProcessing && (!udpSyncConnected || ((audioSyncEnabled & 0x02) == 0))) return; // no audio availeable #ifdef MIC_LOGGER // Debugging functions for audio input and sound processing. Comment out the values you want to see PLOT_PRINT("micReal:"); PLOT_PRINT(micDataReal); PLOT_PRINT("\t"); PLOT_PRINT("volumeSmth:"); PLOT_PRINT(volumeSmth); PLOT_PRINT("\t"); //PLOT_PRINT("volumeRaw:"); PLOT_PRINT(volumeRaw); PLOT_PRINT("\t"); PLOT_PRINT("DC_Level:"); PLOT_PRINT(micLev); PLOT_PRINT("\t"); //PLOT_PRINT("sampleAgc:"); PLOT_PRINT(sampleAgc); PLOT_PRINT("\t"); //PLOT_PRINT("sampleAvg:"); PLOT_PRINT(sampleAvg); PLOT_PRINT("\t"); //PLOT_PRINT("sampleReal:"); PLOT_PRINT(sampleReal); PLOT_PRINT("\t"); #ifdef ARDUINO_ARCH_ESP32 //PLOT_PRINT("micIn:"); PLOT_PRINT(micIn); PLOT_PRINT("\t"); //PLOT_PRINT("sample:"); PLOT_PRINT(sample); PLOT_PRINT("\t"); //PLOT_PRINT("sampleMax:"); PLOT_PRINT(sampleMax); PLOT_PRINT("\t"); //PLOT_PRINT("samplePeak:"); PLOT_PRINT((samplePeak!=0) ? 128:0); PLOT_PRINT("\t"); //PLOT_PRINT("multAgc:"); PLOT_PRINT(multAgc, 4); PLOT_PRINT("\t"); #endif PLOT_PRINTLN(); #endif #ifdef FFT_SAMPLING_LOG #if 0 for(int i=0; i maxVal) maxVal = fftResult[i]; if(fftResult[i] < minVal) minVal = fftResult[i]; } for(int i = 0; i < NUM_GEQ_CHANNELS; i++) { PLOT_PRINT(i); PLOT_PRINT(":"); PLOT_PRINTF("%04ld ", map(fftResult[i], 0, (scaleValuesFromCurrentMaxVal ? maxVal : defaultScalingFromHighValue), (mapValuesToPlotterSpace*i*scalingToHighValue)+0, (mapValuesToPlotterSpace*i*scalingToHighValue)+scalingToHighValue-1)); } if(printMaxVal) { PLOT_PRINTF("maxVal:%04d ", maxVal + (mapValuesToPlotterSpace ? 16*256 : 0)); } if(printMinVal) { PLOT_PRINTF("%04d:minVal ", minVal); // printed with value first, then label, so negative values can be seen in Serial Monitor but don't throw off y axis in Serial Plotter } if(mapValuesToPlotterSpace) PLOT_PRINTF("max:%04d ", (printMaxVal ? 17 : 16)*256); // print line above the maximum value we expect to see on the plotter to avoid autoscaling y axis else { PLOT_PRINTF("max:%04d ", 256); } PLOT_PRINTLN(); #endif // FFT_SAMPLING_LOG } // logAudio() #ifdef ARDUINO_ARCH_ESP32 ////////////////////// // Audio Processing // ////////////////////// /* * A "PI controller" multiplier to automatically adjust sound sensitivity. * * A few tricks are implemented so that sampleAgc does't only utilize 0% and 100%: * 0. don't amplify anything below squelch (but keep previous gain) * 1. gain input = maximum signal observed in the last 5-10 seconds * 2. we use two setpoints, one at ~60%, and one at ~80% of the maximum signal * 3. the amplification depends on signal level: * a) normal zone - very slow adjustment * b) emergency zone (<10% or >90%) - very fast adjustment */ void agcAvg(unsigned long the_time) { const int AGC_preset = (soundAgc > 0)? (soundAgc-1): 0; // make sure the _compiler_ knows this value will not change while we are inside the function float lastMultAgc = multAgc; // last multiplier used float multAgcTemp = multAgc; // new multiplier float tmpAgc = sampleReal * multAgc; // what-if amplified signal float control_error; // "control error" input for PI control if (last_soundAgc != soundAgc) control_integrated = 0.0; // new preset - reset integrator // For PI controller, we need to have a constant "frequency" // so let's make sure that the control loop is not running at insane speed static unsigned long last_time = 0; unsigned long time_now = millis(); if ((the_time > 0) && (the_time < time_now)) time_now = the_time; // allow caller to override my clock if (time_now - last_time > 2) { last_time = time_now; if((fabsf(sampleReal) < 2.0f) || (sampleMax < 1.0)) { // MIC signal is "squelched" - deliver silence tmpAgc = 0; // we need to "spin down" the intgrated error buffer if (fabs(control_integrated) < 0.01) control_integrated = 0.0; else control_integrated *= 0.91; } else { // compute new setpoint if (tmpAgc <= agcTarget0Up[AGC_preset]) multAgcTemp = agcTarget0[AGC_preset] / sampleMax; // Make the multiplier so that sampleMax * multiplier = first setpoint else multAgcTemp = agcTarget1[AGC_preset] / sampleMax; // Make the multiplier so that sampleMax * multiplier = second setpoint } // limit amplification if (multAgcTemp > 32.0f) multAgcTemp = 32.0f; if (multAgcTemp < 1.0f/64.0f) multAgcTemp = 1.0f/64.0f; // compute error terms control_error = multAgcTemp - lastMultAgc; if (((multAgcTemp > 0.085f) && (multAgcTemp < 6.5f)) //integrator anti-windup by clamping && (multAgc*sampleMax < agcZoneStop[AGC_preset])) //integrator ceiling (>140% of max) control_integrated += control_error * 0.002 * 0.25; // 2ms = integration time; 0.25 for damping else control_integrated *= 0.9; // spin down that beasty integrator // apply PI Control tmpAgc = sampleReal * lastMultAgc; // check "zone" of the signal using previous gain if ((tmpAgc > agcZoneHigh[AGC_preset]) || (tmpAgc < soundSquelch + agcZoneLow[AGC_preset])) { // upper/lower energy zone multAgcTemp = lastMultAgc + agcFollowFast[AGC_preset] * agcControlKp[AGC_preset] * control_error; multAgcTemp += agcFollowFast[AGC_preset] * agcControlKi[AGC_preset] * control_integrated; } else { // "normal zone" multAgcTemp = lastMultAgc + agcFollowSlow[AGC_preset] * agcControlKp[AGC_preset] * control_error; multAgcTemp += agcFollowSlow[AGC_preset] * agcControlKi[AGC_preset] * control_integrated; } // limit amplification again - PI controller sometimes "overshoots" //multAgcTemp = constrain(multAgcTemp, 0.015625f, 32.0f); // 1/64 < multAgcTemp < 32 if (multAgcTemp > 32.0f) multAgcTemp = 32.0f; if (multAgcTemp < 1.0f/64.0f) multAgcTemp = 1.0f/64.0f; } // NOW finally amplify the signal tmpAgc = sampleReal * multAgcTemp; // apply gain to signal if (fabsf(sampleReal) < 2.0f) tmpAgc = 0.0f; // apply squelch threshold //tmpAgc = constrain(tmpAgc, 0, 255); if (tmpAgc > 255) tmpAgc = 255.0f; // limit to 8bit if (tmpAgc < 1) tmpAgc = 0.0f; // just to be sure // update global vars ONCE - multAgc, sampleAGC, rawSampleAgc multAgc = multAgcTemp; rawSampleAgc = 0.8f * tmpAgc + 0.2f * (float)rawSampleAgc; // update smoothed AGC sample if (fabsf(tmpAgc) < 1.0f) sampleAgc = 0.5f * tmpAgc + 0.5f * sampleAgc; // fast path to zero else sampleAgc += agcSampleSmooth[AGC_preset] * (tmpAgc - sampleAgc); // smooth path sampleAgc = fabsf(sampleAgc); // // make sure we have a positive value last_soundAgc = soundAgc; } // agcAvg() // post-processing and filtering of MIC sample (micDataReal) from FFTcode() void getSample() { float sampleAdj; // Gain adjusted sample value float tmpSample; // An interim sample variable used for calculations. const float weighting = 0.2f; // Exponential filter weighting. Will be adjustable in a future release. const int AGC_preset = (soundAgc > 0)? (soundAgc-1): 0; // make sure the _compiler_ knows this value will not change while we are inside the function #ifdef WLED_DISABLE_SOUND micIn = perlin8(millis(), millis()); // Simulated analog read micDataReal = micIn; #else #ifdef ARDUINO_ARCH_ESP32 micIn = int(micDataReal); // micDataSm = ((micData * 3) + micData)/4; #else // this is the minimal code for reading analog mic input on 8266. // warning!! Absolutely experimental code. Audio on 8266 is still not working. Expects a million follow-on problems. static unsigned long lastAnalogTime = 0; static float lastAnalogValue = 0.0f; if (millis() - lastAnalogTime > 20) { micDataReal = analogRead(A0); // read one sample with 10bit resolution. This is a dirty hack, supporting volumereactive effects only. lastAnalogTime = millis(); lastAnalogValue = micDataReal; yield(); } else micDataReal = lastAnalogValue; micIn = int(micDataReal); #endif #endif micLev += (micDataReal-micLev) / 12288.0f; if(micIn < micLev) micLev = ((micLev * 31.0f) + micDataReal) / 32.0f; // align MicLev to lowest input signal micIn -= micLev; // Let's center it to 0 now // Using an exponential filter to smooth out the signal. We'll add controls for this in a future release. float micInNoDC = fabsf(micDataReal - micLev); expAdjF = (weighting * micInNoDC + (1.0f-weighting) * expAdjF); expAdjF = fabsf(expAdjF); // Now (!) take the absolute value expAdjF = (expAdjF <= soundSquelch) ? 0: expAdjF; // simple noise gate if ((soundSquelch == 0) && (expAdjF < 0.25f)) expAdjF = 0; // do something meaningfull when "squelch = 0" tmpSample = expAdjF; micIn = abs(micIn); // And get the absolute value of each sample sampleAdj = tmpSample * sampleGain / 40.0f * inputLevel/128.0f + tmpSample / 16.0f; // Adjust the gain. with inputLevel adjustment sampleReal = tmpSample; sampleAdj = fmax(fmin(sampleAdj, 255), 0); // Question: why are we limiting the value to 8 bits ??? sampleRaw = (int16_t)sampleAdj; // ONLY update sample ONCE!!!! // keep "peak" sample, but decay value if current sample is below peak if ((sampleMax < sampleReal) && (sampleReal > 0.5f)) { sampleMax = sampleMax + 0.5f * (sampleReal - sampleMax); // new peak - with some filtering // another simple way to detect samplePeak - cannot detect beats, but reacts on peak volume if (((binNum < 12) || ((maxVol < 1))) && (millis() - timeOfPeak > 80) && (sampleAvg > 1)) { samplePeak = true; timeOfPeak = millis(); udpSamplePeak = true; } } else { if ((multAgc*sampleMax > agcZoneStop[AGC_preset]) && (soundAgc > 0)) sampleMax += 0.5f * (sampleReal - sampleMax); // over AGC Zone - get back quickly else sampleMax *= agcSampleDecay[AGC_preset]; // signal to zero --> 5-8sec } if (sampleMax < 0.5f) sampleMax = 0.0f; sampleAvg = ((sampleAvg * 15.0f) + sampleAdj) / 16.0f; // Smooth it out over the last 16 samples. sampleAvg = fabsf(sampleAvg); // make sure we have a positive value } // getSample() #endif /* Limits the dynamics of volumeSmth (= sampleAvg or sampleAgc). * does not affect FFTResult[] or volumeRaw ( = sample or rawSampleAgc) */ // effects: Gravimeter, Gravcenter, Gravcentric, Noisefire, Plasmoid, Freqpixels, Freqwave, Gravfreq, (2D Swirl, 2D Waverly) void limitSampleDynamics(void) { const float bigChange = 196; // just a representative number - a large, expected sample value static unsigned long last_time = 0; static float last_volumeSmth = 0.0f; if (limiterOn == false) return; long delta_time = millis() - last_time; delta_time = constrain(delta_time , 1, 1000); // below 1ms -> 1ms; above 1sec -> sily lil hick-up float deltaSample = volumeSmth - last_volumeSmth; if (attackTime > 0) { // user has defined attack time > 0 float maxAttack = bigChange * float(delta_time) / float(attackTime); if (deltaSample > maxAttack) deltaSample = maxAttack; } if (decayTime > 0) { // user has defined decay time > 0 float maxDecay = - bigChange * float(delta_time) / float(decayTime); if (deltaSample < maxDecay) deltaSample = maxDecay; } volumeSmth = last_volumeSmth + deltaSample; last_volumeSmth = volumeSmth; last_time = millis(); } ////////////////////// // UDP Sound Sync // ////////////////////// // try to establish UDP sound sync connection void connectUDPSoundSync(void) { // This function tries to establish a UDP sync connection if needed // necessary as we also want to transmit in "AP Mode", but the standard "connected()" callback only reacts on STA connection static unsigned long last_connection_attempt = 0; if ((audioSyncPort <= 0) || ((audioSyncEnabled & 0x03) == 0)) return; // Sound Sync not enabled if (udpSyncConnected) return; // already connected if (!(apActive || interfacesInited)) return; // neither AP nor other connections availeable if (millis() - last_connection_attempt < 15000) return; // only try once in 15 seconds if (updateIsRunning) return; // if we arrive here, we need a UDP connection but don't have one last_connection_attempt = millis(); connected(); // try to start UDP } #ifdef ARDUINO_ARCH_ESP32 void transmitAudioData() { if (!udpSyncConnected) return; //DEBUGSR_PRINTLN("Transmitting UDP Mic Packet"); audioSyncPacket transmitData; memset(reinterpret_cast(&transmitData), 0, sizeof(transmitData)); // make sure that the packet - including "invisible" padding bytes added by the compiler - is fully initialized strncpy_P(transmitData.header, PSTR(UDP_SYNC_HEADER), 6); // transmit samples that were not modified by limitSampleDynamics() transmitData.sampleRaw = (soundAgc) ? rawSampleAgc: sampleRaw; transmitData.sampleSmth = (soundAgc) ? sampleAgc : sampleAvg; transmitData.samplePeak = udpSamplePeak ? 1:0; udpSamplePeak = false; // Reset udpSamplePeak after we've transmitted it for (int i = 0; i < NUM_GEQ_CHANNELS; i++) { transmitData.fftResult[i] = (uint8_t)constrain(fftResult[i], 0, 254); } transmitData.FFT_Magnitude = my_magnitude; transmitData.FFT_MajorPeak = FFT_MajorPeak; if (fftUdp.beginMulticastPacket() != 0) { // beginMulticastPacket returns 0 in case of error fftUdp.write(reinterpret_cast(&transmitData), sizeof(transmitData)); fftUdp.endPacket(); } return; } // transmitAudioData() #endif static bool isValidUdpSyncVersion(const char *header) { return strncmp_P(header, UDP_SYNC_HEADER, 6) == 0; } static bool isValidUdpSyncVersion_v1(const char *header) { return strncmp_P(header, UDP_SYNC_HEADER_v1, 6) == 0; } void decodeAudioData(int packetSize, uint8_t *fftBuff) { audioSyncPacket receivedPacket; memset(&receivedPacket, 0, sizeof(receivedPacket)); // start clean memcpy(&receivedPacket, fftBuff, min((unsigned)packetSize, (unsigned)sizeof(receivedPacket))); // don't violate alignment - thanks @willmmiles# // update samples for effects volumeSmth = fmaxf(receivedPacket.sampleSmth, 0.0f); volumeRaw = fmaxf(receivedPacket.sampleRaw, 0.0f); #ifdef ARDUINO_ARCH_ESP32 // update internal samples sampleRaw = volumeRaw; sampleAvg = volumeSmth; rawSampleAgc = volumeRaw; sampleAgc = volumeSmth; multAgc = 1.0f; #endif // Only change samplePeak IF it's currently false. // If it's true already, then the animation still needs to respond. autoResetPeak(); if (!samplePeak) { samplePeak = receivedPacket.samplePeak >0 ? true:false; if (samplePeak) timeOfPeak = millis(); //userVar1 = samplePeak; } //These values are only computed by ESP32 for (int i = 0; i < NUM_GEQ_CHANNELS; i++) fftResult[i] = receivedPacket.fftResult[i]; my_magnitude = fmaxf(receivedPacket.FFT_Magnitude, 0.0f); FFT_Magnitude = my_magnitude; FFT_MajorPeak = constrain(receivedPacket.FFT_MajorPeak, 1.0f, 11025.0f); // restrict value to range expected by effects } void decodeAudioData_v1(int packetSize, uint8_t *fftBuff) { audioSyncPacket_v1 *receivedPacket = reinterpret_cast(fftBuff); // update samples for effects volumeSmth = fmaxf(receivedPacket->sampleAgc, 0.0f); volumeRaw = volumeSmth; // V1 format does not have "raw" AGC sample #ifdef ARDUINO_ARCH_ESP32 // update internal samples sampleRaw = fmaxf(receivedPacket->sampleRaw, 0.0f); sampleAvg = fmaxf(receivedPacket->sampleAvg, 0.0f);; sampleAgc = volumeSmth; rawSampleAgc = volumeRaw; multAgc = 1.0f; #endif // Only change samplePeak IF it's currently false. // If it's true already, then the animation still needs to respond. autoResetPeak(); if (!samplePeak) { samplePeak = receivedPacket->samplePeak >0 ? true:false; if (samplePeak) timeOfPeak = millis(); //userVar1 = samplePeak; } //These values are only available on the ESP32 for (int i = 0; i < NUM_GEQ_CHANNELS; i++) fftResult[i] = receivedPacket->fftResult[i]; my_magnitude = fmaxf(receivedPacket->FFT_Magnitude, 0.0); FFT_Magnitude = my_magnitude; FFT_MajorPeak = constrain(receivedPacket->FFT_MajorPeak, 1.0, 11025.0); // restrict value to range expected by effects } bool receiveAudioData() // check & process new data. return TRUE in case that new audio data was received. { if (!udpSyncConnected) return false; bool haveFreshData = false; size_t packetSize = fftUdp.parsePacket(); #ifdef ARDUINO_ARCH_ESP32 if ((packetSize > 0) && ((packetSize < 5) || (packetSize > UDPSOUND_MAX_PACKET))) fftUdp.flush(); // discard invalid packets (too small or too big) - only works on esp32 #endif if ((packetSize > 5) && (packetSize <= UDPSOUND_MAX_PACKET)) { //DEBUGSR_PRINTLN("Received UDP Sync Packet"); uint8_t fftBuff[UDPSOUND_MAX_PACKET+1] = { 0 }; // fixed-size buffer for receiving (stack), to avoid heap fragmentation caused by variable sized arrays fftUdp.read(fftBuff, packetSize); // VERIFY THAT THIS IS A COMPATIBLE PACKET if (packetSize == sizeof(audioSyncPacket) && (isValidUdpSyncVersion((const char *)fftBuff))) { decodeAudioData(packetSize, fftBuff); //DEBUGSR_PRINTLN("Finished parsing UDP Sync Packet v2"); haveFreshData = true; receivedFormat = 2; } else { if (packetSize == sizeof(audioSyncPacket_v1) && (isValidUdpSyncVersion_v1((const char *)fftBuff))) { decodeAudioData_v1(packetSize, fftBuff); //DEBUGSR_PRINTLN("Finished parsing UDP Sync Packet v1"); haveFreshData = true; receivedFormat = 1; } else receivedFormat = 0; // unknown format } } return haveFreshData; } ////////////////////// // usermod functions// ////////////////////// public: //Functions called by WLED or other usermods /* * setup() is called once at boot. WiFi is not yet connected at this point. * You can use it to initialize variables, sensors or similar. * It is called *AFTER* readFromConfig() */ void setup() override { disableSoundProcessing = true; // just to be sure if (!initDone) { // usermod exchangeable data // we will assign all usermod exportable data here as pointers to original variables or arrays and allocate memory for pointers um_data = new um_data_t; um_data->u_size = 8; um_data->u_type = new um_types_t[um_data->u_size]; um_data->u_data = new void*[um_data->u_size]; um_data->u_data[0] = &volumeSmth; //*used (New) um_data->u_type[0] = UMT_FLOAT; um_data->u_data[1] = &volumeRaw; // used (New) um_data->u_type[1] = UMT_UINT16; um_data->u_data[2] = fftResult; //*used (Blurz, DJ Light, Noisemove, GEQ_base, 2D Funky Plank, Akemi) um_data->u_type[2] = UMT_BYTE_ARR; um_data->u_data[3] = &samplePeak; //*used (Puddlepeak, Ripplepeak, Waterfall) um_data->u_type[3] = UMT_BYTE; um_data->u_data[4] = &FFT_MajorPeak; //*used (Ripplepeak, Freqmap, Freqmatrix, Freqpixels, Freqwave, Gravfreq, Rocktaves, Waterfall) um_data->u_type[4] = UMT_FLOAT; um_data->u_data[5] = &my_magnitude; // used (New) um_data->u_type[5] = UMT_FLOAT; um_data->u_data[6] = &maxVol; // assigned in effect function from UI element!!! (Puddlepeak, Ripplepeak, Waterfall) um_data->u_type[6] = UMT_BYTE; um_data->u_data[7] = &binNum; // assigned in effect function from UI element!!! (Puddlepeak, Ripplepeak, Waterfall) um_data->u_type[7] = UMT_BYTE; } #ifdef ARDUINO_ARCH_ESP32 // Reset I2S peripheral for good measure i2s_driver_uninstall(I2S_NUM_0); // E (696) I2S: i2s_driver_uninstall(2006): I2S port 0 has not installed #if !defined(CONFIG_IDF_TARGET_ESP32C3) delay(100); periph_module_reset(PERIPH_I2S0_MODULE); // not possible on -C3 #endif delay(100); // Give that poor microphone some time to setup. useBandPassFilter = false; #if !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) if ((i2sckPin == I2S_PIN_NO_CHANGE) && (i2ssdPin >= 0) && (i2swsPin >= 0) && ((dmType == 1) || (dmType == 4)) ) dmType = 5; // dummy user support: SCK == -1 --means--> PDM microphone #endif switch (dmType) { #if defined(CONFIG_IDF_TARGET_ESP32S2) || defined(CONFIG_IDF_TARGET_ESP32C3) || defined(CONFIG_IDF_TARGET_ESP32S3) // stub cases for not-yet-supported I2S modes on other ESP32 chips case 0: //ADC analog #if defined(CONFIG_IDF_TARGET_ESP32S2) || defined(CONFIG_IDF_TARGET_ESP32C3) case 5: //PDM Microphone #endif #endif case 1: DEBUGSR_PRINT(F("AR: Generic I2S Microphone - ")); DEBUGSR_PRINTLN(F(I2S_MIC_CHANNEL_TEXT)); audioSource = new I2SSource(SAMPLE_RATE, BLOCK_SIZE); delay(100); if (audioSource) audioSource->initialize(i2swsPin, i2ssdPin, i2sckPin); break; case 2: DEBUGSR_PRINTLN(F("AR: ES7243 Microphone (right channel only).")); audioSource = new ES7243(SAMPLE_RATE, BLOCK_SIZE); delay(100); if (audioSource) audioSource->initialize(i2swsPin, i2ssdPin, i2sckPin, mclkPin); break; case 3: DEBUGSR_PRINT(F("AR: SPH0645 Microphone - ")); DEBUGSR_PRINTLN(F(I2S_MIC_CHANNEL_TEXT)); audioSource = new SPH0654(SAMPLE_RATE, BLOCK_SIZE); delay(100); audioSource->initialize(i2swsPin, i2ssdPin, i2sckPin); break; case 4: DEBUGSR_PRINT(F("AR: Generic I2S Microphone with Master Clock - ")); DEBUGSR_PRINTLN(F(I2S_MIC_CHANNEL_TEXT)); audioSource = new I2SSource(SAMPLE_RATE, BLOCK_SIZE, 1.0f/24.0f); delay(100); if (audioSource) audioSource->initialize(i2swsPin, i2ssdPin, i2sckPin, mclkPin); break; #if !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) case 5: DEBUGSR_PRINT(F("AR: Generic PDM Microphone - ")); DEBUGSR_PRINTLN(F(I2S_PDM_MIC_CHANNEL_TEXT)); audioSource = new I2SSource(SAMPLE_RATE, BLOCK_SIZE, 1.0f/4.0f); useBandPassFilter = true; // this reduces the noise floor on SPM1423 from 5% Vpp (~380) down to 0.05% Vpp (~5) delay(100); if (audioSource) audioSource->initialize(i2swsPin, i2ssdPin); break; #endif case 6: DEBUGSR_PRINTLN(F("AR: ES8388 Source")); audioSource = new ES8388Source(SAMPLE_RATE, BLOCK_SIZE); delay(100); if (audioSource) audioSource->initialize(i2swsPin, i2ssdPin, i2sckPin, mclkPin); break; #if !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) && !defined(CONFIG_IDF_TARGET_ESP32S3) // ADC over I2S is only possible on "classic" ESP32 case 0: DEBUGSR_PRINTLN(F("AR: Analog Microphone (left channel only).")); audioSource = new I2SAdcSource(SAMPLE_RATE, BLOCK_SIZE); delay(100); useBandPassFilter = true; // PDM bandpass filter seems to help for bad quality analog if (audioSource) audioSource->initialize(audioPin); break; #endif case 254: // dummy "network receive only" mode if (audioSource) delete audioSource; audioSource = nullptr; disableSoundProcessing = true; audioSyncEnabled = 2; // force udp sound receive mode enabled = true; break; case 255: // 255 = -1 = no audio source // falls through to default default: if (audioSource) delete audioSource; audioSource = nullptr; disableSoundProcessing = true; enabled = false; break; } delay(250); // give microphone enough time to initialise if (!audioSource && (dmType != 254)) enabled = false;// audio failed to initialise #endif if (enabled) onUpdateBegin(false); // create FFT task, and initialize network #ifdef ARDUINO_ARCH_ESP32 if (FFT_Task == nullptr) enabled = false; // FFT task creation failed if((!audioSource) || (!audioSource->isInitialized())) { // audio source failed to initialize. Still stay "enabled", as there might be input arriving via UDP Sound Sync #ifdef WLED_DEBUG DEBUG_PRINTLN(F("AR: Failed to initialize sound input driver. Please check input PIN settings.")); #else DEBUGSR_PRINTLN(F("AR: Failed to initialize sound input driver. Please check input PIN settings.")); #endif disableSoundProcessing = true; } #endif if (enabled) disableSoundProcessing = false; // all good - enable audio processing if (enabled) connectUDPSoundSync(); if (enabled && addPalettes) createAudioPalettes(); initDone = true; } /* * connected() is called every time the WiFi is (re)connected * Use it to initialize network interfaces */ void connected() override { if (udpSyncConnected) { // clean-up: if open, close old UDP sync connection udpSyncConnected = false; fftUdp.stop(); } if (audioSyncPort > 0 && (audioSyncEnabled & 0x03)) { #ifdef ARDUINO_ARCH_ESP32 udpSyncConnected = fftUdp.beginMulticast(IPAddress(239, 0, 0, 1), audioSyncPort); #else udpSyncConnected = fftUdp.beginMulticast(WiFi.localIP(), IPAddress(239, 0, 0, 1), audioSyncPort); #endif } } /* * 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() override { static unsigned long lastUMRun = millis(); if (!enabled) { disableSoundProcessing = true; // keep processing suspended (FFT task) lastUMRun = millis(); // update time keeping return; } // We cannot wait indefinitely before processing audio data if (strip.isUpdating() && (millis() - lastUMRun < 2)) return; // be nice, but not too nice // suspend local sound processing when "real time mode" is active (E131, UDP, ADALIGHT, ARTNET) if ( (realtimeOverride == REALTIME_OVERRIDE_NONE) // please add other overrides here if needed &&( (realtimeMode == REALTIME_MODE_GENERIC) ||(realtimeMode == REALTIME_MODE_E131) ||(realtimeMode == REALTIME_MODE_UDP) ||(realtimeMode == REALTIME_MODE_ADALIGHT) ||(realtimeMode == REALTIME_MODE_ARTNET) ) ) // please add other modes here if needed { #if defined(ARDUINO_ARCH_ESP32) && defined(WLED_DEBUG) if ((disableSoundProcessing == false) && (audioSyncEnabled == 0)) { // we just switched to "disabled" DEBUG_PRINTLN(F("[AR userLoop] realtime mode active - audio processing suspended.")); DEBUG_PRINTF_P(PSTR(" RealtimeMode = %d; RealtimeOverride = %d\n"), int(realtimeMode), int(realtimeOverride)); } #endif disableSoundProcessing = true; } else { #if defined(ARDUINO_ARCH_ESP32) && defined(WLED_DEBUG) if ((disableSoundProcessing == true) && (audioSyncEnabled == 0) && audioSource && audioSource->isInitialized()) { // we just switched to "enabled" DEBUG_PRINTLN(F("[AR userLoop] realtime mode ended - audio processing resumed.")); DEBUG_PRINTF_P(PSTR(" RealtimeMode = %d; RealtimeOverride = %d\n"), int(realtimeMode), int(realtimeOverride)); } #endif if ((disableSoundProcessing == true) && (audioSyncEnabled == 0)) lastUMRun = millis(); // just left "realtime mode" - update timekeeping disableSoundProcessing = false; } if (audioSyncEnabled & 0x02) disableSoundProcessing = true; // make sure everything is disabled IF in audio Receive mode if (audioSyncEnabled & 0x01) disableSoundProcessing = false; // keep running audio IF we're in audio Transmit mode #ifdef ARDUINO_ARCH_ESP32 if (!audioSource || !audioSource->isInitialized()) disableSoundProcessing = true; // no audio source // Only run the sampling code IF we're not in Receive mode or realtime mode if (!(audioSyncEnabled & 0x02) && !disableSoundProcessing) { if (soundAgc > AGC_NUM_PRESETS) soundAgc = 0; // make sure that AGC preset is valid (to avoid array bounds violation) unsigned long t_now = millis(); // remember current time int userloopDelay = int(t_now - lastUMRun); if (lastUMRun == 0) userloopDelay=0; // startup - don't have valid data from last run. #ifdef WLED_DEBUG // complain when audio userloop has been delayed for long time. Currently we need userloop running between 500 and 1500 times per second. // softhack007 disabled temporarily - avoid serial console spam with MANY leds and low FPS //if ((userloopDelay > 65) && !disableSoundProcessing && (audioSyncEnabled == 0)) { // DEBUG_PRINTF_P(PSTR("[AR userLoop] hiccup detected -> was inactive for last %d millis!\n"), userloopDelay); //} #endif // run filters, and repeat in case of loop delays (hick-up compensation) if (userloopDelay <2) userloopDelay = 0; // minor glitch, no problem if (userloopDelay >200) userloopDelay = 200; // limit number of filter re-runs do { getSample(); // run microphone sampling filters agcAvg(t_now - userloopDelay); // Calculated the PI adjusted value as sampleAvg userloopDelay -= 2; // advance "simulated time" by 2ms } while (userloopDelay > 0); lastUMRun = t_now; // update time keeping // update samples for effects (raw, smooth) volumeSmth = (soundAgc) ? sampleAgc : sampleAvg; volumeRaw = (soundAgc) ? rawSampleAgc: sampleRaw; // update FFTMagnitude, taking into account AGC amplification my_magnitude = FFT_Magnitude; // / 16.0f, 8.0f, 4.0f done in effects if (soundAgc) my_magnitude *= multAgc; if (volumeSmth < 1 ) my_magnitude = 0.001f; // noise gate closed - mute limitSampleDynamics(); } // if (!disableSoundProcessing) #endif autoResetPeak(); // auto-reset sample peak after strip minShowDelay if (!udpSyncConnected) udpSamplePeak = false; // reset UDP samplePeak while UDP is unconnected connectUDPSoundSync(); // ensure we have a connection - if needed // UDP Microphone Sync - receive mode if ((audioSyncEnabled & 0x02) && udpSyncConnected) { // Only run the audio listener code if we're in Receive mode static float syncVolumeSmth = 0; bool have_new_sample = false; if (millis() - lastTime > delayMs) { have_new_sample = receiveAudioData(); if (have_new_sample) last_UDPTime = millis(); #ifdef ARDUINO_ARCH_ESP32 else fftUdp.flush(); // Flush udp input buffers if we haven't read it - avoids hickups in receive mode. Does not work on 8266. #endif lastTime = millis(); } if (have_new_sample) syncVolumeSmth = volumeSmth; // remember received sample else volumeSmth = syncVolumeSmth; // restore originally received sample for next run of dynamics limiter limitSampleDynamics(); // run dynamics limiter on received volumeSmth, to hide jumps and hickups } #if defined(MIC_LOGGER) || defined(MIC_SAMPLING_LOG) || defined(FFT_SAMPLING_LOG) static unsigned long lastMicLoggerTime = 0; if (millis()-lastMicLoggerTime > 20) { lastMicLoggerTime = millis(); logAudio(); } #endif // Info Page: keep max sample from last 5 seconds #ifdef ARDUINO_ARCH_ESP32 if ((millis() - sampleMaxTimer) > CYCLE_SAMPLEMAX) { sampleMaxTimer = millis(); maxSample5sec = (0.15f * maxSample5sec) + 0.85f *((soundAgc) ? sampleAgc : sampleAvg); // reset, and start with some smoothing if (sampleAvg < 1) maxSample5sec = 0; // noise gate } else { if ((sampleAvg >= 1)) maxSample5sec = fmaxf(maxSample5sec, (soundAgc) ? rawSampleAgc : sampleRaw); // follow maximum volume } #else // similar functionality for 8266 receive only - use VolumeSmth instead of raw sample data if ((millis() - sampleMaxTimer) > CYCLE_SAMPLEMAX) { sampleMaxTimer = millis(); maxSample5sec = (0.15 * maxSample5sec) + 0.85 * volumeSmth; // reset, and start with some smoothing if (volumeSmth < 1.0f) maxSample5sec = 0; // noise gate if (maxSample5sec < 0.0f) maxSample5sec = 0; // avoid negative values } else { if (volumeSmth >= 1.0f) maxSample5sec = fmaxf(maxSample5sec, volumeRaw); // follow maximum volume } #endif #ifdef ARDUINO_ARCH_ESP32 //UDP Microphone Sync - transmit mode if ((audioSyncEnabled & 0x01) && (millis() - lastTime > 20)) { // Only run the transmit code IF we're in Transmit mode transmitAudioData(); lastTime = millis(); } #endif fillAudioPalettes(); } bool getUMData(um_data_t **data) override { if (!data || !enabled) return false; // no pointer provided by caller or not enabled -> exit *data = um_data; return true; } #ifdef ARDUINO_ARCH_ESP32 void onUpdateBegin(bool init) override { #ifdef WLED_DEBUG fftTime = sampleTime = 0; #endif // gracefully suspend FFT task (if running) disableSoundProcessing = true; // reset sound data micDataReal = 0.0f; volumeRaw = 0; volumeSmth = 0; sampleAgc = 0; sampleAvg = 0; sampleRaw = 0; rawSampleAgc = 0; my_magnitude = 0; FFT_Magnitude = 0; FFT_MajorPeak = 1; multAgc = 1; // reset FFT data memset(fftCalc, 0, sizeof(fftCalc)); memset(fftAvg, 0, sizeof(fftAvg)); memset(fftResult, 0, sizeof(fftResult)); for(int i=(init?0:1); i don't process audio updateIsRunning = init; } #endif #ifdef ARDUINO_ARCH_ESP32 /** * handleButton() can be used to override default button behaviour. Returning true * will prevent button working in a default way. */ bool handleButton(uint8_t b) override { yield(); // crude way of determining if audio input is analog // better would be for AudioSource to implement getType() if (enabled && dmType == 0 && audioPin>=0 && (buttons[b].type == BTN_TYPE_ANALOG || buttons[b].type == BTN_TYPE_ANALOG_INVERTED) ) { return true; } return false; } #endif //////////////////////////// // Settings and Info Page // //////////////////////////// /* * 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) override { #ifdef ARDUINO_ARCH_ESP32 char myStringBuffer[16]; // buffer for snprintf() - not used yet on 8266 #endif JsonObject user = root["u"]; if (user.isNull()) user = root.createNestedObject("u"); JsonArray infoArr = user.createNestedArray(FPSTR(_name)); String uiDomString = F(""); infoArr.add(uiDomString); if (enabled) { #ifdef ARDUINO_ARCH_ESP32 // Input Level Slider if (disableSoundProcessing == false) { // only show slider when audio processing is running if (soundAgc > 0) { infoArr = user.createNestedArray(F("GEQ Input Level")); // if AGC is on, this slider only affects fftResult[] frequencies } else { infoArr = user.createNestedArray(F("Audio Input Level")); } uiDomString = F("
"); // infoArr.add(uiDomString); } #endif // The following can be used for troubleshooting user errors and is so not enclosed in #ifdef WLED_DEBUG // current Audio input infoArr = user.createNestedArray(F("Audio Source")); if (audioSyncEnabled & 0x02) { // UDP sound sync - receive mode infoArr.add(F("UDP sound sync")); if (udpSyncConnected) { if (millis() - last_UDPTime < 2500) infoArr.add(F(" - receiving")); else infoArr.add(F(" - idle")); } else { infoArr.add(F(" - no connection")); } #ifndef ARDUINO_ARCH_ESP32 // substitute for 8266 } else { infoArr.add(F("sound sync Off")); } #else // ESP32 only } else { // Analog or I2S digital input if (audioSource && (audioSource->isInitialized())) { // audio source successfully configured if (audioSource->getType() == AudioSource::Type_I2SAdc) { infoArr.add(F("ADC analog")); } else { if (dmType == 5) infoArr.add(F("PDM digital")); // dmType 5 => generic PDM microphone else infoArr.add(F("I2S digital")); } // input level or "silence" if (maxSample5sec > 1.0f) { float my_usage = 100.0f * (maxSample5sec / 255.0f); snprintf_P(myStringBuffer, 15, PSTR(" - peak %3d%%"), int(my_usage)); infoArr.add(myStringBuffer); } else { infoArr.add(F(" - quiet")); } } else { // error during audio source setup infoArr.add(F("not initialized")); infoArr.add(F(" - check pin settings")); } } // Sound processing (FFT and input filters) infoArr = user.createNestedArray(F("Sound Processing")); if (audioSource && (disableSoundProcessing == false)) { infoArr.add(F("running")); } else { infoArr.add(F("suspended")); } // AGC or manual Gain if ((soundAgc==0) && (disableSoundProcessing == false) && !(audioSyncEnabled & 0x02)) { infoArr = user.createNestedArray(F("Manual Gain")); float myGain = ((float)sampleGain/40.0f * (float)inputLevel/128.0f) + 1.0f/16.0f; // non-AGC gain from presets infoArr.add(roundf(myGain*100.0f) / 100.0f); infoArr.add("x"); } if (soundAgc && (disableSoundProcessing == false) && !(audioSyncEnabled & 0x02)) { infoArr = user.createNestedArray(F("AGC Gain")); infoArr.add(roundf(multAgc*100.0f) / 100.0f); infoArr.add("x"); } #endif // UDP Sound Sync status infoArr = user.createNestedArray(F("UDP Sound Sync")); if (audioSyncEnabled) { if (audioSyncEnabled & 0x01) { infoArr.add(F("send mode")); if ((udpSyncConnected) && (millis() - lastTime < 2500)) infoArr.add(F(" v2")); } else if (audioSyncEnabled & 0x02) { infoArr.add(F("receive mode")); } } else infoArr.add("off"); if (audioSyncEnabled && !udpSyncConnected) infoArr.add(" (unconnected)"); if (audioSyncEnabled && udpSyncConnected && (millis() - last_UDPTime < 2500)) { if (receivedFormat == 1) infoArr.add(F(" v1")); if (receivedFormat == 2) infoArr.add(F(" v2")); } #if defined(WLED_DEBUG) || defined(SR_DEBUG) #ifdef ARDUINO_ARCH_ESP32 infoArr = user.createNestedArray(F("Sampling time")); infoArr.add(float(sampleTime)/100.0f); infoArr.add(" ms"); infoArr = user.createNestedArray(F("FFT time")); infoArr.add(float(fftTime)/100.0f); if ((fftTime/100) >= FFT_MIN_CYCLE) // FFT time over budget -> I2S buffer will overflow infoArr.add("! ms"); else if ((fftTime/80 + sampleTime/80) >= FFT_MIN_CYCLE) // FFT time >75% of budget -> risk of instability infoArr.add(" ms!"); else infoArr.add(" ms"); DEBUGSR_PRINTF("AR Sampling time: %5.2f ms\n", float(sampleTime)/100.0f); DEBUGSR_PRINTF("AR FFT time : %5.2f ms\n", float(fftTime)/100.0f); #endif #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) override { if (!initDone) return; // prevent crash on boot applyPreset() JsonObject usermod = root[FPSTR(_name)]; if (usermod.isNull()) { usermod = root.createNestedObject(FPSTR(_name)); } usermod["on"] = enabled; } /* * 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) override { if (!initDone) return; // prevent crash on boot applyPreset() bool prevEnabled = enabled; JsonObject usermod = root[FPSTR(_name)]; if (!usermod.isNull()) { if (usermod[FPSTR(_enabled)].is()) { enabled = usermod[FPSTR(_enabled)].as(); if (prevEnabled != enabled) onUpdateBegin(!enabled); if (addPalettes) { // add/remove custom/audioreactive palettes if (prevEnabled && !enabled) removeAudioPalettes(); if (!prevEnabled && enabled) createAudioPalettes(); } } #ifdef ARDUINO_ARCH_ESP32 if (usermod[FPSTR(_inputLvl)].is()) { inputLevel = min(255,max(0,usermod[FPSTR(_inputLvl)].as())); } #endif } if (palettes > 0 && root.containsKey(F("rmcpal"))) { // handle removal of custom palettes from JSON call so we don't break things removeAudioPalettes(); } } void onStateChange(uint8_t callMode) override { if (initDone && enabled && addPalettes && palettes==0 && customPalettes.size()(♪ effects only)');")); uiScript.print(F("addInfo(ux+':dynamics:fall',1,'ms (♪ effects only)');")); uiScript.print(F("dd=addDropdown(ux,'frequency:scale');")); uiScript.print(F("addOption(dd,'None',0);")); uiScript.print(F("addOption(dd,'Linear (Amplitude)',2);")); uiScript.print(F("addOption(dd,'Square Root (Energy)',3);")); uiScript.print(F("addOption(dd,'Logarithmic (Loudness)',1);")); #endif uiScript.print(F("dd=addDropdown(ux,'sync:mode');")); uiScript.print(F("addOption(dd,'Off',0);")); #ifdef ARDUINO_ARCH_ESP32 uiScript.print(F("addOption(dd,'Send',1);")); #endif uiScript.print(F("addOption(dd,'Receive',2);")); #ifdef ARDUINO_ARCH_ESP32 uiScript.print(F("addInfo(ux+':digitalmic:type',1,'requires reboot!');")); // 0 is field type, 1 is actual field uiScript.print(F("addInfo(uxp,0,'sd/data/dout','I2S SD');")); uiScript.print(F("addInfo(uxp,1,'ws/clk/lrck','I2S WS');")); uiScript.print(F("addInfo(uxp,2,'sck/bclk','I2S SCK');")); #if !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) && !defined(CONFIG_IDF_TARGET_ESP32S3) uiScript.print(F("addInfo(uxp,3,'only use -1, 0, 1 or 3','I2S MCLK');")); #else uiScript.print(F("addInfo(uxp,3,'master clock','I2S MCLK');")); #endif #endif } /* * handleOverlayDraw() is called just before every show() (LED strip update frame) after effects have set the colors. * Use this to blank out some LEDs or set them to a different color regardless of the set effect mode. * Commonly used for custom clocks (Cronixie, 7 segment) */ //void handleOverlayDraw() override //{ //strip.setPixelColor(0, RGBW32(0,0,0,0)) // set the first pixel to black //} /* * 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() override { return USERMOD_ID_AUDIOREACTIVE; } }; void AudioReactive::removeAudioPalettes(void) { DEBUG_PRINTLN(F("Removing audio palettes.")); while (palettes>0) { customPalettes.pop_back(); DEBUG_PRINTLN(palettes); palettes--; } DEBUG_PRINT(F("Total # of palettes: ")); DEBUG_PRINTLN(customPalettes.size()); } void AudioReactive::createAudioPalettes(void) { DEBUG_PRINT(F("Total # of palettes: ")); DEBUG_PRINTLN(customPalettes.size()); if (palettes) return; DEBUG_PRINTLN(F("Adding audio palettes.")); for (int i=0; i= palettes) lastCustPalette -= palettes; for (int pal=0; pal #include #include // needed for SPH0465 timing workaround (classic ESP32) #if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(4, 4, 0) #if !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32S3) && !defined(CONFIG_IDF_TARGET_ESP32C3) #include #include #endif // type of i2s_config_t.SampleRate was changed from "int" to "unsigned" in IDF 4.4.x #define SRate_t uint32_t #else #define SRate_t int #endif //#include //#include //#include //#include // see https://docs.espressif.com/projects/esp-idf/en/latest/esp32s3/hw-reference/chip-series-comparison.html#related-documents // and https://docs.espressif.com/projects/esp-idf/en/latest/esp32s3/api-reference/peripherals/i2s.html#overview-of-all-modes #if defined(CONFIG_IDF_TARGET_ESP32C2) || defined(CONFIG_IDF_TARGET_ESP32C3) || defined(CONFIG_IDF_TARGET_ESP32C5) || defined(CONFIG_IDF_TARGET_ESP32C6) || defined(CONFIG_IDF_TARGET_ESP32H2) || defined(ESP8266) || defined(ESP8265) // there are two things in these MCUs that could lead to problems with audio processing: // * no floating point hardware (FPU) support - FFT uses float calculations. If done in software, a strong slow-down can be expected (between 8x and 20x) // * single core, so FFT task might slow down other things like LED updates #if !defined(SOC_I2S_NUM) || (SOC_I2S_NUM < 1) #error This audio reactive usermod does not support ESP32-C2 or ESP32-C3. #else #warning This audio reactive usermod does not support ESP32-C2 and ESP32-C3. #endif #endif /* ToDo: remove. ES7243 is controlled via compiler defines Until this configuration is moved to the webinterface */ // if you have problems to get your microphone work on the left channel, uncomment the following line //#define I2S_USE_RIGHT_CHANNEL // (experimental) define this to use right channel (digital mics only) // Uncomment the line below to utilize ADC1 _exclusively_ for I2S sound input. // benefit: analog mic inputs will be sampled contiously -> better response times and less "glitches" // WARNING: this option WILL lock-up your device in case that any other analogRead() operation is performed; // for example if you want to read "analog buttons" //#define I2S_GRAB_ADC1_COMPLETELY // (experimental) continuously sample analog ADC microphone. WARNING will cause analogRead() lock-up // data type requested from the I2S driver - currently we always use 32bit //#define I2S_USE_16BIT_SAMPLES // (experimental) define this to request 16bit - more efficient but possibly less compatible #ifdef I2S_USE_16BIT_SAMPLES #define I2S_SAMPLE_RESOLUTION I2S_BITS_PER_SAMPLE_16BIT #define I2S_datatype int16_t #define I2S_unsigned_datatype uint16_t #define I2S_data_size I2S_BITS_PER_CHAN_16BIT #undef I2S_SAMPLE_DOWNSCALE_TO_16BIT #else #define I2S_SAMPLE_RESOLUTION I2S_BITS_PER_SAMPLE_32BIT //#define I2S_SAMPLE_RESOLUTION I2S_BITS_PER_SAMPLE_24BIT #define I2S_datatype int32_t #define I2S_unsigned_datatype uint32_t #define I2S_data_size I2S_BITS_PER_CHAN_32BIT #define I2S_SAMPLE_DOWNSCALE_TO_16BIT #endif /* There are several (confusing) options in IDF 4.4.x: * I2S_CHANNEL_FMT_RIGHT_LEFT, I2S_CHANNEL_FMT_ALL_RIGHT and I2S_CHANNEL_FMT_ALL_LEFT stands for stereo mode, which means two channels will transport different data. * I2S_CHANNEL_FMT_ONLY_RIGHT and I2S_CHANNEL_FMT_ONLY_LEFT they are mono mode, both channels will only transport same data. * I2S_CHANNEL_FMT_MULTIPLE means TDM channels, up to 16 channel will available, and they are stereo as default. * if you want to receive two channels, one is the actual data from microphone and another channel is suppose to receive 0, it's different data in two channels, you need to choose I2S_CHANNEL_FMT_RIGHT_LEFT in this case. */ #if (ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(4, 4, 0)) && (ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(4, 5, 0)) // espressif bug: only_left has no sound, left and right are swapped // https://github.com/espressif/esp-idf/issues/9635 I2S mic not working since 4.4 (IDFGH-8138) // https://github.com/espressif/esp-idf/issues/8538 I2S channel selection issue? (IDFGH-6918) // https://github.com/espressif/esp-idf/issues/6625 I2S: left/right channels are swapped for read (IDFGH-4826) #ifdef I2S_USE_RIGHT_CHANNEL #define I2S_MIC_CHANNEL I2S_CHANNEL_FMT_ONLY_LEFT #define I2S_MIC_CHANNEL_TEXT "right channel only (work-around swapped channel bug in IDF 4.4)." #define I2S_PDM_MIC_CHANNEL I2S_CHANNEL_FMT_ONLY_RIGHT #define I2S_PDM_MIC_CHANNEL_TEXT "right channel only" #else //#define I2S_MIC_CHANNEL I2S_CHANNEL_FMT_ALL_LEFT //#define I2S_MIC_CHANNEL I2S_CHANNEL_FMT_RIGHT_LEFT #define I2S_MIC_CHANNEL I2S_CHANNEL_FMT_ONLY_RIGHT #define I2S_MIC_CHANNEL_TEXT "left channel only (work-around swapped channel bug in IDF 4.4)." #define I2S_PDM_MIC_CHANNEL I2S_CHANNEL_FMT_ONLY_LEFT #define I2S_PDM_MIC_CHANNEL_TEXT "left channel only." #endif #else // not swapped #ifdef I2S_USE_RIGHT_CHANNEL #define I2S_MIC_CHANNEL I2S_CHANNEL_FMT_ONLY_RIGHT #define I2S_MIC_CHANNEL_TEXT "right channel only." #else #define I2S_MIC_CHANNEL I2S_CHANNEL_FMT_ONLY_LEFT #define I2S_MIC_CHANNEL_TEXT "left channel only." #endif #define I2S_PDM_MIC_CHANNEL I2S_MIC_CHANNEL #define I2S_PDM_MIC_CHANNEL_TEXT I2S_MIC_CHANNEL_TEXT #endif /* Interface class AudioSource serves as base class for all microphone types This enables accessing all microphones with one single interface which simplifies the caller code */ class AudioSource { public: /* All public methods are virtual, so they can be overridden Everything but the destructor is also removed, to make sure each mic Implementation provides its version of this function */ virtual ~AudioSource() {}; /* Initialize This function needs to take care of anything that needs to be done before samples can be obtained from the microphone. */ virtual void initialize(int8_t = I2S_PIN_NO_CHANGE, int8_t = I2S_PIN_NO_CHANGE, int8_t = I2S_PIN_NO_CHANGE, int8_t = I2S_PIN_NO_CHANGE) = 0; /* Deinitialize Release all resources and deactivate any functionality that is used by this microphone */ virtual void deinitialize() = 0; /* getSamples Read num_samples from the microphone, and store them in the provided buffer */ virtual void getSamples(float *buffer, uint16_t num_samples) = 0; /* check if the audio source driver was initialized successfully */ virtual bool isInitialized(void) {return(_initialized);} /* identify Audiosource type - I2S-ADC or I2S-digital */ typedef enum{Type_unknown=0, Type_I2SAdc=1, Type_I2SDigital=2} AudioSourceType; virtual AudioSourceType getType(void) {return(Type_I2SDigital);} // default is "I2S digital source" - ADC type overrides this method protected: /* Post-process audio sample - currently on needed for I2SAdcSource*/ virtual I2S_datatype postProcessSample(I2S_datatype sample_in) {return(sample_in);} // default method can be overriden by instances (ADC) that need sample postprocessing // Private constructor, to make sure it is not callable except from derived classes AudioSource(SRate_t sampleRate, int blockSize, float sampleScale) : _sampleRate(sampleRate), _blockSize(blockSize), _initialized(false), _sampleScale(sampleScale) {}; SRate_t _sampleRate; // Microphone sampling rate int _blockSize; // I2S block size bool _initialized; // Gets set to true if initialization is successful float _sampleScale; // pre-scaling factor for I2S samples }; /* Basic I2S microphone source All functions are marked virtual, so derived classes can replace them */ class I2SSource : public AudioSource { public: I2SSource(SRate_t sampleRate, int blockSize, float sampleScale = 1.0f) : AudioSource(sampleRate, blockSize, sampleScale) { _config = { .mode = i2s_mode_t(I2S_MODE_MASTER | I2S_MODE_RX), .sample_rate = _sampleRate, .bits_per_sample = I2S_SAMPLE_RESOLUTION, .channel_format = I2S_MIC_CHANNEL, #if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(4, 2, 0) .communication_format = i2s_comm_format_t(I2S_COMM_FORMAT_STAND_I2S), //.intr_alloc_flags = ESP_INTR_FLAG_LEVEL1, .intr_alloc_flags = ESP_INTR_FLAG_LEVEL2, .dma_buf_count = 8, .dma_buf_len = _blockSize, .use_apll = 0, .bits_per_chan = I2S_data_size, #else .communication_format = i2s_comm_format_t(I2S_COMM_FORMAT_I2S | I2S_COMM_FORMAT_I2S_MSB), .intr_alloc_flags = ESP_INTR_FLAG_LEVEL1, .dma_buf_count = 8, .dma_buf_len = _blockSize, .use_apll = false #endif }; } virtual void initialize(int8_t i2swsPin = I2S_PIN_NO_CHANGE, int8_t i2ssdPin = I2S_PIN_NO_CHANGE, int8_t i2sckPin = I2S_PIN_NO_CHANGE, int8_t mclkPin = I2S_PIN_NO_CHANGE) { DEBUGSR_PRINTLN(F("I2SSource:: initialize().")); if (i2swsPin != I2S_PIN_NO_CHANGE && i2ssdPin != I2S_PIN_NO_CHANGE) { if (!PinManager::allocatePin(i2swsPin, true, PinOwner::UM_Audioreactive) || !PinManager::allocatePin(i2ssdPin, false, PinOwner::UM_Audioreactive)) { // #206 DEBUGSR_PRINTF("\nAR: Failed to allocate I2S pins: ws=%d, sd=%d\n", i2swsPin, i2ssdPin); return; } } // i2ssckPin needs special treatment, since it might be unused on PDM mics if (i2sckPin != I2S_PIN_NO_CHANGE) { if (!PinManager::allocatePin(i2sckPin, true, PinOwner::UM_Audioreactive)) { DEBUGSR_PRINTF("\nAR: Failed to allocate I2S pins: sck=%d\n", i2sckPin); return; } } else { #if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(4, 2, 0) #if !defined(SOC_I2S_SUPPORTS_PDM_RX) #warning this MCU does not support PDM microphones #endif #endif #if !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) // This is an I2S PDM microphone, these microphones only use a clock and // data line, to make it simpler to debug, use the WS pin as CLK and SD pin as DATA // example from espressif: https://github.com/espressif/esp-idf/blob/release/v4.4/examples/peripherals/i2s/i2s_audio_recorder_sdcard/main/i2s_recorder_main.c // note to self: PDM has known bugs on S3, and does not work on C3 // * S3: PDM sample rate only at 50% of expected rate: https://github.com/espressif/esp-idf/issues/9893 // * S3: I2S PDM has very low amplitude: https://github.com/espressif/esp-idf/issues/8660 // * C3: does not support PDM to PCM input. SoC would allow PDM RX, but there is no hardware to directly convert to PCM so it will not work. https://github.com/espressif/esp-idf/issues/8796 _config.mode = i2s_mode_t(I2S_MODE_MASTER | I2S_MODE_RX | I2S_MODE_PDM); // Change mode to pdm if clock pin not provided. PDM is not supported on ESP32-S2. PDM RX not supported on ESP32-C3 _config.channel_format =I2S_PDM_MIC_CHANNEL; // seems that PDM mono mode always uses left channel. _config.use_apll = false; // don't use aPLL clock source (fix for #5391) #endif } #if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(4, 2, 0) if (mclkPin != I2S_PIN_NO_CHANGE) { #if !defined(WLED_USE_ETHERNET) // fix for #5391 aPLL resource conflict - aPLL is needed for ethernet boards with internal RMII clock _config.use_apll = true; // experimental - use aPLL clock source to improve sampling quality, and to avoid glitches. // //_config.fixed_mclk = 512 * _sampleRate; // //_config.fixed_mclk = 256 * _sampleRate; #endif } #if !defined(SOC_I2S_SUPPORTS_APLL) #warning this MCU does not have an APLL high accuracy clock for audio // S3: not supported; S2: supported; C3: not supported _config.use_apll = false; // APLL not supported on this MCU #endif #if defined(ARDUINO_ARCH_ESP32) && !defined(CONFIG_IDF_TARGET_ESP32S3) && !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) if (ESP.getChipRevision() == 0) _config.use_apll = false; // APLL is broken on ESP32 revision 0 #endif #endif // Reserve the master clock pin if provided _mclkPin = mclkPin; if (mclkPin != I2S_PIN_NO_CHANGE) { if(!PinManager::allocatePin(mclkPin, true, PinOwner::UM_Audioreactive)) { DEBUGSR_PRINTF("\nAR: Failed to allocate I2S pin: MCLK=%d\n", mclkPin); return; } else _routeMclk(mclkPin); } _pinConfig = { #if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(4, 4, 0) .mck_io_num = mclkPin, // "classic" ESP32 supports setting MCK on GPIO0/GPIO1/GPIO3 only. i2s_set_pin() will fail if wrong mck_io_num is provided. #endif .bck_io_num = i2sckPin, .ws_io_num = i2swsPin, .data_out_num = I2S_PIN_NO_CHANGE, .data_in_num = i2ssdPin }; //DEBUGSR_PRINTF("[AR] I2S: SD=%d, WS=%d, SCK=%d, MCLK=%d\n", i2ssdPin, i2swsPin, i2sckPin, mclkPin); esp_err_t err = i2s_driver_install(I2S_NUM_0, &_config, 0, nullptr); if (err != ESP_OK) { DEBUGSR_PRINTF("AR: Failed to install i2s driver: %d\n", err); return; } DEBUGSR_PRINTF("AR: I2S#0 driver %s aPLL; fixed_mclk=%d.\n", _config.use_apll? "uses":"without", _config.fixed_mclk); DEBUGSR_PRINTF("AR: %d bits, Sample scaling factor = %6.4f\n", _config.bits_per_sample, _sampleScale); if (_config.mode & I2S_MODE_PDM) { DEBUGSR_PRINTLN(F("AR: I2S#0 driver installed in PDM MASTER mode.")); } else { DEBUGSR_PRINTLN(F("AR: I2S#0 driver installed in MASTER mode.")); } err = i2s_set_pin(I2S_NUM_0, &_pinConfig); if (err != ESP_OK) { DEBUGSR_PRINTF("AR: Failed to set i2s pin config: %d\n", err); i2s_driver_uninstall(I2S_NUM_0); // uninstall already-installed driver return; } #if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(4, 2, 0) err = i2s_set_clk(I2S_NUM_0, _sampleRate, I2S_SAMPLE_RESOLUTION, I2S_CHANNEL_MONO); // set bit clocks. Also takes care of MCLK routing if needed. if (err != ESP_OK) { DEBUGSR_PRINTF("AR: Failed to configure i2s clocks: %d\n", err); i2s_driver_uninstall(I2S_NUM_0); // uninstall already-installed driver return; } #endif _initialized = true; } virtual void deinitialize() { _initialized = false; esp_err_t err = i2s_driver_uninstall(I2S_NUM_0); if (err != ESP_OK) { DEBUGSR_PRINTF("Failed to uninstall i2s driver: %d\n", err); return; } if (_pinConfig.ws_io_num != I2S_PIN_NO_CHANGE) PinManager::deallocatePin(_pinConfig.ws_io_num, PinOwner::UM_Audioreactive); if (_pinConfig.data_in_num != I2S_PIN_NO_CHANGE) PinManager::deallocatePin(_pinConfig.data_in_num, PinOwner::UM_Audioreactive); if (_pinConfig.bck_io_num != I2S_PIN_NO_CHANGE) PinManager::deallocatePin(_pinConfig.bck_io_num, PinOwner::UM_Audioreactive); // Release the master clock pin if (_mclkPin != I2S_PIN_NO_CHANGE) PinManager::deallocatePin(_mclkPin, PinOwner::UM_Audioreactive); } virtual void getSamples(float *buffer, uint16_t num_samples) { if (_initialized) { esp_err_t err; size_t bytes_read = 0; /* Counter variable to check if we actually got enough data */ I2S_datatype newSamples[num_samples]; /* Intermediary sample storage */ err = i2s_read(I2S_NUM_0, (void *)newSamples, sizeof(newSamples), &bytes_read, portMAX_DELAY); if (err != ESP_OK) { DEBUGSR_PRINTF("Failed to get samples: %d\n", err); return; } // For correct operation, we need to read exactly sizeof(samples) bytes from i2s if (bytes_read != sizeof(newSamples)) { DEBUGSR_PRINTF("Failed to get enough samples: wanted: %d read: %d\n", sizeof(newSamples), bytes_read); return; } // Store samples in sample buffer and update DC offset for (int i = 0; i < num_samples; i++) { newSamples[i] = postProcessSample(newSamples[i]); // perform postprocessing (needed for ADC samples) float currSample = 0.0f; #ifdef I2S_SAMPLE_DOWNSCALE_TO_16BIT currSample = (float) newSamples[i] / 65536.0f; // 32bit input -> 16bit; keeping lower 16bits as decimal places #else currSample = (float) newSamples[i]; // 16bit input -> use as-is #endif buffer[i] = currSample; buffer[i] *= _sampleScale; // scale samples } } } protected: void _routeMclk(int8_t mclkPin) { #if !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) && !defined(CONFIG_IDF_TARGET_ESP32S3) // MCLK routing by writing registers is not needed any more with IDF > 4.4.0 #if ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(4, 4, 0) // this way of MCLK routing only works on "classic" ESP32 /* Enable the mclk routing depending on the selected mclk pin (ESP32: only 0,1,3) Only I2S_NUM_0 is supported */ if (mclkPin == GPIO_NUM_0) { PIN_FUNC_SELECT(PERIPHS_IO_MUX_GPIO0_U, FUNC_GPIO0_CLK_OUT1); WRITE_PERI_REG(PIN_CTRL,0xFFF0); } else if (mclkPin == GPIO_NUM_1) { PIN_FUNC_SELECT(PERIPHS_IO_MUX_U0TXD_U, FUNC_U0TXD_CLK_OUT3); WRITE_PERI_REG(PIN_CTRL, 0xF0F0); } else { PIN_FUNC_SELECT(PERIPHS_IO_MUX_U0RXD_U, FUNC_U0RXD_CLK_OUT2); WRITE_PERI_REG(PIN_CTRL, 0xFF00); } #endif #endif } i2s_config_t _config; i2s_pin_config_t _pinConfig; int8_t _mclkPin; }; /* ES7243 Microphone This is an I2S microphone that requires initialization over I2C before I2S data can be received */ class ES7243 : public I2SSource { private: void _es7243I2cWrite(uint8_t reg, uint8_t val) { #ifndef ES7243_ADDR #define ES7243_ADDR 0x13 // default address #endif Wire.beginTransmission(ES7243_ADDR); Wire.write((uint8_t)reg); Wire.write((uint8_t)val); uint8_t i2cErr = Wire.endTransmission(); // i2cErr == 0 means OK if (i2cErr != 0) { DEBUGSR_PRINTF("AR: ES7243 I2C write failed with error=%d (addr=0x%X, reg 0x%X, val 0x%X).\n", i2cErr, ES7243_ADDR, reg, val); } } void _es7243InitAdc() { _es7243I2cWrite(0x00, 0x01); _es7243I2cWrite(0x06, 0x00); _es7243I2cWrite(0x05, 0x1B); _es7243I2cWrite(0x01, 0x00); // 0x00 for 24 bit to match INMP441 - not sure if this needs adjustment to get 16bit samples from I2S _es7243I2cWrite(0x08, 0x43); _es7243I2cWrite(0x05, 0x13); } public: ES7243(SRate_t sampleRate, int blockSize, float sampleScale = 1.0f) : I2SSource(sampleRate, blockSize, sampleScale) { _config.channel_format = I2S_CHANNEL_FMT_ONLY_RIGHT; }; void initialize(int8_t i2swsPin, int8_t i2ssdPin, int8_t i2sckPin, int8_t mclkPin) { DEBUGSR_PRINTLN(F("ES7243:: initialize();")); if ((i2sckPin < 0) || (mclkPin < 0)) { DEBUGSR_PRINTF("\nAR: invalid I2S pin: SCK=%d, MCLK=%d\n", i2sckPin, mclkPin); return; } // First route mclk, then configure ADC over I2C, then configure I2S _es7243InitAdc(); I2SSource::initialize(i2swsPin, i2ssdPin, i2sckPin, mclkPin); } void deinitialize() { I2SSource::deinitialize(); } }; /* ES8388 Sound Module This is an I2S sound processing unit that requires initialization over I2C before I2S data can be received. */ class ES8388Source : public I2SSource { private: void _es8388I2cWrite(uint8_t reg, uint8_t val) { #ifndef ES8388_ADDR Wire.beginTransmission(0x10); #define ES8388_ADDR 0x10 // default address #else Wire.beginTransmission(ES8388_ADDR); #endif Wire.write((uint8_t)reg); Wire.write((uint8_t)val); uint8_t i2cErr = Wire.endTransmission(); // i2cErr == 0 means OK if (i2cErr != 0) { DEBUGSR_PRINTF("AR: ES8388 I2C write failed with error=%d (addr=0x%X, reg 0x%X, val 0x%X).\n", i2cErr, ES8388_ADDR, reg, val); } } void _es8388InitAdc() { // https://dl.radxa.com/rock2/docs/hw/ds/ES8388%20user%20Guide.pdf Section 10.1 // http://www.everest-semi.com/pdf/ES8388%20DS.pdf Better spec sheet, more clear. // https://docs.google.com/spreadsheets/d/1CN3MvhkcPVESuxKyx1xRYqfUit5hOdsG45St9BCUm-g/edit#gid=0 generally // Sets ADC to around what AudioReactive expects, and loops line-in to line-out/headphone for monitoring. // Registries are decimal, settings are binary as that's how everything is listed in the docs // ...which makes it easier to reference the docs. // _es8388I2cWrite( 8,0b00000000); // I2S to slave _es8388I2cWrite( 2,0b11110011); // Power down DEM and STM _es8388I2cWrite(43,0b10000000); // Set same LRCK _es8388I2cWrite( 0,0b00000101); // Set chip to Play & Record Mode _es8388I2cWrite(13,0b00000010); // Set MCLK/LRCK ratio to 256 _es8388I2cWrite( 1,0b01000000); // Power up analog and lbias _es8388I2cWrite( 3,0b00000000); // Power up ADC, Analog Input, and Mic Bias _es8388I2cWrite( 4,0b11111100); // Power down DAC, Turn on LOUT1 and ROUT1 and LOUT2 and ROUT2 power _es8388I2cWrite( 2,0b01000000); // Power up DEM and STM and undocumented bit for "turn on line-out amp" // #define use_es8388_mic #ifdef use_es8388_mic // The mics *and* line-in are BOTH connected to LIN2/RIN2 on the AudioKit // so there's no way to completely eliminate the mics. It's also hella noisy. // Line-in works OK on the AudioKit, generally speaking, as the mics really need // amplification to be noticeable in a quiet room. If you're in a very loud room, // the mics on the AudioKit WILL pick up sound even in line-in mode. // TL;DR: Don't use the AudioKit for anything, use the LyraT. // // The LyraT does a reasonable job with mic input as configured below. // Pick one of these. If you have to use the mics, use a LyraT over an AudioKit if you can: _es8388I2cWrite(10,0b00000000); // Use Lin1/Rin1 for ADC input (mic on LyraT) //_es8388I2cWrite(10,0b01010000); // Use Lin2/Rin2 for ADC input (mic *and* line-in on AudioKit) _es8388I2cWrite( 9,0b10001000); // Select Analog Input PGA Gain for ADC to +24dB (L+R) _es8388I2cWrite(16,0b00000000); // Set ADC digital volume attenuation to 0dB (left) _es8388I2cWrite(17,0b00000000); // Set ADC digital volume attenuation to 0dB (right) _es8388I2cWrite(38,0b00011011); // Mixer - route LIN1/RIN1 to output after mic gain _es8388I2cWrite(39,0b01000000); // Mixer - route LIN to mixL, +6dB gain _es8388I2cWrite(42,0b01000000); // Mixer - route RIN to mixR, +6dB gain _es8388I2cWrite(46,0b00100001); // LOUT1VOL - 0b00100001 = +4.5dB _es8388I2cWrite(47,0b00100001); // ROUT1VOL - 0b00100001 = +4.5dB _es8388I2cWrite(48,0b00100001); // LOUT2VOL - 0b00100001 = +4.5dB _es8388I2cWrite(49,0b00100001); // ROUT2VOL - 0b00100001 = +4.5dB // Music ALC - the mics like Auto Level Control // You can also use this for line-in, but it's not really needed. // _es8388I2cWrite(18,0b11111000); // ALC: stereo, max gain +35.5dB, min gain -12dB _es8388I2cWrite(19,0b00110000); // ALC: target -1.5dB, 0ms hold time _es8388I2cWrite(20,0b10100110); // ALC: gain ramp up = 420ms/93ms, gain ramp down = check manual for calc _es8388I2cWrite(21,0b00000110); // ALC: use "ALC" mode, no zero-cross, window 96 samples _es8388I2cWrite(22,0b01011001); // ALC: noise gate threshold, PGA gain constant, noise gate enabled #else _es8388I2cWrite(10,0b01010000); // Use Lin2/Rin2 for ADC input ("line-in") _es8388I2cWrite( 9,0b00000000); // Select Analog Input PGA Gain for ADC to 0dB (L+R) _es8388I2cWrite(16,0b01000000); // Set ADC digital volume attenuation to -32dB (left) _es8388I2cWrite(17,0b01000000); // Set ADC digital volume attenuation to -32dB (right) _es8388I2cWrite(38,0b00001001); // Mixer - route LIN2/RIN2 to output _es8388I2cWrite(39,0b01010000); // Mixer - route LIN to mixL, 0dB gain _es8388I2cWrite(42,0b01010000); // Mixer - route RIN to mixR, 0dB gain _es8388I2cWrite(46,0b00011011); // LOUT1VOL - 0b00011110 = +0dB, 0b00011011 = LyraT balance fix _es8388I2cWrite(47,0b00011110); // ROUT1VOL - 0b00011110 = +0dB _es8388I2cWrite(48,0b00011110); // LOUT2VOL - 0b00011110 = +0dB _es8388I2cWrite(49,0b00011110); // ROUT2VOL - 0b00011110 = +0dB #endif } public: ES8388Source(SRate_t sampleRate, int blockSize, float sampleScale = 1.0f, bool i2sMaster=true) : I2SSource(sampleRate, blockSize, sampleScale) { _config.channel_format = I2S_CHANNEL_FMT_ONLY_LEFT; }; void initialize(int8_t i2swsPin, int8_t i2ssdPin, int8_t i2sckPin, int8_t mclkPin) { DEBUGSR_PRINTLN(F("ES8388Source:: initialize();")); if ((i2sckPin < 0) || (mclkPin < 0)) { DEBUGSR_PRINTF("\nAR: invalid I2S pin: SCK=%d, MCLK=%d\n", i2sckPin, mclkPin); return; } // First route mclk, then configure ADC over I2C, then configure I2S _es8388InitAdc(); I2SSource::initialize(i2swsPin, i2ssdPin, i2sckPin, mclkPin); } void deinitialize() { I2SSource::deinitialize(); } }; #if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(4, 2, 0) #if !defined(SOC_I2S_SUPPORTS_ADC) && !defined(SOC_I2S_SUPPORTS_ADC_DAC) #warning this MCU does not support analog sound input #endif #endif #if !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) && !defined(CONFIG_IDF_TARGET_ESP32S3) // ADC over I2S is only availeable in "classic" ESP32 /* ADC over I2S Microphone This microphone is an ADC pin sampled via the I2S interval This allows to use the I2S API to obtain ADC samples with high sample rates without the need of manual timing of the samples */ class I2SAdcSource : public I2SSource { public: I2SAdcSource(SRate_t sampleRate, int blockSize, float sampleScale = 1.0f) : I2SSource(sampleRate, blockSize, sampleScale) { _config = { .mode = i2s_mode_t(I2S_MODE_MASTER | I2S_MODE_RX | I2S_MODE_ADC_BUILT_IN), .sample_rate = _sampleRate, .bits_per_sample = I2S_SAMPLE_RESOLUTION, .channel_format = I2S_CHANNEL_FMT_ONLY_LEFT, #if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(4, 2, 0) .communication_format = i2s_comm_format_t(I2S_COMM_FORMAT_STAND_I2S), #else .communication_format = i2s_comm_format_t(I2S_COMM_FORMAT_I2S | I2S_COMM_FORMAT_I2S_MSB), #endif .intr_alloc_flags = ESP_INTR_FLAG_LEVEL1, .dma_buf_count = 8, .dma_buf_len = _blockSize, .use_apll = false, .tx_desc_auto_clear = false, .fixed_mclk = 0 }; } /* identify Audiosource type - I2S-ADC*/ AudioSourceType getType(void) {return(Type_I2SAdc);} void initialize(int8_t audioPin, int8_t = I2S_PIN_NO_CHANGE, int8_t = I2S_PIN_NO_CHANGE, int8_t = I2S_PIN_NO_CHANGE) { DEBUGSR_PRINTLN(F("I2SAdcSource:: initialize().")); _myADCchannel = 0x0F; if(!PinManager::allocatePin(audioPin, false, PinOwner::UM_Audioreactive)) { DEBUGSR_PRINTF("failed to allocate GPIO for audio analog input: %d\n", audioPin); return; } _audioPin = audioPin; // Determine Analog channel. Only Channels on ADC1 are supported int8_t channel = digitalPinToAnalogChannel(_audioPin); if (channel > 9) { DEBUGSR_PRINTF("Incompatible GPIO used for analog audio input: %d\n", _audioPin); return; } else { adc_gpio_init(ADC_UNIT_1, adc_channel_t(channel)); _myADCchannel = channel; } // Install Driver esp_err_t err = i2s_driver_install(I2S_NUM_0, &_config, 0, nullptr); if (err != ESP_OK) { DEBUGSR_PRINTF("Failed to install i2s driver: %d\n", err); return; } adc1_config_width(ADC_WIDTH_BIT_12); // ensure that ADC runs with 12bit resolution // Enable I2S mode of ADC err = i2s_set_adc_mode(ADC_UNIT_1, adc1_channel_t(channel)); if (err != ESP_OK) { DEBUGSR_PRINTF("Failed to set i2s adc mode: %d\n", err); return; } // see example in https://github.com/espressif/arduino-esp32/blob/master/libraries/ESP32/examples/I2S/HiFreq_ADC/HiFreq_ADC.ino adc1_config_channel_atten(adc1_channel_t(channel), ADC_ATTEN_DB_12); // configure ADC input amplification #if defined(I2S_GRAB_ADC1_COMPLETELY) // according to docs from espressif, the ADC needs to be started explicitly // fingers crossed err = i2s_adc_enable(I2S_NUM_0); if (err != ESP_OK) { DEBUGSR_PRINTF("Failed to enable i2s adc: %d\n", err); //return; } #else // bugfix: do not disable ADC initially - its already disabled after driver install. //err = i2s_adc_disable(I2S_NUM_0); // //err = i2s_stop(I2S_NUM_0); //if (err != ESP_OK) { // DEBUGSR_PRINTF("Failed to initially disable i2s adc: %d\n", err); //} #endif _initialized = true; } I2S_datatype postProcessSample(I2S_datatype sample_in) { static I2S_datatype lastADCsample = 0; // last good sample static unsigned int broken_samples_counter = 0; // number of consecutive broken (and fixed) ADC samples I2S_datatype sample_out = 0; // bring sample down down to 16bit unsigned I2S_unsigned_datatype rawData = * reinterpret_cast (&sample_in); // C++ acrobatics to get sample as "unsigned" #ifndef I2S_USE_16BIT_SAMPLES rawData = (rawData >> 16) & 0xFFFF; // scale input down from 32bit -> 16bit I2S_datatype lastGoodSample = lastADCsample / 16384 ; // prepare "last good sample" accordingly (26bit-> 12bit with correct sign handling) #else rawData = rawData & 0xFFFF; // input is already in 16bit, just mask off possible junk I2S_datatype lastGoodSample = lastADCsample * 4; // prepare "last good sample" accordingly (10bit-> 12bit) #endif // decode ADC sample data fields uint16_t the_channel = (rawData >> 12) & 0x000F; // upper 4 bit = ADC channel uint16_t the_sample = rawData & 0x0FFF; // lower 12bit -> ADC sample (unsigned) I2S_datatype finalSample = (int(the_sample) - 2048); // convert unsigned sample to signed (centered at 0); if ((the_channel != _myADCchannel) && (_myADCchannel != 0x0F)) { // 0x0F means "don't know what my channel is" // fix bad sample finalSample = lastGoodSample; // replace with last good ADC sample broken_samples_counter ++; if (broken_samples_counter > 256) _myADCchannel = 0x0F; // too many bad samples in a row -> disable sample corrections //Serial.print("\n!ADC rogue sample 0x"); Serial.print(rawData, HEX); Serial.print("\tchannel:");Serial.println(the_channel); } else broken_samples_counter = 0; // good sample - reset counter // back to original resolution #ifndef I2S_USE_16BIT_SAMPLES finalSample = finalSample << 16; // scale up from 16bit -> 32bit; #endif finalSample = finalSample / 4; // mimic old analog driver behaviour (12bit -> 10bit) sample_out = (3 * finalSample + lastADCsample) / 4; // apply low-pass filter (2-tap FIR) //sample_out = (finalSample + lastADCsample) / 2; // apply stronger low-pass filter (2-tap FIR) lastADCsample = sample_out; // update ADC last sample return(sample_out); } void getSamples(float *buffer, uint16_t num_samples) { /* Enable ADC. This has to be enabled and disabled directly before and * after sampling, otherwise Wifi dies */ if (_initialized) { #if !defined(I2S_GRAB_ADC1_COMPLETELY) // old code - works for me without enable/disable, at least on ESP32. //esp_err_t err = i2s_start(I2S_NUM_0); esp_err_t err = i2s_adc_enable(I2S_NUM_0); if (err != ESP_OK) { DEBUGSR_PRINTF("Failed to enable i2s adc: %d\n", err); return; } #endif I2SSource::getSamples(buffer, num_samples); #if !defined(I2S_GRAB_ADC1_COMPLETELY) // old code - works for me without enable/disable, at least on ESP32. err = i2s_adc_disable(I2S_NUM_0); //i2s_adc_disable() may cause crash with IDF 4.4 (https://github.com/espressif/arduino-esp32/issues/6832) //err = i2s_stop(I2S_NUM_0); if (err != ESP_OK) { DEBUGSR_PRINTF("Failed to disable i2s adc: %d\n", err); return; } #endif } } void deinitialize() { PinManager::deallocatePin(_audioPin, PinOwner::UM_Audioreactive); _initialized = false; _myADCchannel = 0x0F; esp_err_t err; #if defined(I2S_GRAB_ADC1_COMPLETELY) // according to docs from espressif, the ADC needs to be stopped explicitly // fingers crossed err = i2s_adc_disable(I2S_NUM_0); if (err != ESP_OK) { DEBUGSR_PRINTF("Failed to disable i2s adc: %d\n", err); } #endif i2s_stop(I2S_NUM_0); err = i2s_driver_uninstall(I2S_NUM_0); if (err != ESP_OK) { DEBUGSR_PRINTF("Failed to uninstall i2s driver: %d\n", err); return; } } private: int8_t _audioPin; int8_t _myADCchannel = 0x0F; // current ADC channel for analog input. 0x0F means "undefined" }; #endif /* SPH0645 Microphone This is an I2S microphone with some timing quirks that need special consideration. */ // https://github.com/espressif/esp-idf/issues/7192 SPH0645 i2s microphone issue when migrate from legacy esp-idf version (IDFGH-5453) // a user recommended this: Try to set .communication_format to I2S_COMM_FORMAT_STAND_I2S and call i2s_set_clk() after i2s_set_pin(). class SPH0654 : public I2SSource { public: SPH0654(SRate_t sampleRate, int blockSize, float sampleScale = 1.0f) : I2SSource(sampleRate, blockSize, sampleScale) {} void initialize(int8_t i2swsPin, int8_t i2ssdPin, int8_t i2sckPin, int8_t = I2S_PIN_NO_CHANGE) { DEBUGSR_PRINTLN(F("SPH0654:: initialize();")); I2SSource::initialize(i2swsPin, i2ssdPin, i2sckPin); #if !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) && !defined(CONFIG_IDF_TARGET_ESP32S3) // these registers are only existing in "classic" ESP32 REG_SET_BIT(I2S_TIMING_REG(I2S_NUM_0), BIT(9)); REG_SET_BIT(I2S_CONF_REG(I2S_NUM_0), I2S_RX_MSB_SHIFT); #else #warning FIX ME! Please. #endif } }; #endif ================================================ FILE: usermods/audioreactive/library.json ================================================ { "name": "audioreactive", "build": { "libArchive": false, "extraScript": "override_sqrt.py" }, "dependencies": [ { "owner": "kosme", "name": "arduinoFFT", "version": "2.0.1", "platforms": "espressif32" } ] } ================================================ FILE: usermods/audioreactive/override_sqrt.py ================================================ Import('env') for lb in env.GetLibBuilders(): if lb.name == "arduinoFFT": lb.env.Append(CPPDEFINES=[("sqrt_internal", "sqrtf")]) ================================================ FILE: usermods/audioreactive/readme.md ================================================ # Audioreactive usermod Enables controlling LEDs via audio input. Audio source can be a microphone or analog-in (AUX) using an appropriate adapter. Supported microphones range from analog (MAX4466, MAX9814, ...) to digital (INMP441, ICS-43434, ...). Does audio processing and provides data structure that specially written effects can use. **does not** provide effects or draw anything to an LED strip/matrix. ## Additional Documentation This usermod is an evolution of [SR-WLED](https://github.com/atuline/WLED), and a lot of documentation and information can be found in the [SR-WLED wiki](https://github.com/atuline/WLED/wiki): * [getting started with audio](https://github.com/atuline/WLED/wiki/First-Time-Setup#sound) * [Sound settings](https://github.com/atuline/WLED/wiki/Sound-Settings) - similar to options on the usemod settings page in WLED. * [Digital Audio](https://github.com/atuline/WLED/wiki/Digital-Microphone-Hookup) * [Analog Audio](https://github.com/atuline/WLED/wiki/Analog-Audio-Input-Options) * [UDP Sound sync](https://github.com/atuline/WLED/wiki/UDP-Sound-Sync) ## Supported MCUs This audioreactive usermod works best on "classic ESP32" (dual core), and on ESP32-S3 which also has dual core and hardware floating point support. It will compile successfully for ESP32-S2 and ESP32-C3, however might not work well, as other WLED functions will become slow. Audio processing requires a lot of computing power, which can be problematic on smaller MCUs like -S2 and -C3. Analog audio is only possible on "classic" ESP32, but not on other MCUs like ESP32-S3. Currently ESP8266 is not supported, due to low speed and small RAM of this chip. There are however plans to create a lightweight audioreactive for the 8266, with reduced features. ## Installation Add 'ADS1115_v2' to `custom_usermods` in your platformio environment. ## Configuration All parameters are runtime configurable. Some may require a hard reset after changing them (I2S microphone or selected GPIOs). If you want to define default GPIOs during compile time, use the following (default values in parentheses): * `-D SR_DMTYPE=x` : defines digital microphone type: 0=analog, 1=generic I2S (default), 2=ES7243 I2S, 3=SPH0645 I2S, 4=generic I2S with master clock, 5=PDM I2S * `-D AUDIOPIN=x` : GPIO for analog microphone/AUX-in (36) * `-D I2S_SDPIN=x` : GPIO for SD pin on digital microphone (32) * `-D I2S_WSPIN=x` : GPIO for WS pin on digital microphone (15) * `-D I2S_CKPIN=x` : GPIO for SCK pin on digital microphone (14) * `-D MCLK_PIN=x` : GPIO for master clock pin on digital Line-In boards (-1) * `-D ES7243_SDAPIN` : GPIO for I2C SDA pin on ES7243 microphone (-1) * `-D ES7243_SCLPIN` : GPIO for I2C SCL pin on ES7243 microphone (-1) Other options: * `-D UM_AUDIOREACTIVE_ENABLE` : makes usermod default enabled (not the same as include into build option!) * `-D UM_AUDIOREACTIVE_DYNAMICS_LIMITER_OFF` : disables rise/fall limiter default **NOTE** I2S is used for analog audio sampling. Hence, the analog *buttons* (i.e. potentiometers) are disabled when running this usermod with an analog microphone. ### Advanced Compile-Time Options You can use the following additional flags in your `build_flags` * `-D SR_SQUELCH=x` : Default "squelch" setting (10) * `-D SR_GAIN=x` : Default "gain" setting (60) * `-D SR_AGC=x` : (Only ESP32) Default "AGC (Automatic Gain Control)" setting (0): 0=off, 1=normal, 2=vivid, 3=lazy * `-D I2S_USE_RIGHT_CHANNEL`: Use RIGHT instead of LEFT channel (not recommended unless you strictly need this). * `-D I2S_USE_16BIT_SAMPLES`: Use 16bit instead of 32bit for internal sample buffers. Reduces sampling quality, but frees some RAM resources (not recommended unless you absolutely need this). * `-D I2S_GRAB_ADC1_COMPLETELY`: Experimental: continuously sample analog ADC microphone. Only effective on ESP32. WARNING this *will* cause conflicts(lock-up) with any analogRead() call. * `-D MIC_LOGGER` : (debugging) Logs samples from the microphone to serial USB. Use with serial plotter (Arduino IDE) * `-D SR_DEBUG` : (debugging) Additional error diagnostics and debug info on serial USB. ## Release notes * 2022-06 Ported from [soundreactive WLED](https://github.com/atuline/WLED) - by @blazoncek (AKA Blaz Kristan) and the [SR-WLED team](https://github.com/atuline/WLED/wiki#sound-reactive-wled-fork-team). * 2022-11 Updated to align with "[MoonModules/WLED](https://amg.wled.me)" audioreactive usermod - by @softhack007 (AKA Frank Möhle). ================================================ FILE: usermods/battery_keypad_controller/README.md ================================================ # Battery powered controller with keypad I'm using this controller for a festival totem. Runs on 3 18650 Cells, can deliver >5A current. Via keypad one can select 8 presets, change effect, effect speed, effect intensity and palette. Brightness can be adjusted with a potentiometer. ## Pictures ![bat-key-ctrl-1](assets/bat-key-ctrl-1.jpg) ![bat-key-ctrl-2](assets/bat-key-ctrl-2.jpg) ![bat-key-ctrl-3](assets/bat-key-ctrl-3.jpg) ================================================ FILE: usermods/battery_keypad_controller/wled06_usermod.ino ================================================ /* * WLED usermod for keypad and brightness-pot. * 3'2020 https://github.com/hobbyquaker */ #include const byte keypad_rows = 4; const byte keypad_cols = 4; char keypad_keys[keypad_rows][keypad_cols] = { {'1', '2', '3', 'A'}, {'4', '5', '6', 'B'}, {'7', '8', '9', 'C'}, {'*', '0', '#', 'D'} }; byte keypad_colPins[keypad_rows] = {D3, D2, D1, D0}; byte keypad_rowPins[keypad_cols] = {D7, D6, D5, D4}; Keypad myKeypad = Keypad(makeKeymap(keypad_keys), keypad_rowPins, keypad_colPins, keypad_rows, keypad_cols); void userSetup() { } void userConnected() { } long lastTime = 0; int delayMs = 20; //we want to do something every 2 seconds void userLoop() { if (millis()-lastTime > delayMs) { long analog = analogRead(0); int new_bri = 1; if (analog > 900) { new_bri = 255; } else if (analog > 30) { new_bri = dim8_video(map(analog, 31, 900, 16, 255)); } if (bri != new_bri) { bri = new_bri; colorUpdated(1); } char myKey = myKeypad.getKey(); if (myKey != NULL) { switch (myKey) { case '1': applyPreset(1); break; case '2': applyPreset(2); break; case '3': applyPreset(3); break; case '4': applyPreset(4); break; case '5': applyPreset(5); break; case '6': applyPreset(6); break; case 'A': applyPreset(7); break; case 'B': applyPreset(8); break; case '7': effectCurrent += 1; if (effectCurrent >= MODE_COUNT) effectCurrent = 0; colorUpdated(CALL_MODE_FX_CHANGED); break; case '*': effectCurrent -= 1; if (effectCurrent < 0) effectCurrent = (MODE_COUNT-1); colorUpdated(CALL_MODE_FX_CHANGED); break; case '8': if (effectSpeed < 240) { effectSpeed += 12; } else if (effectSpeed < 255) { effectSpeed += 1; } colorUpdated(CALL_MODE_FX_CHANGED); break; case '0': if (effectSpeed > 15) { effectSpeed -= 12; } else if (effectSpeed > 0) { effectSpeed -= 1; } colorUpdated(CALL_MODE_FX_CHANGED); break; case '9': if (effectIntensity < 240) { effectIntensity += 12; } else if (effectIntensity < 255) { effectIntensity += 1; } colorUpdated(CALL_MODE_FX_CHANGED); break; case '#': if (effectIntensity > 15) { effectIntensity -= 12; } else if (effectIntensity > 0) { effectIntensity -= 1; } colorUpdated(CALL_MODE_FX_CHANGED); break; case 'C': effectPalette += 1; if (effectPalette >= 50) effectPalette = 0; colorUpdated(CALL_MODE_FX_CHANGED); break; case 'D': effectPalette -= 1; if (effectPalette <= 0) effectPalette = 50; colorUpdated(CALL_MODE_FX_CHANGED); break; } } lastTime = millis(); } } ================================================ FILE: usermods/boblight/boblight.cpp ================================================ #include "wled.h" /* * Usermod that implements BobLight "ambilight" protocol * * See the accompanying README.md file for more info. */ #ifndef BOB_PORT #define BOB_PORT 19333 // Default boblightd port #endif class BobLightUsermod : public Usermod { typedef struct _LIGHT { char lightname[5]; float hscan[2]; float vscan[2]; } light_t; private: unsigned long lastTime = 0; bool enabled = false; bool initDone = false; light_t *lights = nullptr; uint16_t numLights = 0; // 16 + 9 + 16 + 9 uint16_t top, bottom, left, right; // will be filled in readFromConfig() uint16_t pct; WiFiClient bobClient; WiFiServer *bob; uint16_t bobPort = BOB_PORT; static const char _name[]; static const char _enabled[]; /* # boblight # Copyright (C) Bob 2009 # # makeboblight.sh created by Adam Boeglin # # boblight is free software: you can redistribute it and/or modify it # under the terms of the GNU General Public License as published by the # Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # boblight is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. # See the GNU General Public License for more details. # # You should have received a copy of the GNU General Public License along # with this program. If not, see . */ // fills the lights[] array with position & depth of scan for each LED void fillBobLights(int bottom, int left, int top, int right, float pct_scan) { int lightcount = 0; int total = top+left+right+bottom; int bcount; if (total > strip.getLengthTotal()) { DEBUG_PRINTLN(F("BobLight: Too many lights.")); return; } // start left part of bottom strip (clockwise direction, 1st half) if (bottom > 0) { bcount = 1; float brange = 100.0/bottom; float bcurrent = 50.0; if (bottom < top) { int diff = top - bottom; brange = 100.0/top; bcurrent -= (diff/2)*brange; } while (bcount <= bottom/2) { float btop = bcurrent - brange; String name = "b"+String(bcount); strncpy(lights[lightcount].lightname, name.c_str(), 4); lights[lightcount].hscan[0] = btop; lights[lightcount].hscan[1] = bcurrent; lights[lightcount].vscan[0] = 100 - pct_scan; lights[lightcount].vscan[1] = 100; lightcount+=1; bcurrent = btop; bcount+=1; } } // left side if (left > 0) { int lcount = 1; float lrange = 100.0/left; float lcurrent = 100.0; while (lcount <= left) { float ltop = lcurrent - lrange; String name = "l"+String(lcount); strncpy(lights[lightcount].lightname, name.c_str(), 4); lights[lightcount].hscan[0] = 0; lights[lightcount].hscan[1] = pct_scan; lights[lightcount].vscan[0] = ltop; lights[lightcount].vscan[1] = lcurrent; lightcount+=1; lcurrent = ltop; lcount+=1; } } // top side if (top > 0) { int tcount = 1; float trange = 100.0/top; float tcurrent = 0; while (tcount <= top) { float ttop = tcurrent + trange; String name = "t"+String(tcount); strncpy(lights[lightcount].lightname, name.c_str(), 4); lights[lightcount].hscan[0] = tcurrent; lights[lightcount].hscan[1] = ttop; lights[lightcount].vscan[0] = 0; lights[lightcount].vscan[1] = pct_scan; lightcount+=1; tcurrent = ttop; tcount+=1; } } // right side if (right > 0) { int rcount = 1; float rrange = 100.0/right; float rcurrent = 0; while (rcount <= right) { float rtop = rcurrent + rrange; String name = "r"+String(rcount); strncpy(lights[lightcount].lightname, name.c_str(), 4); lights[lightcount].hscan[0] = 100-pct_scan; lights[lightcount].hscan[1] = 100; lights[lightcount].vscan[0] = rcurrent; lights[lightcount].vscan[1] = rtop; lightcount+=1; rcurrent = rtop; rcount+=1; } } // right side of bottom strip (2nd half) if (bottom > 0) { float brange = 100.0/bottom; float bcurrent = 100; if (bottom < top) { brange = 100.0/top; } while (bcount <= bottom) { float btop = bcurrent - brange; String name = "b"+String(bcount); strncpy(lights[lightcount].lightname, name.c_str(), 4); lights[lightcount].hscan[0] = btop; lights[lightcount].hscan[1] = bcurrent; lights[lightcount].vscan[0] = 100 - pct_scan; lights[lightcount].vscan[1] = 100; lightcount+=1; bcurrent = btop; bcount+=1; } } numLights = lightcount; #if WLED_DEBUG DEBUG_PRINTLN(F("Fill light data: ")); DEBUG_PRINTF_P(PSTR(" lights %d\n"), numLights); for (int i=0; i strip.getLengthTotal() ) { DEBUG_PRINTLN(F("BobLight: Too many lights.")); DEBUG_PRINTF_P(PSTR("%d+%d+%d+%d>%d\n"), bottom, left, top, right, strip.getLengthTotal()); totalLights = strip.getLengthTotal(); top = bottom = (uint16_t) roundf((float)totalLights * 16.0f / 50.0f); left = right = (uint16_t) roundf((float)totalLights * 9.0f / 50.0f); } lights = new light_t[totalLights]; if (lights) fillBobLights(bottom, left, top, right, float(pct)); // will fill numLights else enable(false); initDone = true; } void connected() override { // we can only start server when WiFi is connected if (!bob) bob = new WiFiServer(bobPort, 1); bob->begin(); bob->setNoDelay(true); } void loop() override { if (!enabled || strip.isUpdating()) return; if (millis() - lastTime > 10) { lastTime = millis(); pollBob(); } } void enable(bool en) { enabled = en; } #ifndef WLED_DISABLE_MQTT /** * handling of MQTT message * topic only contains stripped topic (part after /wled/MAC) * topic should look like: /swipe with amessage of [up|down] */ bool onMqttMessage(char* topic, char* payload) override { //if (strlen(topic) == 6 && strncmp_P(topic, PSTR("/subtopic"), 6) == 0) { // String action = payload; // if (action == "on") { // enable(true); // return true; // } else if (action == "off") { // enable(false); // return true; // } //} return false; } /** * subscribe to MQTT topic for controlling usermod */ void onMqttConnect(bool sessionPresent) override { //char subuf[64]; //if (mqttDeviceTopic[0] != 0) { // strcpy(subuf, mqttDeviceTopic); // strcat_P(subuf, PSTR("/subtopic")); // mqtt->subscribe(subuf, 0); //} } #endif void addToJsonInfo(JsonObject& root) override { JsonObject user = root["u"]; if (user.isNull()) user = root.createNestedObject("u"); JsonArray infoArr = user.createNestedArray(FPSTR(_name)); String uiDomString = F(""); infoArr.add(uiDomString); } /* * 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) override { } /* * 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) override { if (!initDone) return; // prevent crash on boot applyPreset() bool en = enabled; JsonObject um = root[FPSTR(_name)]; if (!um.isNull()) { if (um[FPSTR(_enabled)].is()) { en = um[FPSTR(_enabled)].as(); } else { String str = um[FPSTR(_enabled)]; // checkbox -> off or on en = (bool)(str!="off"); // off is guaranteed to be present } if (en != enabled && lights) { enable(en); if (!enabled && bob && bob->hasClient()) { if (bobClient) bobClient.stop(); bobClient = bob->available(); BobClear(); exitRealtime(); } } } } void appendConfigData() override { //oappend(F("dd=addDropdown('usermod','selectfield');")); //oappend(F("addOption(dd,'1st value',0);")); //oappend(F("addOption(dd,'2nd value',1);")); oappend(F("addInfo('BobLight:top',1,'LEDs');")); // 0 is field type, 1 is actual field oappend(F("addInfo('BobLight:bottom',1,'LEDs');")); // 0 is field type, 1 is actual field oappend(F("addInfo('BobLight:left',1,'LEDs');")); // 0 is field type, 1 is actual field oappend(F("addInfo('BobLight:right',1,'LEDs');")); // 0 is field type, 1 is actual field oappend(F("addInfo('BobLight:pct',1,'Depth of scan [%]');")); // 0 is field type, 1 is actual field } void addToConfig(JsonObject& root) override { JsonObject umData = root.createNestedObject(FPSTR(_name)); umData[FPSTR(_enabled)] = enabled; umData[ "port" ] = bobPort; umData[F("top")] = top; umData[F("bottom")] = bottom; umData[F("left")] = left; umData[F("right")] = right; umData[F("pct")] = pct; } bool readFromConfig(JsonObject& root) override { JsonObject umData = root[FPSTR(_name)]; bool configComplete = !umData.isNull(); bool en = enabled; configComplete &= getJsonValue(umData[FPSTR(_enabled)], en); enable(en); configComplete &= getJsonValue(umData[ "port" ], bobPort); configComplete &= getJsonValue(umData[F("bottom")], bottom, 16); configComplete &= getJsonValue(umData[F("top")], top, 16); configComplete &= getJsonValue(umData[F("left")], left, 9); configComplete &= getJsonValue(umData[F("right")], right, 9); configComplete &= getJsonValue(umData[F("pct")], pct, 5); // Depth of scan [%] pct = MIN(50,MAX(1,pct)); uint16_t totalLights = bottom + left + top + right; if (initDone && numLights != totalLights) { if (lights) delete[] lights; setup(); } return configComplete; } /* * handleOverlayDraw() is called just before every show() (LED strip update frame) after effects have set the colors. * Use this to blank out some LEDs or set them to a different color regardless of the set effect mode. * Commonly used for custom clocks (Cronixie, 7 segment) */ void handleOverlayDraw() override { //strip.setPixelColor(0, RGBW32(0,0,0,0)) // set the first pixel to black } uint16_t getId() override { return USERMOD_ID_BOBLIGHT; } }; // strings to reduce flash memory usage (used more than twice) const char BobLightUsermod::_name[] PROGMEM = "BobLight"; const char BobLightUsermod::_enabled[] PROGMEM = "enabled"; // main boblight handling (definition here prevents inlining) void BobLightUsermod::pollBob() { //check if there are any new clients if (bob && bob->hasClient()) { //find free/disconnected spot if (!bobClient || !bobClient.connected()) { if (bobClient) bobClient.stop(); bobClient = bob->available(); DEBUG_PRINTLN(F("Boblight: Client connected.")); } //no free/disconnected spot so reject WiFiClient bobClientTmp = bob->available(); bobClientTmp.stop(); BobClear(); exitRealtime(); } //check clients for data if (bobClient && bobClient.connected()) { realtimeLock(realtimeTimeoutMs); // lock strip as we have a client connected //get data from the client while (bobClient.available()) { String input = bobClient.readStringUntil('\n'); // DEBUG_PRINT(F("Client: ")); DEBUG_PRINTLN(input); // may be to stressful on Serial if (input.startsWith(F("hello"))) { DEBUG_PRINTLN(F("hello")); bobClient.print(F("hello\n")); } else if (input.startsWith(F("ping"))) { DEBUG_PRINTLN(F("ping 1")); bobClient.print(F("ping 1\n")); } else if (input.startsWith(F("get version"))) { DEBUG_PRINTLN(F("version 5")); bobClient.print(F("version 5\n")); } else if (input.startsWith(F("get lights"))) { char tmp[64]; String answer = ""; sprintf_P(tmp, PSTR("lights %d\n"), numLights); DEBUG_PRINT(tmp); answer.concat(tmp); for (int i=0; i ... input.remove(0,10); String tmp = input.substring(0,input.indexOf(' ')); int light_id = -1; for (uint16_t i=0; iavailable(); BobClear(); } } } } static BobLightUsermod boblight; REGISTER_USERMOD(boblight); ================================================ FILE: usermods/boblight/library.json ================================================ { "name": "boblight", "build": { "libArchive": false } } ================================================ FILE: usermods/boblight/readme.md ================================================ # BobLight usermod This usermod allows displaying BobLight ambilight protocol on WLED device with a limited command set (not a full implementation). BobLight protocol uses a TCP connection which guarantees packet delivery at the possible expense of latency delays. It is not very efficient (as it uses plaintext comands) so is not suited for large number of LEDs. This implementation is intended for TV backlight in combination with XBMC/Kodi BobLight add-on. The LEDs can be configured in usermod settings page. The configuration is simple: you enter the number of LED pixels on each side of your TV (top, right, bottom, left). The LEDs should be wired in a clockwise orientation starting in the middle of bottom side (left half of bottom leds is where the string should start). ``` +-------->-------+ | | ^ v | | +---<--+ ---<---+ ^ start ``` ## Installation Add `boblight` to `custom_usermods` in your PlatformIO environment. ## Configuration All parameters are runtime configurable though changing port may require reboot. If you want to define default port during compile time use the following (default values in parentheses): - `BOB_PORT=x` : defines default TCP port for usermod to listen on (19333) ## Release notes 2022-11 Initial implementation by @blazoncek (AKA Blaz Kristan) ================================================ FILE: usermods/buzzer/buzzer.cpp ================================================ #include "wled.h" #include "Arduino.h" #include #define USERMOD_ID_BUZZER 900 #ifndef USERMOD_BUZZER_PIN #ifdef GPIO_NUM_32 #define USERMOD_BUZZER_PIN GPIO_NUM_32 #else #define USERMOD_BUZZER_PIN 21 #endif #endif /* * Usermods allow you to add own functionality to WLED more easily * See: https://github.com/wled-dev/WLED/wiki/Add-own-functionality * */ class BuzzerUsermod : public Usermod { private: unsigned long lastTime_ = 0; unsigned long delay_ = 0; std::deque> sequence_ {}; 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() { // Setup the pin, and default to LOW pinMode(USERMOD_BUZZER_PIN, OUTPUT); digitalWrite(USERMOD_BUZZER_PIN, LOW); // Beep on startup sequence_.push_back({ HIGH, 50 }); sequence_.push_back({ LOW, 0 }); } /* * connected() is called every time the WiFi is (re)connected * Use it to initialize network interfaces */ void connected() { // Double beep on WiFi sequence_.push_back({ LOW, 100 }); sequence_.push_back({ HIGH, 50 }); sequence_.push_back({ LOW, 30 }); sequence_.push_back({ HIGH, 50 }); sequence_.push_back({ LOW, 0 }); } /* * loop() is called continuously. Here you can check for events, read sensors, etc. */ void loop() { if (sequence_.size() < 1) return; // Wait until there is a sequence if (millis() - lastTime_ <= delay_) return; // Wait until delay has elapsed auto event = sequence_.front(); sequence_.pop_front(); digitalWrite(USERMOD_BUZZER_PIN, event.first); delay_ = event.second; lastTime_ = millis(); } /* * 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_BUZZER; } }; static BuzzerUsermod buzzer; REGISTER_USERMOD(buzzer); ================================================ FILE: usermods/buzzer/library.json ================================================ { "name": "buzzer", "build": { "libArchive": false } } ================================================ FILE: usermods/deep_sleep/deep_sleep.cpp ================================================ #include "wled.h" #include "driver/rtc_io.h" #ifndef CONFIG_IDF_TARGET_ESP32C3 #include "soc/touch_sensor_periph.h" #endif #ifdef ESP8266 #error The "Deep Sleep" usermod does not support ESP8266 #endif #ifndef DEEPSLEEP_WAKEUPPIN #define DEEPSLEEP_WAKEUPPIN 0 #endif #ifndef DEEPSLEEP_WAKEWHENHIGH #define DEEPSLEEP_WAKEWHENHIGH 0 #endif #ifndef DEEPSLEEP_DISABLEPULL #define DEEPSLEEP_DISABLEPULL 1 #endif #ifndef DEEPSLEEP_WAKEUPINTERVAL #define DEEPSLEEP_WAKEUPINTERVAL 0 #endif #ifndef DEEPSLEEP_DELAY #define DEEPSLEEP_DELAY 1 #endif #ifndef DEEPSLEEP_WAKEUP_TOUCH_PIN #define DEEPSLEEP_WAKEUP_TOUCH_PIN 1 #endif RTC_DATA_ATTR bool powerup = true; // this is first boot after power cycle. note: variable in RTC data persists on a reboot RTC_DATA_ATTR uint8_t wakeupPreset = 0; // preset to apply after deep sleep wakeup (0 = none), set to timer macro preset class DeepSleepUsermod : public Usermod { private: bool enabled = false; // do not enable by default bool initDone = false; uint8_t wakeupPin = DEEPSLEEP_WAKEUPPIN; uint8_t wakeWhenHigh = DEEPSLEEP_WAKEWHENHIGH; // wake up when pin goes high if 1, triggers on low if 0 bool noPull = true; // use pullup/pulldown resistor bool enableTouchWakeup = false; uint8_t touchPin = DEEPSLEEP_WAKEUP_TOUCH_PIN; int wakeupAfter = DEEPSLEEP_WAKEUPINTERVAL; // in seconds, <=0: button only bool presetWake = true; // wakeup timer for preset int sleepDelay = DEEPSLEEP_DELAY; // in seconds, 0 = immediate int delaycounter = 10; // delay deep sleep at bootup until preset settings are applied, force wake up if offmode persists after bootup uint32_t lastLoopTime = 0; // string that are used multiple time (this will save some flash memory) static const char _name[]; static const char _enabled[]; bool pin_is_valid(uint8_t wakePin) { #ifdef CONFIG_IDF_TARGET_ESP32 //ESP32: GPIOs 0,2,4, 12-15, 25-39 can be used for wake-up. note: input-only GPIOs 34-39 do not have internal pull resistors if (wakePin == 0 || wakePin == 2 || wakePin == 4 || (wakePin >= 12 && wakePin <= 15) || (wakePin >= 25 && wakePin <= 27) || (wakePin >= 32 && wakePin <= 39)) { return true; } #endif #if defined(CONFIG_IDF_TARGET_ESP32S3) || defined(CONFIG_IDF_TARGET_ESP32S2) //ESP32 S3 & S3: GPIOs 0-21 can be used for wake-up if (wakePin <= 21) { return true; } #endif #ifdef CONFIG_IDF_TARGET_ESP32C3 // ESP32 C3: GPIOs 0-5 can be used for wake-up if (wakePin <= 5) { return true; } #endif DEBUG_PRINTLN(F("Error: unsupported deep sleep wake-up pin")); return false; } // functions to calculate time difference between now and next scheduled timer event int calculateTimeDifference(int hour1, int minute1, int hour2, int minute2) { int totalMinutes1 = hour1 * 60 + minute1; int totalMinutes2 = hour2 * 60 + minute2; if (totalMinutes2 < totalMinutes1) { totalMinutes2 += 24 * 60; } return totalMinutes2 - totalMinutes1; } int findNextTimerInterval() { if (toki.getTimeSource() == TOKI_TS_NONE) { DEBUG_PRINTLN("DeepSleep: local time not yet synchronized, skipping timer check."); return -1; } int currentHour = hour(localTime); int currentMinute = minute(localTime); int currentWeekday = weekdayMondayFirst(); // 1=Monday ... 7=Sunday int minDifference = INT_MAX; for (uint8_t i = 0; i < 8; i++) { // check if timer is enabled and date is in range, also wakes up if no macro is used if ((timerWeekday[i] & 0x01) && isTodayInDateRange(((timerMonth[i] >> 4) & 0x0F), timerDay[i], timerMonth[i] & 0x0F, timerDayEnd[i])) { // if timer is enabled (bit0 of timerWeekday) and date is in range, check all weekdays it is set for for (int dayOffset = 0; dayOffset < 7; dayOffset++) { int checkWeekday = ((currentWeekday + dayOffset) % 7); // 1-7, check all weekdays starting from today if (checkWeekday == 0) { checkWeekday = 7; // sunday is 7 not 0 } int targetHour = timerHours[i]; int targetMinute = timerMinutes[i]; if ((timerWeekday[i] >> (checkWeekday)) & 0x01) { if (dayOffset == 0 && (targetHour < currentHour || (targetHour == currentHour && targetMinute <= currentMinute))) continue; // skip if time has already passed today int timeDifference = calculateTimeDifference(currentHour, currentMinute, targetHour + (dayOffset * 24), targetMinute); if (timeDifference < minDifference) { minDifference = timeDifference; wakeupPreset = timerMacro[i]; } } } } } return minDifference; } public: inline void enable(bool enable) { enabled = enable; } // Enable/Disable the usermod inline bool isEnabled() { return enabled; } //Get usermod enabled/disabled state // setup is called at boot (or in this case after every exit of sleep mode) void setup() { //TODO: if the de-init of RTC pins is required to do it could be done here //rtc_gpio_deinit(wakeupPin); #ifdef WLED_DEBUG DEBUG_PRINTF("sleep wakeup cause: %d\n", esp_sleep_get_wakeup_cause()); #endif if (esp_sleep_get_wakeup_cause() != ESP_SLEEP_WAKEUP_TIMER) wakeupPreset = 0; // not a timed wakeup, don't apply preset initDone = true; } void loop() { if (!enabled) return; if (!offMode) { // LEDs are on lastLoopTime = 0; // reset timer if (delaycounter) delaycounter--; // decrease delay counter if LEDs are on (they are always turned on after a wake-up, see below) else if (wakeupPreset) applyPreset(wakeupPreset); // apply preset if set, this ensures macro is applied even if we missed the wake-up time return; } if (sleepDelay > 0) { powerup = false; // disable "safety" powerup sleep if delay is set if (lastLoopTime == 0) lastLoopTime = millis(); // initialize if (millis() - lastLoopTime < sleepDelay * 1000) return; // wait until delay is over } else if (powerup && delaycounter) { delaycounter--; // on first boot without sleepDelay set, do not force-turn on delay(1000); // just in case: give user a short ~10s window to turn LEDs on in UI (delaycounter is 10 by default) return; } if (powerup == false && delaycounter) { // delay sleep in case a preset is being loaded and turnOnAtBoot is disabled (beginStrip() / handleIO() does enable offMode temporarily in this case) delaycounter--; if (delaycounter == 1 && offMode) { // force turn on, no matter the settings (device is bricked if user set sleepDelay=0, no bootup preset and turnOnAtBoot=false) if (briS == 0) bri = 10; // turn on and set low brightness to avoid automatic turn off else bri = briS; strip.setBrightness(bri); // needed to make handleIO() not turn off LEDs (really? does not help in bootup preset) offMode = false; applyPresetWithFallback(0, CALL_MODE_INIT, FX_MODE_STATIC, 0); // try to apply preset 0, fallback to static if (rlyPin >= 0) { digitalWrite(rlyPin, (rlyMde ? HIGH : LOW)); // turn relay on TODO: this should be done by wled, what function to call? } } return; } DEBUG_PRINTLN(F("DeepSleep UM: entering deep sleep...")); powerup = false; // turn leds on in all subsequent bootups (overrides Turn LEDs on after power up/reset' at reboot) if (!pin_is_valid(wakeupPin)) return; esp_err_t halerror = ESP_OK; pinMode(wakeupPin, INPUT); // make sure GPIO is input with pullup/pulldown disabled esp_sleep_disable_wakeup_source(ESP_SLEEP_WAKEUP_ALL); //disable all wake-up sources (just in case) uint32_t wakeupAfterSec = 0; if (presetWake) { int nextInterval = findNextTimerInterval(); if (nextInterval > 1 && nextInterval < INT_MAX) wakeupAfterSec = (nextInterval - 1) * 60; // wakeup before next preset } if (wakeupAfter > 0) { // user-defined interval if (wakeupAfterSec == 0 || (uint32_t)wakeupAfter < wakeupAfterSec) { wakeupAfterSec = wakeupAfter; } } if (wakeupAfterSec > 0) { esp_sleep_enable_timer_wakeup(wakeupAfterSec * (uint64_t)1e6); DEBUG_PRINTF("wakeup after %d seconds\n", wakeupAfterSec); } #if defined(CONFIG_IDF_TARGET_ESP32C3) // ESP32 C3 gpio_hold_dis((gpio_num_t)wakeupPin); // disable hold and configure pin if (wakeWhenHigh) halerror = esp_deep_sleep_enable_gpio_wakeup(1<= 0) { oappend(SET_F("addOption(dd,'")); oappend(String(touch_sensor_channel_io_map[touchchannel]).c_str()); oappend(SET_F("',")); oappend(String(touch_sensor_channel_io_map[touchchannel]).c_str()); oappend(SET_F(");")); } } #endif oappend(SET_F("dd=addDropdown('DeepSleep','wakeWhen');")); oappend(SET_F("addOption(dd,'Low',0);")); oappend(SET_F("addOption(dd,'High',1);")); oappend(SET_F("addInfo('DeepSleep:pull',1,'','-up/down disable: ');")); // first string is suffix, second string is prefix oappend(SET_F("addInfo('DeepSleep:wakeAfter',1,'seconds (0 = never)');")); oappend(SET_F("addInfo('DeepSleep:presetWake',1,'(wake up before next preset timer)');")); oappend(SET_F("addInfo('DeepSleep:delaySleep',1,'seconds (0 = sleep at powerup)');")); // first string is suffix, second string is prefix } /* * 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_DEEP_SLEEP; } }; // add more strings here to reduce flash memory usage const char DeepSleepUsermod::_name[] PROGMEM = "DeepSleep"; const char DeepSleepUsermod::_enabled[] PROGMEM = "enabled"; static DeepSleepUsermod deep_sleep; REGISTER_USERMOD(deep_sleep); ================================================ FILE: usermods/deep_sleep/library.json ================================================ { "name": "deep_sleep", "build": { "libArchive": false } } ================================================ FILE: usermods/deep_sleep/readme.md ================================================ # Deep Sleep usermod This usermod unleashes the low power capabilities of th ESP: when you power off your LEDs (using the UI power button or a macro) the ESP will be put into deep sleep mode, reducing power consumption to a minimum. During deep sleep the ESP is shut down completely: no WiFi, no CPU, no outputs. The only way to wake it up is by using an external signal, a button, or an automation timer set to the nearest preset time. Once it wakes from deep sleep it reboots so ***make sure to use a boot-up preset.*** # A word of warning When you disable the WLED option 'Turn LEDs on after power up/reset' and 'DelaySleep' is set to zero the ESP will go into deep sleep directly after power-up and only start WLED after it has been woken up. If the ESP can not be awoken from deep sleep due to a wrong configuration it has to be factory reset, disabling sleep at power-up. There is no other way to wake it up. # Power Consumption in deep sleep The current drawn by the ESP in deep sleep mode depends on the type and is in the range of 5uA-20uA (as in micro Amperes): - ESP32: 10uA - ESP32 S3: 8uA - ESP32 S2: 20uA - ESP32 C3: 5uA - ESP8266: 20uA (not supported in this usermod) However, there is usually additional components on a controller that increase the value: - Power LED: the power LED on a ESP board draws 500uA - 1mA - LDO: the voltage regulator also draws idle current. Depending on the type used this can be around 50uA up to 10mA (LM1117). Special low power LDOs with very low idle currents do exist - Digital LEDs: WS2812 for example draw a current of about 1mA per LED. To make good use of this usermod it is required to power them off using MOSFETs or a Relay For lowest power consumption, remove the Power LED and make sure your board does not use an LM1117. On a ESP32 C3 Supermini with the power LED removed (no other modifications) powered through the 5V pin I measured a current draw of 50uA in deep sleep. # Useable GPIOs The GPIOs that can be used to wake the ESP from deep sleep are limited. Only pins connected to the internal RTC unit can be used: - ESP32: GPIO 0, 2, 4, 12-15, 25-39 note: input-only GPIOs 34-39 do not have internal pull up/down resistors, external resistors are required - ESP32 S3: GPIO 0-21 - ESP32 S2: GPIO 0-21 - ESP32 C3: GPIO 0-5 - ESP8266 is not supported in this usermod You can however use the selected wake-up pin normally in WLED, it only gets activated as a wake-up pin when your LEDs are powered down. # Limitations To keep this usermod simple and easy to use, it is a very basic implementation of the low-power capabilities provided by the ESP. If you need more advanced control you are welcome to implement your own version based on this usermod. ## Usermod installation Add `deep_sleep` to `custom_usermods` in your platformio.ini. Settings can be changed in the usermod config UI. ### Define Settings There are five parameters you can set: - GPIO: the pin to use for wake-up - WakeWhen High/Low: the pin state that triggers the wake-up - Pull-up/down disable: enable or disable the internal pullup resistors during sleep (does not affect normal use while running) - Wake after: if set larger than 0, ESP will automatically wake-up after this many seconds (Turn LEDs on after power up/reset is overriden, it will always turn on) - Delay sleep: if set larger than 0, ESP will not go to sleep for this many seconds after you power it off. Timer is reset when switched back on during this time. To override the default settings, place the `#define` in wled.h or add `-D DEEPSLEEP_xxx` to your platformio_override.ini build flags * `DEEPSLEEP_WAKEUPPIN x` - define the pin to be used for wake-up, see list of useable pins above. The pin can be used normally as a button pin in WLED. * `DEEPSLEEP_WAKEWHENHIGH` - if defined, wakes up when pin goes high (default is low) * `DEEPSLEEP_DISABLEPULL` - if defined, internal pullup/pulldown is disabled in deep sleep (default is ebnabled) * `DEEPSLEEP_WAKEUPINTERVAL` - number of seconds after which a wake-up happens automatically, sooner if button is pressed. 0 = never. accuracy is about 2% * `DEEPSLEEP_DELAY` - delay between power-off and sleep * `DEEPSLEEP_WAKEUP_TOUCH_PIN` - specify GPIO pin used for touch-based wakeup example for env build flags: `-D USERMOD_DEEP_SLEEP` `-D DEEPSLEEP_WAKEUPPIN=4` `-D DEEPSLEEP_DISABLEPULL=0` ;enable pull-up/down resistors by default `-D DEEPSLEEP_WAKEUPINTERVAL=43200` ;wake up after 12 hours (or when button is pressed) ### Hardware Setup To wake from deep-sleep an external trigger signal on the configured GPIO is required. When using timed-only wake-up, use a GPIO that has an on-board pull-up resistor (GPIO0 on most boards). When using push-buttons it is highly recommended to use an external pull-up resistor: not all IO's on all devices have properly working internal resistors. Using sensors like PIR, IR, touch sensors or any other sensor with a digital output can be used instead of a button. now go on and save some power @dedehai ## Change log 2024-09 * Initial version 2024-10 * Changed from #define configuration to UI configuration ================================================ FILE: usermods/mpu6050_imu/library.json ================================================ { "name": "mpu6050_imu", "build": { "libArchive": false}, "dependencies": { "electroniccats/MPU6050":"1.0.1" } } ================================================ FILE: usermods/mpu6050_imu/mpu6050_imu.cpp ================================================ #include "wled.h" /* This driver reads quaternion data from the MPU6060 and adds it to the JSON This example is adapted from: https://github.com/jrowberg/i2cdevlib/tree/master/Arduino/MPU6050/examples/MPU6050_DMP6_ESPWiFi Tested with a d1 mini esp-12f GY-521 NodeMCU MPU6050 devkit 1.0 board Lolin Description ======= ========== ==================================================== VCC VU (5V USB) Not available on all boards so use 3.3V if needed. GND G Ground SCL D1 (GPIO05) I2C clock SDA D2 (GPIO04) I2C data XDA not connected XCL not connected AD0 not connected INT D8 (GPIO15) Interrupt pin Using usermod: 1. Copy the usermod into the sketch folder (same folder as wled00.ino) 2. Register the usermod by adding #include "usermod_filename.h" in the top and registerUsermod(new MyUsermodClass()) in the bottom of usermods_list.cpp 3. I2Cdev and MPU6050 must be installed as libraries, or else the .cpp/.h file for both classes must be in the include path of your project. To install the libraries add I2Cdevlib-MPU6050@fbde122cc5 to lib_deps in the platformio.ini file. 4. You also need to change lib_compat_mode from strict to soft in platformio.ini (This ignores that I2Cdevlib-MPU6050 doesn't list platform compatibility) 5. Wire up the MPU6050 as detailed above. */ #include "I2Cdev.h" #undef DEBUG_PRINT #undef DEBUG_PRINTLN #undef DEBUG_PRINTF #include "MPU6050_6Axis_MotionApps20.h" //#include "MPU6050.h" // not necessary if using MotionApps include file // Arduino Wire library is required if I2Cdev I2CDEV_ARDUINO_WIRE implementation // is used in I2Cdev.h #if I2CDEV_IMPLEMENTATION == I2CDEV_ARDUINO_WIRE #include "Wire.h" #endif // Restore debug macros // MPU6050 unfortunately uses the same macro names as WLED :( #undef DEBUG_PRINT #undef DEBUG_PRINTLN #undef DEBUG_PRINTF #ifdef WLED_DEBUG #define DEBUG_PRINT(x) DEBUGOUT.print(x) #define DEBUG_PRINTLN(x) DEBUGOUT.println(x) #define DEBUG_PRINTF(x...) DEBUGOUT.printf(x) #else #define DEBUG_PRINT(x) #define DEBUG_PRINTLN(x) #define DEBUG_PRINTF(x...) #endif // ================================================================ // === INTERRUPT DETECTION ROUTINE === // ================================================================ volatile bool mpuInterrupt = false; // indicates whether MPU interrupt pin has gone high void IRAM_ATTR dmpDataReady() { mpuInterrupt = true; } class MPU6050Driver : public Usermod { private: MPU6050 mpu; // configuration state // default values are set in readFromConfig // By making this a struct, we enable easy backup and comparison in the readFromConfig class struct config_t { bool enabled; int8_t interruptPin; int16_t gyro_offset[3]; int16_t accel_offset[3]; }; config_t config; bool configDirty = true; // does the configuration need an update? // MPU control/status vars bool irqBound = false; // set true if we have bound the IRQ pin bool dmpReady = false; // set true if DMP init was successful uint16_t packetSize; // expected DMP packet size (default is 42 bytes) uint16_t fifoCount; // count of all bytes currently in FIFO uint8_t fifoBuffer[64]; // FIFO storage buffer // TODO: some of these can be removed to save memory, processing time if the measurement isn't needed Quaternion qat; // [w, x, y, z] quaternion container float euler[3]; // [psi, theta, phi] Euler angle container float ypr[3]; // [yaw, pitch, roll] yaw/pitch/roll container VectorInt16 aa; // [x, y, z] accel sensor measurements VectorInt16 gy; // [x, y, z] gyro sensor measurements VectorInt16 aaReal; // [x, y, z] gravity-free accel sensor measurements VectorInt16 aaWorld; // [x, y, z] world-frame accel sensor measurements VectorFloat gravity; // [x, y, z] gravity vector uint32_t sample_count; // Usermod output um_data_t um_data; // config element names as progmem strs static const char _name[]; static const char _enabled[]; static const char _interrupt_pin[]; static const char _x_acc_bias[]; static const char _y_acc_bias[]; static const char _z_acc_bias[]; static const char _x_gyro_bias[]; static const char _y_gyro_bias[]; static const char _z_gyro_bias[]; public: inline bool initDone() { return um_data.u_size != 0; }; // recycle this instead of storing an extra variable //Functions called by WLED /* * setup() is called once at boot. WiFi is not yet connected at this point. */ void setup() { dmpReady = false; // Start clean // one time init if (!initDone()) { um_data.u_size = 9; um_data.u_type = new um_types_t[um_data.u_size]; um_data.u_data = new void*[um_data.u_size]; um_data.u_data[0] = &qat; um_data.u_type[0] = UMT_FLOAT_ARR; um_data.u_data[1] = &euler; um_data.u_type[1] = UMT_FLOAT_ARR; um_data.u_data[2] = &ypr; um_data.u_type[2] = UMT_FLOAT_ARR; um_data.u_data[3] = &aa; um_data.u_type[3] = UMT_INT16_ARR; um_data.u_data[4] = &gy; um_data.u_type[4] = UMT_INT16_ARR; um_data.u_data[5] = &aaReal; um_data.u_type[5] = UMT_INT16_ARR; um_data.u_data[6] = &aaWorld; um_data.u_type[6] = UMT_INT16_ARR; um_data.u_data[7] = &gravity; um_data.u_type[7] = UMT_FLOAT_ARR; um_data.u_data[8] = &sample_count; um_data.u_type[8] = UMT_UINT32; } configDirty = false; // we have now accepted the current configuration, success or not if (!config.enabled) return; // TODO: notice if these have changed ?? if (i2c_scl<0 || i2c_sda<0) { DEBUG_PRINTLN(F("MPU6050: I2C is no good.")); return; } // Check the interrupt pin if (config.interruptPin >= 0) { irqBound = PinManager::allocatePin(config.interruptPin, false, PinOwner::UM_IMU); if (!irqBound) { DEBUG_PRINTLN(F("MPU6050: IRQ pin already in use.")); return; } pinMode(config.interruptPin, INPUT); }; #if I2CDEV_IMPLEMENTATION == I2CDEV_ARDUINO_WIRE Wire.setClock(400000U); // 400kHz I2C clock. Comment this line if having compilation difficulties #elif I2CDEV_IMPLEMENTATION == I2CDEV_BUILTIN_FASTWIRE Fastwire::setup(400, true); #endif // initialize device DEBUG_PRINTLN(F("Initializing I2C devices...")); mpu.initialize(); // verify connection DEBUG_PRINTLN(F("Testing device connections...")); DEBUG_PRINTLN(mpu.testConnection() ? F("MPU6050 connection successful") : F("MPU6050 connection failed")); // load and configure the DMP DEBUG_PRINTLN(F("Initializing DMP...")); auto devStatus = mpu.dmpInitialize(); // set offsets (from config) mpu.setXGyroOffset(config.gyro_offset[0]); mpu.setYGyroOffset(config.gyro_offset[1]); mpu.setZGyroOffset(config.gyro_offset[2]); mpu.setXAccelOffset(config.accel_offset[0]); mpu.setYAccelOffset(config.accel_offset[1]); mpu.setZAccelOffset(config.accel_offset[2]); // set sample rate mpu.setRate(16); // ~100Hz // make sure it worked (returns 0 if so) if (devStatus == 0) { // turn on the DMP, now that it's ready DEBUG_PRINTLN(F("Enabling DMP...")); mpu.setDMPEnabled(true); mpuInterrupt = true; if (irqBound) { // enable Arduino interrupt detection DEBUG_PRINTLN(F("Enabling interrupt detection (Arduino external interrupt 0)...")); attachInterrupt(digitalPinToInterrupt(config.interruptPin), dmpDataReady, RISING); } // get expected DMP packet size for later comparison packetSize = mpu.dmpGetFIFOPacketSize(); // set our DMP Ready flag so the main loop() function knows it's okay to use it DEBUG_PRINTLN(F("DMP ready!")); dmpReady = true; } else { // ERROR! // 1 = initial memory load failed // 2 = DMP configuration updates failed // (if it's going to break, usually the code will be 1) DEBUG_PRINT(F("DMP Initialization failed (code ")); DEBUG_PRINT(devStatus); DEBUG_PRINTLN(")"); } fifoCount = 0; sample_count = 0; } /* * connected() is called every time the WiFi is (re)connected * Use it to initialize network interfaces */ void connected() { //DEBUG_PRINTLN(F("Connected to WiFi!")); } /* * loop() is called continuously. Here you can check for events, read sensors, etc. */ void loop() { if (configDirty) setup(); // if programming failed, don't try to do anything if (!config.enabled || !dmpReady || strip.isUpdating()) return; // wait for MPU interrupt or extra packet(s) available // mpuInterrupt is fixed on if interrupt pin is disabled if (!mpuInterrupt && fifoCount < packetSize) return; // reset interrupt flag and get INT_STATUS byte auto mpuIntStatus = mpu.getIntStatus(); // Update current FIFO count fifoCount = mpu.getFIFOCount(); // check for overflow (this should never happen unless our code is too inefficient) if ((mpuIntStatus & 0x10) || fifoCount == 1024) { // reset so we can continue cleanly mpu.resetFIFO(); DEBUG_PRINTLN(F("MPU6050: FIFO overflow!")); // otherwise, check for data ready } else if (fifoCount >= packetSize) { // clear local interrupt pending status, if not polling mpuInterrupt = !irqBound; // DEBUG_PRINT(F("MPU6050: Processing packet: ")); // DEBUG_PRINT(fifoCount); // DEBUG_PRINTLN(F(" bytes in FIFO")); // read a packet from FIFO mpu.getFIFOBytes(fifoBuffer, packetSize); // track FIFO count here in case there is > 1 packet available // (this lets us immediately read more without waiting for an interrupt) fifoCount -= packetSize; //NOTE: some of these can be removed to save memory, processing time // if the measurement isn't needed mpu.dmpGetQuaternion(&qat, fifoBuffer); mpu.dmpGetEuler(euler, &qat); mpu.dmpGetGravity(&gravity, &qat); mpu.dmpGetGyro(&gy, fifoBuffer); mpu.dmpGetAccel(&aa, fifoBuffer); mpu.dmpGetLinearAccel(&aaReal, &aa, &gravity); mpu.dmpGetLinearAccelInWorld(&aaWorld, &aaReal, &qat); mpu.dmpGetYawPitchRoll(ypr, &qat, &gravity); ++sample_count; } } void addToJsonInfo(JsonObject& root) { JsonObject user = root["u"]; if (user.isNull()) user = root.createNestedObject("u"); // Unfortunately the web UI doesn't know how to print sub-objects: you just see '[object Object]' // For now, we just put everything in the root userdata object. //auto imu_meas = user.createNestedObject("IMU"); auto& imu_meas = user; // If an element is an array, the UI expects two elements in the form [value, unit] // Since our /value/ is an array, wrap it, eg. [[a, b, c]] JsonArray quat_json = imu_meas.createNestedArray("Quat").createNestedArray(); quat_json.add(qat.w); quat_json.add(qat.x); quat_json.add(qat.y); quat_json.add(qat.z); JsonArray euler_json = imu_meas.createNestedArray("Euler").createNestedArray(); euler_json.add(euler[0]); euler_json.add(euler[1]); euler_json.add(euler[2]); JsonArray accel_json = imu_meas.createNestedArray("Accel").createNestedArray(); accel_json.add(aa.x); accel_json.add(aa.y); accel_json.add(aa.z); JsonArray gyro_json = imu_meas.createNestedArray("Gyro").createNestedArray(); gyro_json.add(gy.x); gyro_json.add(gy.y); gyro_json.add(gy.z); JsonArray world_json = imu_meas.createNestedArray("WorldAccel").createNestedArray(); world_json.add(aaWorld.x); world_json.add(aaWorld.y); world_json.add(aaWorld.z); JsonArray real_json = imu_meas.createNestedArray("RealAccel").createNestedArray(); real_json.add(aaReal.x); real_json.add(aaReal.y); real_json.add(aaReal.z); JsonArray grav_json = imu_meas.createNestedArray("Gravity").createNestedArray(); grav_json.add(gravity.x); grav_json.add(gravity.y); grav_json.add(gravity.z); JsonArray orient_json = imu_meas.createNestedArray("Orientation").createNestedArray(); orient_json.add(ypr[0]); orient_json.add(ypr[1]); orient_json.add(ypr[2]); } /* * 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) * I highly recommend checking out the basics of ArduinoJson serialization and deserialization in order to use custom settings! */ void addToConfig(JsonObject& root) { JsonObject top = root.createNestedObject(FPSTR(_name)); //save these vars persistently whenever settings are saved top[FPSTR(_enabled)] = config.enabled; top[FPSTR(_interrupt_pin)] = config.interruptPin; top[FPSTR(_x_acc_bias)] = config.accel_offset[0]; top[FPSTR(_y_acc_bias)] = config.accel_offset[1]; top[FPSTR(_z_acc_bias)] = config.accel_offset[2]; top[FPSTR(_x_gyro_bias)] = config.gyro_offset[0]; top[FPSTR(_y_gyro_bias)] = config.gyro_offset[1]; top[FPSTR(_z_gyro_bias)] = config.gyro_offset[2]; } /* * 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 immediately after boot, or after saving on the Usermod Settings page) * * 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 :) * * Return true in case the config values returned from Usermod Settings were complete, or false if you'd like WLED to save your defaults to disk (so any missing values are editable in Usermod Settings) * * getJsonValue() returns false if the value is missing, or copies the value into the variable provided and returns true if the value is present * The configComplete variable is true only if the "exampleUsermod" object and all values are present. If any values are missing, WLED will know to call addToConfig() to save them * * This function is guaranteed to be called on boot, but could also be called every time settings are updated */ bool readFromConfig(JsonObject& root) { // default settings values could be set here (or below using the 3-argument getJsonValue()) instead of in the class definition or constructor // setting them inside readFromConfig() is slightly more robust, handling the rare but plausible use case of single value being missing after boot (e.g. if the cfg.json was manually edited and a value was removed) auto old_cfg = config; JsonObject top = root[FPSTR(_name)]; bool configComplete = top.isNull(); // Ensure default configuration is loaded configComplete &= getJsonValue(top[FPSTR(_enabled)], config.enabled, true); configComplete &= getJsonValue(top[FPSTR(_interrupt_pin)], config.interruptPin, -1); configComplete &= getJsonValue(top[FPSTR(_x_acc_bias)], config.accel_offset[0], 0); configComplete &= getJsonValue(top[FPSTR(_y_acc_bias)], config.accel_offset[1], 0); configComplete &= getJsonValue(top[FPSTR(_z_acc_bias)], config.accel_offset[2], 0); configComplete &= getJsonValue(top[FPSTR(_x_gyro_bias)], config.gyro_offset[0], 0); configComplete &= getJsonValue(top[FPSTR(_y_gyro_bias)], config.gyro_offset[1], 0); configComplete &= getJsonValue(top[FPSTR(_z_gyro_bias)], config.gyro_offset[2], 0); DEBUG_PRINT(FPSTR(_name)); if (top.isNull()) { DEBUG_PRINTLN(F(": No config found. (Using defaults.)")); } else if (!initDone()) { DEBUG_PRINTLN(F(": config loaded.")); } else if (memcmp(&config, &old_cfg, sizeof(config)) == 0) { DEBUG_PRINTLN(F(": config unchanged.")); } else { DEBUG_PRINTLN(F(": config updated.")); // Previously loaded and config changed if (irqBound && ((old_cfg.interruptPin != config.interruptPin) || !config.enabled)) { detachInterrupt(old_cfg.interruptPin); PinManager::deallocatePin(old_cfg.interruptPin, PinOwner::UM_IMU); irqBound = false; } // Re-call setup on the next loop() configDirty = true; } return configComplete; } bool getUMData(um_data_t **data) { if (!data || !config.enabled || !dmpReady) return false; // no pointer provided by caller or not enabled -> exit *data = &um_data; return true; } /* * getId() allows you to optionally give your V2 usermod an unique ID (please define it in const.h!). */ uint16_t getId() { return USERMOD_ID_IMU; } }; const char MPU6050Driver::_name[] PROGMEM = "MPU6050_IMU"; const char MPU6050Driver::_enabled[] PROGMEM = "enabled"; const char MPU6050Driver::_interrupt_pin[] PROGMEM = "interrupt_pin"; const char MPU6050Driver::_x_acc_bias[] PROGMEM = "x_acc_bias"; const char MPU6050Driver::_y_acc_bias[] PROGMEM = "y_acc_bias"; const char MPU6050Driver::_z_acc_bias[] PROGMEM = "z_acc_bias"; const char MPU6050Driver::_x_gyro_bias[] PROGMEM = "x_gyro_bias"; const char MPU6050Driver::_y_gyro_bias[] PROGMEM = "y_gyro_bias"; const char MPU6050Driver::_z_gyro_bias[] PROGMEM = "z_gyro_bias"; static MPU6050Driver mpu6050_imu; REGISTER_USERMOD(mpu6050_imu); ================================================ FILE: usermods/mpu6050_imu/readme.md ================================================ # MPU-6050 Six-Axis (Gyro + Accelerometer) Driver v2 of this usermod enables connection of a MPU-6050 IMU sensor to work with effects controlled by the orientation or motion of the WLED Device. The MPU6050 has a built in "Digital Motion Processor" which does the "heavy lifting" integrating the gyro and accelerometer measurements to get potentially more useful gravity vector and orientation output. It is fairly straightforward to comment out variables being read from the device if they're not needed. Saves CPU/Memory/Bandwidth. _Story:_ As a memento to a long trip I was on, I built an icosahedron globe. I put lights inside to indicate cities I travelled to. I wanted to integrate an IMU to allow either on-board, or off-board effects that would react to the globes orientation. See the blog post on building it or a video demo . ## Wiring The connections needed to the MPU6050 are as follows: ``` VCC VU (5V USB) Not available on all boards so use 3.3V if needed. GND G Ground SCL D1 (GPIO05) I2C clock SDA D2 (GPIO04) I2C data XDA not connected XCL not connected AD0 not connected INT D8 (GPIO15) Interrupt pin ``` You could probably modify the code not to need an interrupt, but I used the setup directly from the example. ## JSON API This code adds: ```json "u":{ "IMU":{ "Quat": [w, x, y, z], "Euler": [psi, theta, phi], "Gyro": [x, y, z], "Accel": [x, y, z], "RealAccel": [x, y, z], "WorldAccel": [x, y, z], "Gravity": [x, y, z], "Orientation": [yaw, pitch, roll] } } ``` to the info object ## Usermod installation Add `mpu6050_imu` to `custom_usermods` in your platformio_override.ini. Example **platformio_override.ini**: ```ini [env:usermod_mpu6050_imu_esp32dev] extends = env:esp32dev custom_usermods = ${env:esp32dev.custom_usermods} mpu6050_imu ``` ================================================ FILE: usermods/mpu6050_imu/usermod_gyro_surge.h ================================================ #pragma once /* This usermod uses gyro data to provide a "surge" effect on movement Requires lib_deps = bolderflight/Bolder Flight Systems Eigen@^3.0.0 */ #include "wled.h" // Eigen include block #ifdef A0 namespace { constexpr size_t A0_temp {A0}; } #undef A0 static constexpr size_t A0 {A0_temp}; #endif #ifdef A1 namespace { constexpr size_t A1_temp {A1}; } #undef A1 static constexpr size_t A1 {A1_temp}; #endif #ifdef B0 namespace { constexpr size_t B0_temp {B0}; } #undef B0 static constexpr size_t B0 {B0_temp}; #endif #ifdef B1 namespace { constexpr size_t B1_temp {B1}; } #undef B1 static constexpr size_t B1 {B1_temp}; #endif #ifdef D0 namespace { constexpr size_t D0_temp {D0}; } #undef D0 static constexpr size_t D0 {D0_temp}; #endif #ifdef D1 namespace { constexpr size_t D1_temp {D1}; } #undef D1 static constexpr size_t D1 {D1_temp}; #endif #ifdef D2 namespace { constexpr size_t D2_temp {D2}; } #undef D2 static constexpr size_t D2 {D2_temp}; #endif #ifdef D3 namespace { constexpr size_t D3_temp {D3}; } #undef D3 static constexpr size_t D3 {D3_temp}; #endif #include "eigen.h" #include constexpr auto ESTIMATED_G = 9.801; // m/s^2 constexpr auto ESTIMATED_G_COUNTS = 8350.; constexpr auto ESTIMATED_ANGULAR_RATE = (M_PI * 2000) / (INT16_MAX * 180); // radians per second // Horribly lame digital filter code // Currently implements a static IIR filter. template class xir_filter { typedef Eigen::Array array_t; const array_t a_coeff, b_coeff; const T gain; array_t x, y; public: xir_filter(T gain_, array_t a, array_t b) : a_coeff(std::move(a)), b_coeff(std::move(b)), gain(gain_), x(array_t::Zero()), y(array_t::Zero()) {}; T operator()(T input) { x.head(C-1) = x.tail(C-1); // shift by one x(C-1) = input / gain; y.head(C-1) = y.tail(C-1); // shift by one y(C-1) = (x * b_coeff).sum(); y(C-1) -= (y.head(C-1) * a_coeff.head(C-1)).sum(); return y(C-1); } T last() { return y(C-1); }; }; class GyroSurge : public Usermod { private: static const char _name[]; bool enabled = true; // Params uint8_t max = 0; float sensitivity = 0; // State uint32_t last_sample; // 100hz input // butterworth low pass filter at 20hz xir_filter filter = { 1., { -0.36952738, 0.19581571, 1.}, {0.20657208, 0.41314417, 0.20657208} }; // { 1., { 0., 0., 1.}, { 0., 0., 1. } }; // no filter public: /* * setup() is called once at boot. WiFi is not yet connected at this point. */ void setup() {}; /* * 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) * I highly recommend checking out the basics of ArduinoJson serialization and deserialization in order to use custom settings! */ void addToConfig(JsonObject& root) { JsonObject top = root.createNestedObject(FPSTR(_name)); //save these vars persistently whenever settings are saved top["max"] = max; top["sensitivity"] = sensitivity; } /* * 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 immediately after boot, or after saving on the Usermod Settings page) * * 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 :) * * Return true in case the config values returned from Usermod Settings were complete, or false if you'd like WLED to save your defaults to disk (so any missing values are editable in Usermod Settings) * * getJsonValue() returns false if the value is missing, or copies the value into the variable provided and returns true if the value is present * The configComplete variable is true only if the "exampleUsermod" object and all values are present. If any values are missing, WLED will know to call addToConfig() to save them * * This function is guaranteed to be called on boot, but could also be called every time settings are updated */ bool readFromConfig(JsonObject& root) { // default settings values could be set here (or below using the 3-argument getJsonValue()) instead of in the class definition or constructor // setting them inside readFromConfig() is slightly more robust, handling the rare but plausible use case of single value being missing after boot (e.g. if the cfg.json was manually edited and a value was removed) JsonObject top = root[FPSTR(_name)]; bool configComplete = !top.isNull(); configComplete &= getJsonValue(top["max"], max, 0); configComplete &= getJsonValue(top["sensitivity"], sensitivity, 10); return configComplete; } void loop() { // get IMU data um_data_t *um_data; if (!UsermodManager::getUMData(&um_data, USERMOD_ID_IMU)) { // Apply max strip.getSegment(0).fadeToBlackBy(max); return; } uint32_t sample_count = *(uint32_t*)(um_data->u_data[8]); if (sample_count != last_sample) { last_sample = sample_count; // Calculate based on new data // We use the raw gyro data (angular rate) auto gyros = (int16_t*)um_data->u_data[4]; // 16384 == 2000 deg/s // Compute the overall rotation rate // For my application (a plasma sword) we ignore X axis rotations (eg. around the long axis) auto gyro_q = Eigen::AngleAxis { //Eigen::AngleAxis(ESTIMATED_ANGULAR_RATE * gyros[0], Eigen::Vector3f::UnitX()) * Eigen::AngleAxis(ESTIMATED_ANGULAR_RATE * gyros[1], Eigen::Vector3f::UnitY()) * Eigen::AngleAxis(ESTIMATED_ANGULAR_RATE * gyros[2], Eigen::Vector3f::UnitZ()) }; // Filter the results filter(std::min(sensitivity * gyro_q.angle(), 1.0f)); // radians per second /* Serial.printf("[%lu] Gy: %d, %d, %d -- ", millis(), (int)gyros[0], (int)gyros[1], (int)gyros[2]); Serial.print(gyro_q.angle()); Serial.print(", "); Serial.print(sensitivity * gyro_q.angle()); Serial.print(" --> "); Serial.println(filter.last()); */ } }; // noop /* * handleOverlayDraw() is called just before every show() (LED strip update frame) after effects have set the colors. * Use this to blank out some LEDs or set them to a different color regardless of the set effect mode. * Commonly used for custom clocks (Cronixie, 7 segment) */ void handleOverlayDraw() { // TODO: some kind of timing analysis for filtering ... // Calculate brightness boost auto r_float = std::max(std::min(filter.last(), 1.0f), 0.f); auto result = (uint8_t) (r_float * max); //Serial.printf("[%lu] %d -- ", millis(), result); //Serial.println(r_float); // TODO - multiple segment handling?? strip.getSegment(0).fadeToBlackBy(max - result); } }; const char GyroSurge::_name[] PROGMEM = "GyroSurge"; ================================================ FILE: usermods/multi_relay/library.json ================================================ { "name": "multi_relay", "build": { "libArchive": false } } ================================================ FILE: usermods/multi_relay/multi_relay.cpp ================================================ #include "wled.h" #define COUNT_OF(x) ((sizeof(x)/sizeof(0[x])) / ((size_t)(!(sizeof(x) % sizeof(0[x]))))) #ifndef MULTI_RELAY_MAX_RELAYS #define MULTI_RELAY_MAX_RELAYS 4 #else #if MULTI_RELAY_MAX_RELAYS>8 #undef MULTI_RELAY_MAX_RELAYS #define MULTI_RELAY_MAX_RELAYS 8 #warning Maximum relays set to 8 #endif #endif #ifndef MULTI_RELAY_PINS #define MULTI_RELAY_PINS -1 #define MULTI_RELAY_ENABLED false #else #define MULTI_RELAY_ENABLED true #endif #ifndef MULTI_RELAY_HA_DISCOVERY #define MULTI_RELAY_HA_DISCOVERY false #endif #ifndef MULTI_RELAY_DELAYS #define MULTI_RELAY_DELAYS 0 #endif #ifndef MULTI_RELAY_EXTERNALS #define MULTI_RELAY_EXTERNALS false #endif #ifndef MULTI_RELAY_INVERTS #define MULTI_RELAY_INVERTS false #endif #define WLED_DEBOUNCE_THRESHOLD 50 //only consider button input of at least 50ms as valid (debouncing) #define ON true #define OFF false #ifndef USERMOD_USE_PCF8574 #undef USE_PCF8574 #define USE_PCF8574 false #else #undef USE_PCF8574 #define USE_PCF8574 true #endif #ifndef PCF8574_ADDRESS #define PCF8574_ADDRESS 0x20 // some may start at 0x38 #endif /* * This usermod handles multiple relay outputs. * These outputs complement built-in relay output in a way that the activation can be delayed. * They can also activate/deactivate in reverse logic independently. * * Written and maintained by @blazoncek */ typedef struct relay_t { int8_t pin; struct { // reduces memory footprint bool active : 1; // is the relay waiting to be switched bool invert : 1; // does On mean 1 or 0 bool state : 1; // 1 relay is On, 0 relay is Off bool external : 1; // is the relay externally controlled int8_t button : 4; // which button triggers relay }; uint16_t delay; // amount of ms to wait after it is activated } Relay; class MultiRelay : public Usermod { private: // array of relays Relay _relay[MULTI_RELAY_MAX_RELAYS]; uint32_t _switchTimerStart; // switch timer start time bool _oldMode; // old brightness bool enabled; // usermod enabled bool initDone; // status of initialisation bool usePcf8574; uint8_t addrPcf8574; bool HAautodiscovery; uint16_t periodicBroadcastSec; unsigned long lastBroadcast; // strings to reduce flash memory usage (used more than twice) static const char _name[]; static const char _enabled[]; static const char _relay_str[]; static const char _delay_str[]; static const char _activeHigh[]; static const char _external[]; static const char _button[]; static const char _broadcast[]; static const char _HAautodiscovery[]; static const char _pcf8574[]; static const char _pcfAddress[]; static const char _switch[]; static const char _toggle[]; static const char _Command[]; void handleOffTimer(); void InitHtmlAPIHandle(); int getValue(String data, char separator, int index); uint8_t getActiveRelayCount(); byte IOexpanderWrite(byte address, byte _data); byte IOexpanderRead(int address); void publishMqtt(int relay); #ifndef WLED_DISABLE_MQTT void publishHomeAssistantAutodiscovery(); #endif public: /** * constructor */ MultiRelay(); /** * desctructor */ //~MultiRelay() {} /** * Enable/Disable the usermod */ inline void enable(bool enable) { enabled = enable; } /** * Get usermod enabled/disabled state */ inline bool isEnabled() { return enabled; } /** * 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. */ inline uint16_t getId() override { return USERMOD_ID_MULTI_RELAY; } /** * switch relay on/off */ void switchRelay(uint8_t relay, bool mode); /** * toggle relay */ inline void toggleRelay(uint8_t relay) { switchRelay(relay, !_relay[relay].state); } /** * 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() override; /** * connected() is called every time the WiFi is (re)connected * Use it to initialize network interfaces */ inline void connected() override { InitHtmlAPIHandle(); } /** * loop() is called continuously. Here you can check for events, read sensors, etc. */ void loop() override; #ifndef WLED_DISABLE_MQTT bool onMqttMessage(char* topic, char* payload) override; void onMqttConnect(bool sessionPresent) override; #endif /** * handleButton() can be used to override default button behaviour. Returning true * will prevent button working in a default way. * Replicating button.cpp */ bool handleButton(uint8_t b) override; /** * addToJsonInfo() can be used to add custom entries to the /json/info part of the JSON API. */ void addToJsonInfo(JsonObject &root) override; /** * 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) override; /** * 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) override; /** * provide the changeable values */ void addToConfig(JsonObject &root) override; void appendConfigData() override; /** * restore the changeable values * readFromConfig() is called before setup() to populate properties from values stored in cfg.json * * The function should return true if configuration was successfully loaded or false if there was no configuration. */ bool readFromConfig(JsonObject &root) override; }; // class implementation void MultiRelay::publishMqtt(int relay) { #ifndef WLED_DISABLE_MQTT //Check if MQTT Connected, otherwise it will crash the 8266 if (WLED_MQTT_CONNECTED){ char subuf[64]; sprintf_P(subuf, PSTR("%s/relay/%d"), mqttDeviceTopic, relay); mqtt->publish(subuf, 0, false, _relay[relay].state ? "on" : "off"); } #endif } /** * switch off the strip if the delay has elapsed */ void MultiRelay::handleOffTimer() { unsigned long now = millis(); bool activeRelays = false; for (int i=0; i 0 && now - _switchTimerStart > (_relay[i].delay*1000)) { if (!_relay[i].external) switchRelay(i, !offMode); _relay[i].active = false; } else if (periodicBroadcastSec && now - lastBroadcast > (periodicBroadcastSec*1000)) { if (_relay[i].pin>=0) publishMqtt(i); } activeRelays = activeRelays || _relay[i].active; } if (!activeRelays) _switchTimerStart = 0; if (periodicBroadcastSec && now - lastBroadcast > (periodicBroadcastSec*1000)) lastBroadcast = now; } /** * HTTP API handler * borrowed from: * https://github.com/gsieben/WLED/blob/master/usermods/GeoGab-Relays/usermod_GeoGab.h */ #define GEOGABVERSION "0.1.3" void MultiRelay::InitHtmlAPIHandle() { // https://github.com/me-no-dev/ESPAsyncWebServer DEBUG_PRINTLN(F("Relays: Initialize HTML API")); server.on(F("/relays"), HTTP_GET, [this](AsyncWebServerRequest *request) { DEBUG_PRINTLN(F("Relays: HTML API")); String janswer; String error = ""; //int params = request->params(); janswer = F("{\"NoOfRelays\":"); janswer += String(MULTI_RELAY_MAX_RELAYS) + ","; if (getActiveRelayCount()) { // Commands if (request->hasParam(FPSTR(_switch))) { /**** Switch ****/ AsyncWebParameter* p = request->getParam(FPSTR(_switch)); // Get Values for (int i=0; ivalue(), ',', i); if (value==-1) { error = F("There must be as many arguments as relays"); } else { // Switch if (_relay[i].external) switchRelay(i, (bool)value); } } } else if (request->hasParam(FPSTR(_toggle))) { /**** Toggle ****/ AsyncWebParameter* p = request->getParam(FPSTR(_toggle)); // Get Values for (int i=0;ivalue(), ',', i); if (value==-1) { error = F("There must be as many arguments as relays"); } else { // Toggle if (value && _relay[i].external) toggleRelay(i); } } } else { error = F("No valid command found"); } } else { error = F("No active relays"); } // Status response char sbuf[16]; for (int i=0; isend(200, "application/json", janswer); }); } int MultiRelay::getValue(String data, char separator, int index) { int found = 0; int strIndex[] = {0, -1}; int maxIndex = data.length()-1; for(int i=0; i<=maxIndex && found<=index; i++){ if(data.charAt(i)==separator || i==maxIndex){ found++; strIndex[0] = strIndex[1]+1; strIndex[1] = (i == maxIndex) ? i+1 : i; } } return found>index ? data.substring(strIndex[0], strIndex[1]).toInt() : -1; } //Write a byte to the IO expander byte MultiRelay::IOexpanderWrite(byte address, byte _data ) { Wire.beginTransmission(address); Wire.write(_data); return Wire.endTransmission(); } //Read a byte from the IO expander byte MultiRelay::IOexpanderRead(int address) { byte _data = 0; Wire.requestFrom(address, 1); if (Wire.available()) { _data = Wire.read(); } return _data; } // public methods MultiRelay::MultiRelay() : _switchTimerStart(0) , enabled(MULTI_RELAY_ENABLED) , initDone(false) , usePcf8574(USE_PCF8574) , addrPcf8574(PCF8574_ADDRESS) , HAautodiscovery(MULTI_RELAY_HA_DISCOVERY) , periodicBroadcastSec(60) , lastBroadcast(0) { const int8_t defPins[] = {MULTI_RELAY_PINS}; const int8_t relayDelays[] = {MULTI_RELAY_DELAYS}; const bool relayExternals[] = {MULTI_RELAY_EXTERNALS}; const bool relayInverts[] = {MULTI_RELAY_INVERTS}; for (size_t i=0; i=MULTI_RELAY_MAX_RELAYS || _relay[relay].pin<0) return; _relay[relay].state = mode; if (usePcf8574 && _relay[relay].pin >= 100) { // we need to send all outputs at the same time uint8_t state = 0; for (int i=0; i=0) count++; return count; } //Functions called by WLED #ifndef WLED_DISABLE_MQTT /** * handling of MQTT message * topic only contains stripped topic (part after /wled/MAC) * topic should look like: /relay/X/command; where X is relay number, 0 based */ bool MultiRelay::onMqttMessage(char* topic, char* payload) { if (strlen(topic) > 8 && strncmp_P(topic, PSTR("/relay/"), 7) == 0 && strncmp_P(topic+8, _Command, 8) == 0) { uint8_t relay = strtoul(topic+7, NULL, 10); if (relaysubscribe(subuf, 0); if (HAautodiscovery) publishHomeAssistantAutodiscovery(); for (int i=0; i= 0 && _relay[i].external) { StaticJsonDocument<1024> json; sprintf_P(buf, PSTR("%s Switch %d"), serverDescription, i); //max length: 33 + 8 + 3 = 44 json[F("name")] = buf; sprintf_P(buf, PSTR("%s/relay/%d"), mqttDeviceTopic, i); //max length: 33 + 7 + 3 = 43 json["~"] = buf; strcat_P(buf, _Command); mqtt->subscribe(buf, 0); json[F("stat_t")] = "~"; json[F("cmd_t")] = F("~/command"); json[F("pl_off")] = "off"; json[F("pl_on")] = "on"; json[F("uniq_id")] = uid; strcpy(buf, mqttDeviceTopic); //max length: 33 + 7 = 40 strcat_P(buf, PSTR("/status")); json[F("avty_t")] = buf; json[F("pl_avail")] = F("online"); json[F("pl_not_avail")] = F("offline"); //TODO: dev payload_size = serializeJson(json, json_str); } else { //Unpublish disabled or internal relays json_str[0] = 0; payload_size = 0; } sprintf_P(buf, PSTR("homeassistant/switch/%s/config"), uid); mqtt->publish(buf, 0, true, json_str, payload_size); } } #endif /** * 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 MultiRelay::setup() { // pins retrieved from cfg.json (readFromConfig()) prior to running setup() // if we want PCF8574 expander I2C pins need to be valid if (i2c_sda<0 || i2c_scl<0) usePcf8574 = false; uint8_t state = 0; for (int i=0; i= 100) { uint8_t pin = _relay[i].pin - 100; if (!_relay[i].external) _relay[i].state = !offMode; state |= (uint8_t)(_relay[i].invert ? !_relay[i].state : _relay[i].state) << pin; } else if (_relay[i].pin<100 && _relay[i].pin>=0) { if (PinManager::allocatePin(_relay[i].pin,true, PinOwner::UM_MultiRelay)) { if (!_relay[i].external) _relay[i].state = !offMode; switchRelay(i, _relay[i].state); _relay[i].active = false; } else { _relay[i].pin = -1; // allocation failed } } } if (usePcf8574) { IOexpanderWrite(addrPcf8574, state); // init expander (set all outputs) DEBUG_PRINTLN(F("PCF8574(s) inited.")); } _oldMode = offMode; initDone = true; } /** * loop() is called continuously. Here you can check for events, read sensors, etc. */ void MultiRelay::loop() { static unsigned long lastUpdate = 0; yield(); if (!enabled || (strip.isUpdating() && millis() - lastUpdate < 100)) return; if (millis() - lastUpdate < 100) return; // update only 10 times/s lastUpdate = millis(); //set relay when LEDs turn on if (_oldMode != offMode) { _oldMode = offMode; _switchTimerStart = millis(); for (int i=0; i=0) && !_relay[i].external) _relay[i].active = true; } } handleOffTimer(); } /** * handleButton() can be used to override default button behaviour. Returning true * will prevent button working in a default way. * Replicating button.cpp */ bool MultiRelay::handleButton(uint8_t b) { yield(); if (!enabled || buttons[b].type == BTN_TYPE_NONE || buttons[b].type == BTN_TYPE_RESERVED || buttons[b].type == BTN_TYPE_PIR_SENSOR || buttons[b].type == BTN_TYPE_ANALOG || buttons[b].type == BTN_TYPE_ANALOG_INVERTED) { return false; } bool handled = false; for (int i=0; i WLED_DEBOUNCE_THRESHOLD) { //fire edge event only after 50ms without change (debounce) for (int i=0; i 600) { //long press //longPressAction(b); //not exposed //handled = false; //use if you want to pass to default behaviour buttons[b].longPressed = true; } } else if (!isButtonPressed(b) && buttons[b].pressedBefore) { //released long dur = now - buttons[b].pressedTime; if (dur < WLED_DEBOUNCE_THRESHOLD) { buttons[b].pressedBefore = false; return handled; } //too short "press", debounce bool doublePress = buttons[b].waitTime; //did we have short press before? buttons[b].waitTime = 0; if (!buttons[b].longPressed) { //short press // if this is second release within 350ms it is a double press (buttonWaitTime!=0) if (doublePress) { //doublePressAction(b); //not exposed //handled = false; //use if you want to pass to default behaviour } else { buttons[b].waitTime = now; } } buttons[b].pressedBefore = false; buttons[b].longPressed = false; } // if 350ms elapsed since last press/release it is a short press if (buttons[b].waitTime && now - buttons[b].waitTime > 350 && !buttons[b].pressedBefore) { buttons[b].waitTime = 0; //shortPressAction(b); //not exposed for (int i=0; i"); uiDomString += F(""); infoArr.add(uiDomString); } } } /** * 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 MultiRelay::addToJsonState(JsonObject &root) { if (!initDone || !enabled) return; // prevent crash on boot applyPreset() JsonObject multiRelay = root[FPSTR(_name)]; if (multiRelay.isNull()) { multiRelay = root.createNestedObject(FPSTR(_name)); } #if MULTI_RELAY_MAX_RELAYS > 1 JsonArray rel_arr = multiRelay.createNestedArray(F("relays")); for (int i=0; i() && usermod[FPSTR(_relay_str)].as()>=0) { int rly = usermod[FPSTR(_relay_str)].as(); if (usermod["on"].is()) { switchRelay(rly, usermod["on"].as()); } else if (usermod["on"].is() && usermod["on"].as()[0] == 't') { toggleRelay(rly); } } } else if (root[FPSTR(_name)].is()) { JsonArray relays = root[FPSTR(_name)].as(); for (JsonVariant r : relays) { if (r[FPSTR(_relay_str)].is() && r[FPSTR(_relay_str)].as()>=0) { int rly = r[FPSTR(_relay_str)].as(); if (r["on"].is()) { switchRelay(rly, r["on"].as()); } else if (r["on"].is() && r["on"].as()[0] == 't') { toggleRelay(rly); } } } } } /** * provide the changeable values */ void MultiRelay::addToConfig(JsonObject &root) { JsonObject top = root.createNestedObject(FPSTR(_name)); top[FPSTR(_enabled)] = enabled; top[FPSTR(_pcf8574)] = usePcf8574; top[FPSTR(_pcfAddress)] = addrPcf8574; top[FPSTR(_broadcast)] = periodicBroadcastSec; top[FPSTR(_HAautodiscovery)] = HAautodiscovery; for (int i=0; i(not hex!)');")); oappend(F("addInfo('MultiRelay:broadcast-sec',1,'(MQTT message)');")); //oappend(F("addInfo('MultiRelay:relay-0:pin',1,'(use -1 for PCF8574)');")); oappend(F("d.extra.push({'MultiRelay':{pin:[['P0',100],['P1',101],['P2',102],['P3',103],['P4',104],['P5',105],['P6',106],['P7',107]]}});")); } /** * restore the changeable values * readFromConfig() is called before setup() to populate properties from values stored in cfg.json * * The function should return true if configuration was successfully loaded or false if there was no configuration. */ bool MultiRelay::readFromConfig(JsonObject &root) { int8_t oldPin[MULTI_RELAY_MAX_RELAYS]; JsonObject top = root[FPSTR(_name)]; if (top.isNull()) { DEBUG_PRINT(FPSTR(_name)); DEBUG_PRINTLN(F(": No config found. (Using defaults.)")); return false; } //bool configComplete = !top.isNull(); //configComplete &= getJsonValue(top[FPSTR(_enabled)], enabled); enabled = top[FPSTR(_enabled)] | enabled; usePcf8574 = top[FPSTR(_pcf8574)] | usePcf8574; addrPcf8574 = top[FPSTR(_pcfAddress)] | addrPcf8574; // if I2C is not globally initialised just ignore if (i2c_sda<0 || i2c_scl<0) usePcf8574 = false; periodicBroadcastSec = top[FPSTR(_broadcast)] | periodicBroadcastSec; periodicBroadcastSec = min(900,max(0,(int)periodicBroadcastSec)); HAautodiscovery = top[FPSTR(_HAautodiscovery)] | HAautodiscovery; for (int i=0; i=0 && oldPin[i]<100) { PinManager::deallocatePin(oldPin[i], PinOwner::UM_MultiRelay); } // allocate new pins setup(); DEBUG_PRINTLN(F(" config (re)loaded.")); } // use "return !top["newestParameter"].isNull();" when updating Usermod with new features return !top[FPSTR(_pcf8574)].isNull(); } // strings to reduce flash memory usage (used more than twice) const char MultiRelay::_name[] PROGMEM = "MultiRelay"; const char MultiRelay::_enabled[] PROGMEM = "enabled"; const char MultiRelay::_relay_str[] PROGMEM = "relay"; const char MultiRelay::_delay_str[] PROGMEM = "delay-s"; const char MultiRelay::_activeHigh[] PROGMEM = "active-high"; const char MultiRelay::_external[] PROGMEM = "external"; const char MultiRelay::_button[] PROGMEM = "button"; const char MultiRelay::_broadcast[] PROGMEM = "broadcast-sec"; const char MultiRelay::_HAautodiscovery[] PROGMEM = "HA-autodiscovery"; const char MultiRelay::_pcf8574[] PROGMEM = "use-PCF8574"; const char MultiRelay::_pcfAddress[] PROGMEM = "PCF8574-address"; const char MultiRelay::_switch[] PROGMEM = "switch"; const char MultiRelay::_toggle[] PROGMEM = "toggle"; const char MultiRelay::_Command[] PROGMEM = "/command"; static MultiRelay multi_relay; REGISTER_USERMOD(multi_relay); ================================================ FILE: usermods/multi_relay/readme.md ================================================ # Multi Relay This usermod-v2 modification allows the connection of multiple relays, each with individual delay and on/off mode. Usermod supports PCF8574 I2C port expander to reduce GPIO use. PCF8574 supports 8 outputs and each output corresponds to a relay in WLED (relay 0 = port 0, etc). I you are using more than 8 relays with multiple PCF8574 make sure their addresses are set in sequence (e.g. 0x20 and 0x21). You can set address of first expander in settings. (**NOTE:** Will require Wire library and global I2C pins defined.) ## HTTP API All responses are returned in JSON format. * Status Request: `http://[device-ip]/relays` * Switch Command: `http://[device-ip]/relays?switch=1,0,1,1` The number of values behind the switch parameter must correspond to the number of relays. The value 1 switches the relay on, 0 switches it off. * Toggle Command: `http://[device-ip]/relays?toggle=1,0,1,1` The number of values behind the parameter switch must correspond to the number of relays. The value 1 causes the relay to toggle, 0 leaves its state unchanged. Examples: 1. total of 4 relays, relay 2 will be toggled: `http://[device-ip]/relays?toggle=0,1,0,0` 2. total of 3 relays, relay 1&3 will be switched on: `http://[device-ip]/relays?switch=1,0,1` ## JSON API You can toggle the relay state by sending the following JSON object to: `http://[device-ip]/json` Switch relay 0 on: `{"MultiRelay":{"relay":0,"on":true}}` Switch relay 3 and 4 off: `{"MultiRelay":[{"relay":2,"on":false},{"relay":3,"on":false}]}` ## MQTT API * `wled`/_deviceMAC_/`relay`/`0`/`command` `on`|`off`|`toggle` * `wled`/_deviceMAC_/`relay`/`1`/`command` `on`|`off`|`toggle` When a relay is switched, a message is published: * `wled`/_deviceMAC_/`relay`/`0` `on`|`off` ## Usermod installation Add `multi_relay` to the `custom_usermods` of your platformio.ini environment. You can override the default maximum number of relays (which is 4) by defining MULTI_RELAY_MAX_RELAYS. Some settings can be defined (defaults) at compile time by setting the following defines: ```cpp // enable or disable HA discovery for externally controlled relays #define MULTI_RELAY_HA_DISCOVERY true ``` The following definitions should be a list of values (maximum number of entries is MULTI_RELAY_MAX_RELAYS) that will be applied to the relays in order: (e.g. assuming MULTI_RELAY_MAX_RELAYS=2) ```cpp #define MULTI_RELAY_PINS 12,18 #define MULTI_RELAY_DELAYS 0,0 #define MULTI_RELAY_EXTERNALS false,true #define MULTI_RELAY_INVERTS false,false ``` These can be set via your `platformio_override.ini` file or as `#define` in your `my_config.h` (remember to set `WLED_USE_MY_CONFIG` in your `platformio_override.ini`) ## Configuration Usermod can be configured via the Usermods settings page. * `enabled` - enable/disable usermod * `use-PCF8574` - use PCF8574 port expander instead of GPIO pins * `first-PCF8574` - I2C address of first expander (WARNING: enter *decimal* value) * `broadcast`- time in seconds between MQTT relay-state broadcasts * `HA-discovery`- enable Home Assistant auto discovery * `pin` - ESP GPIO pin the relay is connected to (can be configured at compile time `-D MULTI_RELAY_PINS=xx,xx,...`) * `delay-s` - delay in seconds after on/off command is received * `active-high` - assign high/low activation of relay (can be used to reverse relay states) * `external` - if enabled, WLED does not control relay, it can only be triggered by an external command (MQTT, HTTP, JSON or button) * `button` - button (from LED Settings) that controls this relay If there is no MultiRelay section, just save current configuration and re-open Usermods settings page. Have fun - @blazoncek ## Change log 2021-04 * First implementation. 2021-11 * Added information about dynamic configuration options * Added button support. 2023-05 * Added support for PCF8574 I2C port expander (multiple) 2023-11 * @chrisburrows Added support for compile time defaults for setting DELAY, EXTERNAL, INVERTS and HA discovery ================================================ FILE: usermods/photoresistor_sensor_mqtt_v1/README.md ================================================ # Photoresister sensor with MQTT Enables attaching a photoresistor sensor like the KY-018 and publishing the readings as a percentage, via MQTT. The frequency of MQTT messages is user definable. A threshold value can be set so significant changes in the readings are published immediately vice waiting for the next update. This was found to be a good compromise between excessive MQTT traffic and delayed updates. I also found it useful to limit the frequency of analog pin reads, otherwise the board hangs. This usermod has only been tested with the KY-018 sensor though it should work for any other analog pin sensor. Note: this does not control the LED strip directly, it only publishes MQTT readings for use with other integrations like Home Assistant. ## Installation Copy and replace the file `usermod.cpp` in wled00 directory. ================================================ FILE: usermods/photoresistor_sensor_mqtt_v1/usermod.cpp ================================================ #include "wled.h" /* * This v1 usermod file allows you to add own functionality to WLED more easily * See: https://github.com/wled-dev/WLED/wiki/Add-own-functionality * EEPROM bytes 2750+ are reserved for your custom use case. (if you extend #define EEPSIZE in const.h) * If you just need 8 bytes, use 2551-2559 (you do not need to increase EEPSIZE) * * Consider the v2 usermod API if you need a more advanced feature set! */ //Use userVar0 and userVar1 (API calls &U0=,&U1=, uint16_t) const int LIGHT_PIN = A0; // define analog pin const long UPDATE_MS = 30000; // Upper threshold between mqtt messages const char MQTT_TOPIC[] = "/light"; // MQTT topic for sensor values const int CHANGE_THRESHOLD = 5; // Change threshold in percentage to send before UPDATE_MS // variables long lastTime = 0; long timeDiff = 0; long readTime = 0; int lightValue = 0; float lightPercentage = 0; float lastPercentage = 0; //gets called once at boot. Do all initialization that doesn't depend on network here void userSetup() { pinMode(LIGHT_PIN, INPUT); } //gets called every time WiFi is (re-)connected. Initialize own network interfaces here void userConnected() { } void publishMqtt(float state) { //Check if MQTT Connected, otherwise it will crash the 8266 if (mqtt != nullptr){ char subuf[38]; strcpy(subuf, mqttDeviceTopic); strcat(subuf, MQTT_TOPIC); mqtt->publish(subuf, 0, true, String(state).c_str()); } } //loop. You can use "if (WLED_CONNECTED)" to check for successful connection void userLoop() { // Read only every 500ms, otherwise it causes the board to hang if (millis() - readTime > 500) { readTime = millis(); timeDiff = millis() - lastTime; // Convert value to percentage lightValue = analogRead(LIGHT_PIN); lightPercentage = ((float)lightValue * -1 + 1024)/(float)1024 *100; // Send MQTT message on significant change or after UPDATE_MS if (abs(lightPercentage - lastPercentage) > CHANGE_THRESHOLD || timeDiff > UPDATE_MS) { publishMqtt(lightPercentage); lastTime = millis(); lastPercentage = lightPercentage; } } } ================================================ FILE: usermods/pixels_dice_tray/BLE_REQUIREMENT.md ================================================ # pixels_dice_tray Usermod - BLE Requirement Notice ## Important: This Usermod Requires Special Configuration The `pixels_dice_tray` usermod requires **ESP32 BLE (Bluetooth Low Energy)** support, which is not available in all WLED build configurations. ### Why is library.json disabled? The `library.json` file has been renamed to `library.json.disabled` to prevent this usermod from being automatically included in builds that use `custom_usermods = *` (like the `usermods` environment in platformio.ini). The Tasmota Arduino ESP32 platform used by WLED does not include Arduino BLE library by default, which causes compilation failures when this usermod is auto-included. ### How to Use This Usermod This usermod **requires a custom build configuration**. You cannot simply enable it with `custom_usermods = *`. 1. **Copy the sample configuration:** ```bash cp platformio_override.ini.sample ../../../platformio_override.ini ``` 2. **Edit `platformio_override.ini`** to match your ESP32 board configuration 3. **Build with the custom environment:** ```bash pio run -e t_qt_pro_8MB_dice # or pio run -e esp32s3dev_8MB_qspi_dice ``` ### Platform Requirements - ESP32-S3 or compatible ESP32 board with BLE support - Custom platformio environment (see `platformio_override.ini.sample`) - Cannot be used with ESP8266 or ESP32-S2 ### Re-enabling for Custom Builds If you want to use this usermod in a custom build: 1. Rename `library.json.disabled` back to `library.json` 2. Manually add it to your custom environment's `custom_usermods` list 3. Ensure your platform includes BLE support ### References - See `README.md` for full usermod documentation - See `platformio_override.ini.sample` for build configuration examples ================================================ FILE: usermods/pixels_dice_tray/README.md ================================================ # A mod for using Pixel Dice with ESP32S3 boards A usermod to connect to and handle rolls from [Pixels Dice](https://gamewithpixels.com/). WLED acts as both an display controller, and a gateway to connect the die to the Wifi network. High level features: * Several LED effects that respond to die rolls * Effect color and parameters can be modified like any other effect * Different die can be set to control different segments * An optional GUI on a TFT screen with custom button controls * Gives die connection and roll status * Can do basic LED effect controls * Can display custom info for different roll types (ie. RPG stats/spell info) * Publish MQTT events from die rolls * Also report the selected roll type * Control settings through the WLED web See for a write up of the design process of the hardware and software I used this with. I also set up a custom web installer for the usermod at for 8MB ESP32-S3 boards. ## Table of Contents * [Demos](#demos) + [TFT GUI](#tft-gui) + [Multiple Die Controlling Different Segments](#multiple-die-controlling-different-segments) * [Hardware](#hardware) * [Library used](#library-used) * [Compiling](#compiling) + [platformio_override.ini](#platformio_overrideini) + [Manual platformio.ini changes](#manual-platformioini-changes) * [Configuration](#configuration) + [Controlling Dice Connections](#controlling-dice-connections) + [Controlling Effects](#controlling-effects) - [DieSimple](#diesimple) - [DiePulse](#diepulse) - [DieCheck](#diecheck) * [TFT GUI](#tft-gui-1) + [Status](#status) + [Effect Menu](#effect-menu) + [Roll Info](#roll-info) * [MQTT](#mqtt) * [Potential Modifications and Additional Features](#potential-modifications-and-additional-features) * [ESP32 Issues](#esp32-issues) ## Demos ### TFT GUI [![Watch the video](https://img.youtube.com/vi/VNsHq1TbiW8/0.jpg)](https://youtu.be/VNsHq1TbiW8) ### Multiple Die Controlling Different Segments [![Watch the video](https://img.youtube.com/vi/oCDr44C-qwM/0.jpg)](https://youtu.be/oCDr44C-qwM) ## Hardware The main purpose of this mod is to support [Pixels Dice](https://gamewithpixels.com/). The board acts as a BLE central for the dice acting as peripherals. While any ESP32 variant with BLE capabilities should be able to support this usermod, in practice I found that the original ESP32 did not work. See [ESP32 Issues](#esp32-issues) for a deeper dive. The only other ESP32 variant I tested was the ESP32-S3, which worked without issue. While there's still concern over the contention between BLE and WiFi for the radio, I haven't noticed any performance impact in practice. The only special behavior that was needed was setting `noWifiSleep = false;` to allow the OS to sleep the WiFi when the BLE is active. In addition, the BLE stack requires a lot of flash. This build takes 1.9MB with the TFT code, or 1.85MB without it. This makes it too big to fit in the `tools/WLED_ESP32_4MB_256KB_FS.csv` partition layout, and I needed to make a `WLED_ESP32_4MB_64KB_FS.csv` to even fit on 4MB devices. This only has 64KB of file system space, which is functional, but users with more than a handful of presets would run into problems with 64KB only. This means that while 4MB can be supported, larger flash sizes are needed for full functionality. The basic build of this usermod doesn't require any special hardware. However, the LCD status GUI was specifically designed for the [LILYGO T-QT Pro](https://www.lilygo.cc/products/t-qt-pro). It should be relatively easy to support other displays, though the positioning of the text may need to be adjusted. ## Library used [axlan/pixels-dice-interface](https://github.com/axlan/arduino-pixels-dice) Optional: [Bodmer/TFT_eSPI](https://github.com/Bodmer/TFT_eSPI) ## Compiling ### platformio_override.ini Copy and update the example `platformio_override.ini.sample` to the root directory of your particular build (renaming it `platformio_override.ini`). This file should be placed in the same directory as `platformio.ini`. This file is set up for the [LILYGO T-QT Pro](https://www.lilygo.cc/products/t-qt-pro). Specifically, the 8MB flash version. See the next section for notes on setting the build flags. For other boards, you may want to use a different environment as the basis. ### Manual platformio.ini changes Using the `platformio_override.ini.sample` as a reference, you'll need to update the `build_flags` and `lib_deps` of the target you're building for. If you don't need the TFT GUI, you just need to add ```ini ... build_flags = ... -D USERMOD_PIXELS_DICE_TRAY ;; Enables this UserMod lib_deps = ... ESP32 BLE Arduino axlan/pixels-dice-interface @ 1.2.0 ... ``` For the TFT support you'll need to add `Bodmer/TFT_eSPI` to `lib_deps`, and all of the required TFT parameters to `build_flags` (see `platformio_override.ini.sample`). Save the `platformio.ini` file, and perform the desired build. ## Configuration In addition to configuring which dice to connect to, this mod uses a lot of the built in WLED features: * The LED segments, effects, and customization parameters * The buttons for the UI * The MQTT settings for reporting the dice rolls ### Controlling Dice Connections **NOTE:** To configure the die itself (set its name, the die LEDs, etc.), you still need to use the Pixels Dice phone App. The usermods settings page has the configuration for controlling the dice and the display: * Ble Scan Duration - The time to look for BLE broadcasts before taking a break * Rotation - If display used, set this parameter to rotate the display. The main setting here though are the Die 0 and 1 settings. A slot is disabled if it's left blank. Putting the name of a die will make that slot only connect to die with that name. Alteratively, if the name is set to `*` the slot will use the first unassociated die it sees. Saving the configuration while a wildcard slot is connected to a die will replace the `*` with that die's name. **NOTE:** The slot a die is in is important since that's how they're identified for controlling LED effects. Effects can be set to respond to die 0, 1, or any. The configuration also includes the pins configured in the TFT build flags. These are just so the UI recognizes that these pins are being used. The [Bodmer/TFT_eSPI](https://github.com/Bodmer/TFT_eSPI) requires that these are set at build time and changing these values is ignored. ### Controlling Effects The die effects for rolls take advantage of most of the normal WLED effect features: . If you have different segments, they can have different effects driven by the same die, or different dice. #### DieSimple Turn off LEDs while rolling, than light up solid LEDs in proportion to die roll. * Color 1 - Selects the "good" color that increases based on the die roll * Color 2 - Selects the "background" color for the rest of the segment * Custom 1 - Sets which die should control this effect. If the value is greater then 1, it will respond to both dice. #### DiePulse Play `breath` effect while rolling, than apply `blend` effect in proportion to die roll. * Color 1 - See `breath` and `blend` * Color 2 - Selects the "background" color for the rest of the segment * Palette - See `breath` and `blend` * Custom 1 - Sets which die should control this effect. If the value is greater then 1, it will respond to both dice. #### DieCheck Play `running` effect while rolling, than apply `glitter` effect if roll passes threshold, or `gravcenter` if roll is below. * Color 1 - See `glitter` and `gravcenter`, used as first color for `running` * Color 2 - See `glitter` and `gravcenter` * Color 3 - Used as second color for `running` * Palette - See `glitter` and `gravcenter` * Custom 1 - Sets which die should control this effect. If the value is greater then 1, it will respond to both dice. * Custom 2 - Sets the threshold for success animation. For example if 10, success plays on rolls of 10 or above. ## TFT GUI The optional TFT GUI currently supports 3 "screens": 1. Status 2. Effect Control 3. Roll Info Double pressing the right button goes forward through the screens, and double pressing left goes back (with rollover). ### Status Status Menu Shows the status of each die slot (0 on top and 1 on the bottom). If a die is connected, its roll stats and battery status are shown. The rolls will continue to be tracked even when viewing other screens. Long press either button to clear the roll stats. ### Effect Menu Effect Menu Allows limited customization of the die effect for the currently selected LED segment. The left button moves the cursor (blue box) up and down the options for the current field. The right button updates the value for the field. The first field is the effect. Updating it will switch between the die effects. The DieCheck effect has an additional field "PASS". Pressing the right button on this field will copy the current face up value from the most recently rolled die. Long pressing either value will set the effect parameters (color, palette, controlling dice, etc.) to a default set of values. ### Roll Info Roll Info Menu Sets the "roll type" reported by MQTT events and can show additional info. Pressing the right button goes forward through the rolls, and double pressing left goes back (with rollover). The names and info for the rolls are generated from the `usermods/pixels_dice_tray/generate_roll_info.py` script. It updates `usermods/pixels_dice_tray/roll_info.h` with code generated from a simple markdown language. ## MQTT See for general MQTT configuration for WLED. The usermod produces two types of events * `$mqttDeviceTopic/dice/roll` - JSON that reports each die roll event with the following keys. - name - The name of the die that triggered the event - state - Integer indicating the die state `[UNKNOWN = 0, ON_FACE = 1, HANDLING = 2, ROLLING = 3, CROOKED = 4]` - val - The value on the die's face. For d20 1-20 - time - The uptime timestamp the roll was received in milliseconds. * `$mqttDeviceTopic/dice/roll_label` - A string that indicates the roll type selected in the [Roll Info](#roll-info) TFT menu. Where `$mqttDeviceTopic` is the topic set in the WLED MQTT configuration. Events can be logged to a CSV file using the script `usermods/pixels_dice_tray/mqtt_client/mqtt_logger.py`. These can then be used to generate interactive HTML plots with `usermods/pixels_dice_tray/mqtt_client/mqtt_plotter.py`. Roll Plot ## Potential Modifications and Additional Features This usermod is in support of a particular dice box project, but it would be fairly straightforward to extend for other applications. * Add more dice - There's no reason that several more dice slots couldn't be allowed. In addition LED effects that use multiple dice could be added (e.g. a contested roll). * Better support for die other then d20's. There's a few places where I assume the die is a d20. It wouldn't be that hard to support arbitrary die sizes. * TFT Menu - The menu system is pretty extensible. I put together some basic things I found useful, and was mainly limited by the screen size. * Die controlled UI - I originally planned to make an alternative UI that used the die directly. You'd press a button, and the current face up on the die would trigger an action. This was an interesting idea, but didn't seem to practical since I could more flexibly reproduce this by responding to the dice MQTT events. ## ESP32 Issues I really wanted to have this work on the original ESP32 boards to lower the barrier to entry, but there were several issues. First there are the issues with the partition sizes for 4MB mentioned in the [Hardware](#hardware) section. The bigger issue is that the build consistently crashes if the BLE scan task starts up. It's a bit unclear to me exactly what is failing since the backtrace is showing an exception in `new[]` memory allocation in the UDP stack. There appears to be a ton of heap available, so my guess is that this is a synchronization issue of some sort from the tasks running in parallel. I tried messing with the task core affinity a bit but didn't make much progress. It's not really clear what difference between the ESP32S3 and ESP32 would cause this difference. At the end of the day, its generally not advised to run the BLE and Wifi at the same time anyway (though it appears to work without issue on the ESP32S3). Probably the best path forward would be to switch between them. This would actually not be too much of an issue, since discovering and getting data from the die should be possible to do in bursts (at least in theory). ================================================ FILE: usermods/pixels_dice_tray/WLED_ESP32_4MB_64KB_FS.csv ================================================ # Name, Type, SubType, Offset, Size, Flags nvs, data, nvs, 0x9000, 0x5000, otadata, data, ota, 0xe000, 0x2000, app0, app, ota_0, 0x10000, 0x1F0000, app1, app, ota_1, 0x200000,0x1F0000, spiffs, data, spiffs, 0x3F0000,0x10000, ================================================ FILE: usermods/pixels_dice_tray/dice_state.h ================================================ /** * Structs for passing around usermod state */ #pragma once #include // https://github.com/axlan/arduino-pixels-dice /** * Here's how the rolls are tracked in this usermod. * 1. The arduino-pixels-dice library reports rolls and state mapped to * PixelsDieID. * 2. The "configured_die_names" sets which die to connect to and their order. * 3. The rest of the usermod references the die by this order (ie. the LED * effect is triggered for rolls for die 0). */ static constexpr size_t MAX_NUM_DICE = 2; static constexpr uint8_t INVALID_ROLL_VALUE = 0xFF; /** * The state of the connected die, and new events since the last update. */ struct DiceUpdate { // The vectors to hold results queried from the library // Since vectors allocate data, it's more efficient to keep reusing an instance // instead of declaring them on the stack. std::vector dice_list; pixels::RollUpdates roll_updates; pixels::BatteryUpdates battery_updates; // The PixelsDieID for each dice index. 0 if the die isn't connected. // The ordering here matches configured_die_names. std::array connected_die_ids{0, 0}; }; struct DiceSettings { // The mapping of dice names, to the index of die used for effects (ie. The // die named "Cat" is die 0). BLE discovery will stop when all the dice are // found. The die slot is disabled if the name is empty. If the name is "*", // the slot will use the first unassociated die it sees. std::array configured_die_names{"*", "*"}; // A label set to describe the next die roll. Index into GetRollName(). uint8_t roll_label = INVALID_ROLL_VALUE; }; // These are updated in the main loop, but accessed by the effect functions as // well. My understand is that both of these accesses should be running on the // same "thread/task" since WLED doesn't directly create additional threads. The // exception would be network callbacks and interrupts, but I don't believe // these accesses are triggered by those. If synchronization was needed, I could // look at the example in `requestJSONBufferLock()`. std::array last_die_events; static pixels::RollEvent GetLastRoll() { pixels::RollEvent last_roll; for (const auto& event : last_die_events) { if (event.timestamp > last_roll.timestamp) { last_roll = event; } } return last_roll; } /** * Returns true if the container has an item that matches the value. */ template static bool Contains(const C& container, T value) { return std::find(container.begin(), container.end(), value) != container.end(); } // These aren't known until runtime since they're being added dynamically. static uint8_t FX_MODE_SIMPLE_D20 = 0xFF; static uint8_t FX_MODE_PULSE_D20 = 0xFF; static uint8_t FX_MODE_CHECK_D20 = 0xFF; std::array DIE_LED_MODES = {0xFF, 0xFF, 0xFF}; ================================================ FILE: usermods/pixels_dice_tray/generate_roll_info.py ================================================ ''' File for generating roll labels and info text for the InfoMenu. Uses a very limited markdown language for styling text. ''' import math from pathlib import Path import re from textwrap import indent # Variables for calculating values in info text CASTER_LEVEL = 9 SPELL_ABILITY_MOD = 6 BASE_ATK_BONUS = 6 SIZE_BONUS = 1 STR_BONUS = 2 DEX_BONUS = -1 # TFT library color values TFT_BLACK =0x0000 TFT_NAVY =0x000F TFT_DARKGREEN =0x03E0 TFT_DARKCYAN =0x03EF TFT_MAROON =0x7800 TFT_PURPLE =0x780F TFT_OLIVE =0x7BE0 TFT_LIGHTGREY =0xD69A TFT_DARKGREY =0x7BEF TFT_BLUE =0x001F TFT_GREEN =0x07E0 TFT_CYAN =0x07FF TFT_RED =0xF800 TFT_MAGENTA =0xF81F TFT_YELLOW =0xFFE0 TFT_WHITE =0xFFFF TFT_ORANGE =0xFDA0 TFT_GREENYELLOW =0xB7E0 TFT_PINK =0xFE19 TFT_BROWN =0x9A60 TFT_GOLD =0xFEA0 TFT_SILVER =0xC618 TFT_SKYBLUE =0x867D TFT_VIOLET =0x915C class Size: def __init__(self, w, h): self.w = w self.h = h # Font 1 6x8 # Font 2 12x16 CHAR_SIZE = { 1: Size(6, 8), 2: Size(12, 16), } SCREEN_SIZE = Size(128, 128) # Calculates distance for short range spell. def short_range() -> int: return 25 + 5 * CASTER_LEVEL # Entries in markdown language. # Parameter 0 of the tuple is the roll name # Parameter 1 of the tuple is the roll info. # The text will be shown when the roll type is selected. An error will be raised # if the text would unexpectedly goes past the end of the screen. There are a # few styling parameters that need to be on their own lines: # $COLOR - The color for the text # $SIZE - Sets the text size (see CHAR_SIZE) # $WRAP - By default text won't wrap and generate an error. This enables text wrapping. Lines will wrap mid-word. ENTRIES = [ tuple(["Barb Chain", f'''\ $COLOR({TFT_RED}) Barb Chain $COLOR({TFT_WHITE}) Atk/CMD {BASE_ATK_BONUS + SPELL_ABILITY_MOD} Range: {short_range()} $WRAP(1) $SIZE(1) Summon {1 + math.floor((CASTER_LEVEL-1)/3)} chains. Make a melee atk 1d6 or a trip CMD=AT. On a hit make Will save or shaken 1d4 rnds. ''']), tuple(["Saves", f'''\ $COLOR({TFT_GREEN}) Saves $COLOR({TFT_WHITE}) FORT 8 REFLEX 8 WILL 9 ''']), tuple(["Skill", f'''\ Skill ''']), tuple(["Attack", f'''\ Attack Melee +{BASE_ATK_BONUS + SIZE_BONUS + STR_BONUS} Range +{BASE_ATK_BONUS + SIZE_BONUS + DEX_BONUS} ''']), tuple(["Cure", f'''\ Cure Lit 1d8+{min(5, CASTER_LEVEL)} Mod 2d8+{min(10, CASTER_LEVEL)} Ser 3d8+{min(15, CASTER_LEVEL)} ''']), tuple(["Concentrate", f'''\ Concentrat +{CASTER_LEVEL + SPELL_ABILITY_MOD} $SIZE(1) Defensive 15+2*SP_LV Dmg 10+DMG+SP_LV Grapple 10+CMB+SP_LV ''']), ] RE_SIZE = re.compile(r'\$SIZE\(([0-9])\)') RE_COLOR = re.compile(r'\$COLOR\(([0-9]+)\)') RE_WRAP = re.compile(r'\$WRAP\(([0-9])\)') END_HEADER_TXT = '// GENERATED\n' def main(): roll_info_file = Path(__file__).parent / 'roll_info.h' old_contents = open(roll_info_file, 'r').read() end_header = old_contents.index(END_HEADER_TXT) with open(roll_info_file, 'w') as fd: fd.write(old_contents[:end_header+len(END_HEADER_TXT)]) for key, entry in enumerate(ENTRIES): size = 2 wrap = False y_loc = 0 results = [] for line in entry[1].splitlines(): if line.startswith('$'): m_size = RE_SIZE.match(line) m_color = RE_COLOR.match(line) m_wrap = RE_WRAP.match(line) if m_size: size = int(m_size.group(1)) results.append(f'tft.setTextSize({size});') elif m_color: results.append( f'tft.setTextColor({int(m_color.group(1))});') elif m_wrap: wrap = bool(int(m_wrap.group(1))) else: print(f'Entry {key} unknown modifier "{line}".') exit(1) else: max_chars_per_line = math.floor( SCREEN_SIZE.w / CHAR_SIZE[size].w) if len(line) > max_chars_per_line: if wrap: while len(line) > max_chars_per_line: results.append( f'tft.println("{line[:max_chars_per_line]}");') line = line[max_chars_per_line:].lstrip() y_loc += CHAR_SIZE[size].h else: print(f'Entry {key} line "{line}" too long.') exit(1) if len(line) > 0: y_loc += CHAR_SIZE[size].h results.append(f'tft.println("{line}");') if y_loc > SCREEN_SIZE.h: print( f'Entry {key} line "{line}" went past bottom of screen.') exit(1) result = indent('\n'.join(results), ' ') fd.write(f'''\ static void PrintRoll{key}() {{ {result} }} ''') results = [] for key, entry in enumerate(ENTRIES): results.append(f'''\ case {key}: return "{entry[0]}";''') cases = indent('\n'.join(results), ' ') fd.write(f'''\ static const char* GetRollName(uint8_t key) {{ switch (key) {{ {cases} }} return ""; }} ''') results = [] for key, entry in enumerate(ENTRIES): results.append(f'''\ case {key}: PrintRoll{key}(); return;''') cases = indent('\n'.join(results), ' ') fd.write(f'''\ static void PrintRollInfo(uint8_t key) {{ tft.setTextColor(TFT_WHITE); tft.setCursor(0, 0); tft.setTextSize(2); switch (key) {{ {cases} }} tft.setTextColor(TFT_RED); tft.setCursor(0, 60); tft.println("Unknown"); }} ''') fd.write(f'static constexpr size_t NUM_ROLL_INFOS = {len(ENTRIES)};\n') main() ================================================ FILE: usermods/pixels_dice_tray/led_effects.h ================================================ /** * The LED effects influenced by dice rolls. */ #pragma once #include "wled.h" #include "dice_state.h" // Reuse FX display functions. extern void mode_breath(); extern void mode_blends(); extern void mode_glitter(); extern void mode_gravcenter(); static constexpr uint8_t USER_ANY_DIE = 0xFF; /** * Two custom effect parameters are used. * c1 - Source Die. Sets which die from [0 - MAX_NUM_DICE) controls this effect. * If this is set to 0xFF, use the latest event regardless of which die it * came from. * c2 - Target Roll. Sets the "success" criteria for a roll to >= this value. */ /** * Return the last die roll based on the custom1 effect setting. */ static pixels::RollEvent GetLastRollForSegment() { // If an invalid die is selected, fallback to using the most recent roll from // any die. if (SEGMENT.custom1 >= MAX_NUM_DICE) { return GetLastRoll(); } else { return last_die_events[SEGMENT.custom1]; } } /* * Alternating pixels running function (copied static function). */ // paletteBlend: 0 - wrap when moving, 1 - always wrap, 2 - never wrap, 3 - none (undefined) #define PALETTE_SOLID_WRAP (paletteBlend == 1 || paletteBlend == 3) static void running_copy(uint32_t color1, uint32_t color2, bool theatre = false) { int width = (theatre ? 3 : 1) + (SEGMENT.intensity >> 4); // window uint32_t cycleTime = 50 + (255 - SEGMENT.speed); uint32_t it = strip.now / cycleTime; bool usePalette = color1 == SEGCOLOR(0); for (int i = 0; i < SEGLEN; i++) { uint32_t col = color2; if (usePalette) color1 = SEGMENT.color_from_palette(i, true, PALETTE_SOLID_WRAP, 0); if (theatre) { if ((i % width) == SEGENV.aux0) col = color1; } else { int pos = (i % (width<<1)); if ((pos < SEGENV.aux0-width) || ((pos >= SEGENV.aux0) && (pos < SEGENV.aux0+width))) col = color1; } SEGMENT.setPixelColor(i,col); } if (it != SEGENV.step) { SEGENV.aux0 = (SEGENV.aux0 +1) % (theatre ? width : (width<<1)); SEGENV.step = it; } } static void simple_roll() { auto roll = GetLastRollForSegment(); if (roll.state != pixels::RollState::ON_FACE) { SEGMENT.fill(0); } else { uint16_t num_segments = float(roll.current_face + 1) / 20.0 * SEGLEN; for (int i = 0; i <= num_segments; i++) { SEGMENT.setPixelColor(i, SEGCOLOR(0)); } for (int i = num_segments; i < SEGLEN; i++) { SEGMENT.setPixelColor(i, SEGCOLOR(1)); } } } // See https://kno.wled.ge/interfaces/json-api/#effect-metadata // Name - DieSimple // Parameters - // * Selected Die (custom1) // Colors - Uses color1 and color2 // Palette - Not used // Flags - Effect is optimized for use on 1D LED strips. // Defaults - Selected Die set to 0xFF (USER_ANY_DIE) static const char _data_FX_MODE_SIMPLE_DIE[] PROGMEM = "DieSimple@,,Selected Die;!,!;;1;c1=255"; static void pulse_roll() { auto roll = GetLastRollForSegment(); if (roll.state != pixels::RollState::ON_FACE) { mode_breath(); return; } else { mode_blends(); uint16_t num_segments = float(roll.current_face + 1) / 20.0 * SEGLEN; for (int i = num_segments; i < SEGLEN; i++) { SEGMENT.setPixelColor(i, SEGCOLOR(1)); } } } static const char _data_FX_MODE_PULSE_DIE[] PROGMEM = "DiePulse@!,!,Selected Die;!,!;!;1;sx=24,pal=50,c1=255"; static void check_roll() { auto roll = GetLastRollForSegment(); if (roll.state != pixels::RollState::ON_FACE) { running_copy(SEGCOLOR(0), SEGCOLOR(2)); return; } else { if (roll.current_face + 1 >= SEGMENT.custom2) { mode_glitter(); return; } else { mode_gravcenter(); return; } } } static const char _data_FX_MODE_CHECK_DIE[] PROGMEM = "DieCheck@!,!,Selected Die,Target Roll;1,2,3;!;1;pal=0,ix=128,m12=2,si=0,c1=255,c2=10"; ================================================ FILE: usermods/pixels_dice_tray/mqtt_client/mqtt_logger.py ================================================ #!/usr/bin/env python import argparse import json import os from pathlib import Path import time # Dependency installed with `pip install paho-mqtt`. # https://pypi.org/project/paho-mqtt/ import paho.mqtt.client as mqtt state = {"label": "None"} # Define MQTT callbacks def on_connect(client, userdata, connect_flags, reason_code, properties): print("Connected with result code " + str(reason_code)) state["start_time"] = None client.subscribe(f"{state['root_topic']}#") def on_message(client, userdata, msg): if msg.topic.endswith("roll_label"): state["label"] = msg.payload.decode("ascii") print(f"Label set to {state['label']}") elif msg.topic.endswith("roll"): json_str = msg.payload.decode("ascii") msg_data = json.loads(json_str) # Convert the relative timestamps reported to the dice to an approximate absolute time. # The "last_time" check is to detect if the ESP32 was restarted or the counter rolled over. if state["start_time"] is None or msg_data["time"] < state["last_time"]: state["start_time"] = time.time() - (msg_data["time"] / 1000.0) state["last_time"] = msg_data["time"] timestamp = state["start_time"] + (msg_data["time"] / 1000.0) state["csv_fd"].write( f"{timestamp:.3f}, {msg_data['name']}, {state['label']}, {msg_data['state']}, {msg_data['val']}\n" ) state["csv_fd"].flush() if msg_data["state"] == 1: print( f"{timestamp:.3f}: {msg_data['name']} rolled {msg_data['val']}") def main(): parser = argparse.ArgumentParser( description="Log die rolls from WLED MQTT events to CSV.") # IP address (with a default value) parser.add_argument( "--host", type=str, default="127.0.0.1", help="Host address of broker (default: 127.0.0.1)", ) parser.add_argument( "--port", type=int, default=1883, help="Broker TCP port (default: 1883)" ) parser.add_argument("--user", type=str, help="Optional MQTT username") parser.add_argument("--password", type=str, help="Optional MQTT password") parser.add_argument( "--topic", type=str, help="Optional MQTT topic to listen to. For example if topic is 'wled/e5a658/dice/', subscript to to 'wled/e5a658/dice/#'. By default, listen to all topics looking for ones that end in 'roll_label' and 'roll'.", ) parser.add_argument( "-o", "--output-dir", type=Path, default=Path(__file__).absolute().parent / "logs", help="Directory to log to", ) args = parser.parse_args() timestr = time.strftime("%Y-%m-%d") os.makedirs(args.output_dir, exist_ok=True) state["csv_fd"] = open(args.output_dir / f"roll_log_{timestr}.csv", "a") # Create `an MQTT client client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2) # Set MQTT callbacks client.on_connect = on_connect client.on_message = on_message if args.user and args.password: client.username_pw_set(args.user, args.password) state["root_topic"] = "" # Connect to the MQTT broker client.connect(args.host, args.port, 60) try: while client.loop(timeout=1.0) == mqtt.MQTT_ERR_SUCCESS: time.sleep(0.1) except KeyboardInterrupt: exit(0) print("Connection Failure") exit(1) if __name__ == "__main__": main() ================================================ FILE: usermods/pixels_dice_tray/mqtt_client/mqtt_plotter.py ================================================ import argparse from http import server import os from pathlib import Path import socketserver import pandas as pd import plotly.express as px # python -m http.server 8000 --directory /tmp/ def main(): parser = argparse.ArgumentParser( description="Generate an html plot of rolls captured by mqtt_logger.py") parser.add_argument("input_file", type=Path, help="Log file to plot") parser.add_argument( "-s", "--start-server", action="store_true", help="After generating the plot, run a webserver pointing to it", ) parser.add_argument( "-o", "--output-dir", type=Path, default=Path(__file__).absolute().parent / "logs", help="Directory to log to", ) args = parser.parse_args() df = pd.read_csv( args.input_file, names=["timestamp", "die", "label", "state", "roll"] ) df_filt = df[df["state"] == 1] time = (df_filt["timestamp"] - df_filt["timestamp"].min()) / 60 / 60 fig = px.bar( df_filt, x=time, y="roll", color="label", labels={ "x": "Game Time (min)", }, title=f"Roll Report: {args.input_file.name}", ) output_path = args.output_dir / (args.input_file.stem + ".html") fig.write_html(output_path) if args.start_server: PORT = 8000 os.chdir(args.output_dir) try: with socketserver.TCPServer( ("", PORT), server.SimpleHTTPRequestHandler ) as httpd: print( f"Serving HTTP on http://0.0.0.0:{PORT}/{output_path.name}") httpd.serve_forever() except KeyboardInterrupt: pass if __name__ == "__main__": main() ================================================ FILE: usermods/pixels_dice_tray/mqtt_client/requirements.txt ================================================ plotly-express paho-mqtt ================================================ FILE: usermods/pixels_dice_tray/pixels_dice_tray.cpp ================================================ #include // https://github.com/axlan/arduino-pixels-dice #include "wled.h" #include "dice_state.h" #include "led_effects.h" #include "tft_menu.h" // Set this parameter to rotate the display. 1-3 rotate by 90,180,270 degrees. #ifndef USERMOD_PIXELS_DICE_TRAY_ROTATION #define USERMOD_PIXELS_DICE_TRAY_ROTATION 0 #endif // How often we are redrawing screen #ifndef USERMOD_PIXELS_DICE_TRAY_REFRESH_RATE_MS #define USERMOD_PIXELS_DICE_TRAY_REFRESH_RATE_MS 200 #endif // Time with no updates before screen turns off (-1 to disable) #ifndef USERMOD_PIXELS_DICE_TRAY_TIMEOUT_MS #define USERMOD_PIXELS_DICE_TRAY_TIMEOUT_MS 5 * 60 * 1000 #endif // Duration of each search for BLE devices. #ifndef BLE_SCAN_DURATION_SEC #define BLE_SCAN_DURATION_SEC 4 #endif // Time between searches for BLE devices. #ifndef BLE_TIME_BETWEEN_SCANS_SEC #define BLE_TIME_BETWEEN_SCANS_SEC 5 #endif #define WLED_DEBOUNCE_THRESHOLD \ 50 // only consider button input of at least 50ms as valid (debouncing) #define WLED_LONG_PRESS \ 600 // long press if button is released after held for at least 600ms #define WLED_DOUBLE_PRESS \ 350 // double press if another press within 350ms after a short press class PixelsDiceTrayUsermod : public Usermod { private: bool enabled = true; DiceUpdate dice_update; // Settings uint32_t ble_scan_duration_sec = BLE_SCAN_DURATION_SEC; unsigned rotation = USERMOD_PIXELS_DICE_TRAY_ROTATION; DiceSettings dice_settings; #if USING_TFT_DISPLAY MenuController menu_ctrl; #endif static void center(String& line, uint8_t width) { int len = line.length(); if (len < width) for (byte i = (width - len) / 2; i > 0; i--) line = ' ' + line; for (byte i = line.length(); i < width; i++) line += ' '; } // NOTE: THIS MOD DOES NOT SUPPORT CHANGING THE SPI PINS FROM THE UI! The // TFT_eSPI library requires that they are compiled in. static void SetSPIPinsFromMacros() { #if USING_TFT_DISPLAY spi_mosi = TFT_MOSI; // Done in TFT library. if (TFT_MISO == TFT_MOSI) { spi_miso = -1; } spi_sclk = TFT_SCLK; #endif } void UpdateDieNames( const std::array& new_die_names) { for (size_t i = 0; i < MAX_NUM_DICE; i++) { // If the saved setting was a wildcard, and that connected to a die, use // the new name instead of the wildcard. Saving this "locks" the name in. bool overriden_wildcard = new_die_names[i] == "*" && dice_update.connected_die_ids[i] != 0; if (!overriden_wildcard && new_die_names[i] != dice_settings.configured_die_names[i]) { dice_settings.configured_die_names[i] = new_die_names[i]; dice_update.connected_die_ids[i] = 0; last_die_events[i] = pixels::RollEvent(); } } } public: PixelsDiceTrayUsermod() #if USING_TFT_DISPLAY : menu_ctrl(&dice_settings) #endif { } // Functions called by WLED /* * 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() override { DEBUG_PRINTLN(F("DiceTray: init")); #if USING_TFT_DISPLAY SetSPIPinsFromMacros(); PinManagerPinType spiPins[] = { {spi_mosi, true}, {spi_miso, false}, {spi_sclk, true}}; if (!PinManager::allocateMultiplePins(spiPins, 3, PinOwner::HW_SPI)) { enabled = false; } else { PinManagerPinType displayPins[] = { {TFT_CS, true}, {TFT_DC, true}, {TFT_RST, true}, {TFT_BL, true}}; if (!PinManager::allocateMultiplePins( displayPins, sizeof(displayPins) / sizeof(PinManagerPinType), PinOwner::UM_FourLineDisplay)) { PinManager::deallocateMultiplePins(spiPins, 3, PinOwner::HW_SPI); enabled = false; } } if (!enabled) { DEBUG_PRINTLN(F("DiceTray: TFT Display pin allocations failed.")); return; } #endif // Need to enable WiFi sleep: // "E (1513) wifi:Error! Should enable WiFi modem sleep when both WiFi and Bluetooth are enabled!!!!!!" noWifiSleep = false; // Get the mode indexes that the effects are registered to. FX_MODE_SIMPLE_D20 = strip.addEffect(255, &simple_roll, _data_FX_MODE_SIMPLE_DIE); FX_MODE_PULSE_D20 = strip.addEffect(255, &pulse_roll, _data_FX_MODE_PULSE_DIE); FX_MODE_CHECK_D20 = strip.addEffect(255, &check_roll, _data_FX_MODE_CHECK_DIE); DIE_LED_MODES = {FX_MODE_SIMPLE_D20, FX_MODE_PULSE_D20, FX_MODE_CHECK_D20}; // Start a background task scanning for dice. // On completion the discovered dice are connected to. pixels::ScanForDice(ble_scan_duration_sec, BLE_TIME_BETWEEN_SCANS_SEC); #if USING_TFT_DISPLAY menu_ctrl.Init(rotation); #endif } /* * connected() is called every time the WiFi is (re)connected * Use it to initialize network interfaces */ void connected() override { // 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() override { static long last_loop_time = 0; static long last_die_connected_time = millis(); char mqtt_topic_buffer[MQTT_MAX_TOPIC_LEN + 16]; char mqtt_data_buffer[128]; // Check if we time interval for redrawing passes. if (millis() - last_loop_time < USERMOD_PIXELS_DICE_TRAY_REFRESH_RATE_MS) { return; } last_loop_time = millis(); // Update dice_list with the connected dice pixels::ListDice(dice_update.dice_list); // Get all the roll/battery updates since the last loop pixels::GetDieRollUpdates(dice_update.roll_updates); pixels::GetDieBatteryUpdates(dice_update.battery_updates); // Go through list of connected die. // TODO: Blacklist die that are connected to, but don't match the configured // names. std::array die_connected = {false, false}; for (auto die_id : dice_update.dice_list) { bool matched = false; // First check if we've already matched this ID to a connected die. for (size_t i = 0; i < MAX_NUM_DICE; i++) { if (die_id == dice_update.connected_die_ids[i]) { die_connected[i] = true; matched = true; break; } } // If this isn't already matched, check if its name matches an expected name. if (!matched) { auto die_name = pixels::GetDieDescription(die_id).name; for (size_t i = 0; i < MAX_NUM_DICE; i++) { if (0 == dice_update.connected_die_ids[i] && die_name == dice_settings.configured_die_names[i]) { dice_update.connected_die_ids[i] = die_id; die_connected[i] = true; matched = true; DEBUG_PRINTF_P(PSTR("DiceTray: %u (%s) connected.\n"), i, die_name.c_str()); break; } } // If it doesn't match any expected names, check if there's any wildcards to match. if (!matched) { auto description = pixels::GetDieDescription(die_id); for (size_t i = 0; i < MAX_NUM_DICE; i++) { if (dice_settings.configured_die_names[i] == "*") { dice_update.connected_die_ids[i] = die_id; die_connected[i] = true; dice_settings.configured_die_names[i] = die_name; DEBUG_PRINTF_P(PSTR("DiceTray: %u (%s) connected as wildcard.\n"), i, die_name.c_str()); break; } } } } } // Clear connected die that aren't still present. bool all_found = true; bool none_found = true; for (size_t i = 0; i < MAX_NUM_DICE; i++) { if (!die_connected[i]) { if (dice_update.connected_die_ids[i] != 0) { dice_update.connected_die_ids[i] = 0; last_die_events[i] = pixels::RollEvent(); DEBUG_PRINTF_P(PSTR("DiceTray: %u disconnected.\n"), i); } if (!dice_settings.configured_die_names[i].empty()) { all_found = false; } } else { none_found = false; } } // Update last_die_events for (const auto& roll : dice_update.roll_updates) { for (size_t i = 0; i < MAX_NUM_DICE; i++) { if (dice_update.connected_die_ids[i] == roll.first) { last_die_events[i] = roll.second; } } if (WLED_MQTT_CONNECTED) { snprintf(mqtt_topic_buffer, sizeof(mqtt_topic_buffer), PSTR("%s/%s"), mqttDeviceTopic, "dice/roll"); const char* name = pixels::GetDieDescription(roll.first).name.c_str(); snprintf(mqtt_data_buffer, sizeof(mqtt_data_buffer), "{\"name\":\"%s\",\"state\":%d,\"val\":%d,\"time\":%d}", name, int(roll.second.state), roll.second.current_face + 1, roll.second.timestamp); mqtt->publish(mqtt_topic_buffer, 0, false, mqtt_data_buffer); } } #if USERMOD_PIXELS_DICE_TRAY_TIMEOUT_MS > 0 && USING_TFT_DISPLAY // If at least one die is configured, but none are found if (none_found) { if (millis() - last_die_connected_time > USERMOD_PIXELS_DICE_TRAY_TIMEOUT_MS) { // Turn off LEDs and backlight and go to sleep. // Since none of the wake up pins are wired up, expect to sleep // until power cycle or reset, so don't need to handle normal // wakeup. bri = 0; applyFinalBri(); menu_ctrl.EnableBacklight(false); gpio_hold_en((gpio_num_t)TFT_BL); gpio_deep_sleep_hold_en(); esp_deep_sleep_start(); } } else { last_die_connected_time = millis(); } #endif if (pixels::IsScanning() && all_found) { DEBUG_PRINTF_P(PSTR("DiceTray: All dice found. Stopping search.\n")); pixels::StopScanning(); } else if (!pixels::IsScanning() && !all_found) { DEBUG_PRINTF_P(PSTR("DiceTray: Resuming dice search.\n")); pixels::ScanForDice(ble_scan_duration_sec, BLE_TIME_BETWEEN_SCANS_SEC); } #if USING_TFT_DISPLAY menu_ctrl.Update(dice_update); #endif } /* * 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) override { JsonObject user = root["u"]; if (user.isNull()) user = root.createNestedObject("u"); JsonArray lightArr = user.createNestedArray("DiceTray"); // name lightArr.add(enabled ? F("installed") : F("disabled")); // 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) override { // 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) override { // 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!")); } /* * 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) override { JsonObject top = root.createNestedObject("DiceTray"); top["ble_scan_duration"] = ble_scan_duration_sec; top["die_0"] = dice_settings.configured_die_names[0]; top["die_1"] = dice_settings.configured_die_names[1]; #if USING_TFT_DISPLAY top["rotation"] = rotation; JsonArray pins = top.createNestedArray("pin"); pins.add(TFT_CS); pins.add(TFT_DC); pins.add(TFT_RST); pins.add(TFT_BL); #endif } void appendConfigData() override { // Slightly annoying that you can't put text before an element. // The an item on the usermod config page has the following HTML: // ```html // Die 0 // // // ``` // addInfo let's you add data before or after the two input fields. // // To work around this, add info text to the end of the preceding item. // // See addInfo in wled00/data/settings_um.htm for details on what this function does. oappend(F( "addInfo('DiceTray:ble_scan_duration',1,'

Set to \"*\" to " "connect to any die.
Leave Blank to disable.

Saving will replace \"*\" with die names.','');")); #if USING_TFT_DISPLAY oappend(F("ddr=addDropdown('DiceTray','rotation');")); oappend(F("addOption(ddr,'0 deg',0);")); oappend(F("addOption(ddr,'90 deg',1);")); oappend(F("addOption(ddr,'180 deg',2);")); oappend(F("addOption(ddr,'270 deg',3);")); oappend(F( "addInfo('DiceTray:rotation',1,'
DO NOT CHANGE " "SPI PINS.
CHANGES ARE IGNORED.','');")); oappend(F("addInfo('TFT:pin[]',0,'','SPI CS');")); oappend(F("addInfo('TFT:pin[]',1,'','SPI DC');")); oappend(F("addInfo('TFT:pin[]',2,'','SPI RST');")); oappend(F("addInfo('TFT:pin[]',3,'','SPI BL');")); #endif } /* * 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 :) */ bool readFromConfig(JsonObject& root) override { // we look for JSON object: // {"DiceTray":{"rotation":0,"font_size":1}} JsonObject top = root["DiceTray"]; if (top.isNull()) { DEBUG_PRINTLN(F("DiceTray: No config found. (Using defaults.)")); return false; } if (top.containsKey("die_0") && top.containsKey("die_1")) { const std::array new_die_names{ top["die_0"], top["die_1"]}; UpdateDieNames(new_die_names); } else { DEBUG_PRINTLN(F("DiceTray: No die names found.")); } #if USING_TFT_DISPLAY unsigned new_rotation = min(top["rotation"] | rotation, 3u); // Restore the SPI pins to their compiled in defaults. SetSPIPinsFromMacros(); if (new_rotation != rotation) { rotation = new_rotation; menu_ctrl.Init(rotation); } // Update with any modified settings. menu_ctrl.Redraw(); #endif // use "return !top["newestParameter"].isNull();" when updating Usermod with // new features return !top["DiceTray"].isNull(); } /** * handleButton() can be used to override default button behaviour. Returning true * will prevent button working in a default way. * Replicating button.cpp */ #if USING_TFT_DISPLAY bool handleButton(uint8_t b) override { if (!enabled || b > 1 // buttons 0,1 only || buttons[b].type == BTN_TYPE_SWITCH || buttons[b].type == BTN_TYPE_NONE || buttons[b].type == BTN_TYPE_RESERVED || buttons[b].type == BTN_TYPE_PIR_SENSOR || buttons[b].type == BTN_TYPE_ANALOG || buttons[b].type == BTN_TYPE_ANALOG_INVERTED) { return false; } unsigned long now = millis(); static bool buttonPressedBefore[2] = {false}; static bool buttonLongPressed[2] = {false}; static unsigned long buttonPressedTime[2] = {0}; static unsigned long buttonWaitTime[2] = {0}; //momentary button logic if (!buttons[b].longPressed && isButtonPressed(b)) { //pressed if (!buttons[b].pressedBefore) { buttons[b].pressedTime = now; } buttons[b].pressedBefore = true; if (now - buttons[b].pressedTime > WLED_LONG_PRESS) { //long press menu_ctrl.HandleButton(ButtonType::LONG, b); buttons[b].longPressed = true; return true; } } else if (!isButtonPressed(b) && buttons[b].pressedBefore) { //released long dur = now - buttons[b].pressedTime; if (dur < WLED_DEBOUNCE_THRESHOLD) { buttons[b].pressedBefore = false; return true; } //too short "press", debounce bool doublePress = buttons[b].waitTime; //did we have short press before? buttons[b].waitTime = 0; if (!buttons[b].longPressed) { //short press // if this is second release within 350ms it is a double press (buttonWaitTime!=0) if (doublePress) { menu_ctrl.HandleButton(ButtonType::DOUBLE, b); } else { buttons[b].waitTime = now; } } buttons[b].pressedBefore = false; buttons[b].longPressed = false; } // if 350ms elapsed since last press/release it is a short press if (buttons[b].waitTime && now - buttons[b].waitTime > WLED_DOUBLE_PRESS && !buttons[b].pressedBefore) { buttons[b].waitTime = 0; menu_ctrl.HandleButton(ButtonType::SINGLE, b); } return true; } #endif /* * 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_PIXELS_DICE_TRAY; } // 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! }; static PixelsDiceTrayUsermod pixels_dice_tray; REGISTER_USERMOD(pixels_dice_tray); ================================================ FILE: usermods/pixels_dice_tray/platformio_override.ini.sample ================================================ [platformio] default_envs = t_qt_pro_8MB_dice, esp32s3dev_8MB_qspi_dice # ------------------------------------------------------------------------------ # T-QT Pro 8MB with integrated 128x128 TFT screen # ------------------------------------------------------------------------------ [env:t_qt_pro_8MB_dice] board = esp32-s3-devkitc-1 ;; generic dev board; platform = ${esp32s3.platform} upload_speed = 921600 build_unflags = ${common.build_unflags} board_build.partitions = ${esp32.large_partitions} board_build.f_flash = 80000000L board_build.flash_mode = qio monitor_filters = esp32_exception_decoder build_flags = ${common.build_flags} ${esp32s3.build_flags} -D WLED_RELEASE_NAME=T-QT-PRO-8MB -D CONFIG_LITTLEFS_FOR_IDF_3_2 -D WLED_WATCHDOG_TIMEOUT=0 -D ARDUINO_USB_CDC_ON_BOOT=1 -D ARDUINO_USB_MODE=1 ;; for boards with USB-OTG connector only (USBCDC or "TinyUSB") -D USERMOD_PIXELS_DICE_TRAY ;; Enables this UserMod -D USERMOD_PIXELS_DICE_TRAY_BL_ACTIVE_LOW=1 -D USERMOD_PIXELS_DICE_TRAY_ROTATION=2 ;-D WLED_DEBUG ;;;;;;;;;;;;;;;;;; TFT_eSPI Settings ;;;;;;;;;;;;;;;;;;;;;;;; ;-DCORE_DEBUG_LEVEL=ARDUHAL_LOG_LEVEL_DEBUG -D USER_SETUP_LOADED=1 ; Define the TFT driver, pins etc. from: https://github.com/Bodmer/TFT_eSPI/blob/master/User_Setups/Setup211_LilyGo_T_QT_Pro_S3.h ; GC9A01 128 x 128 display with no chip select line -D USER_SETUP_ID=211 -D GC9A01_DRIVER=1 -D TFT_WIDTH=128 -D TFT_HEIGHT=128 -D TFT_BACKLIGHT_ON=0 -D TFT_ROTATION=3 -D CGRAM_OFFSET=1 -D TFT_MISO=-1 -D TFT_MOSI=2 -D TFT_SCLK=3 -D TFT_CS=5 -D TFT_DC=6 -D TFT_RST=1 -D TFT_BL=10 -D LOAD_GLCD=1 -D LOAD_FONT2=1 -D LOAD_FONT4=1 -D LOAD_FONT6=1 -D LOAD_FONT7=1 -D LOAD_FONT8=1 -D LOAD_GFXFF=1 ; Avoid SPIFFS dependancy that was causing compile issues. ;-D SMOOTH_FONT=1 -D SPI_FREQUENCY=40000000 -D SPI_READ_FREQUENCY=20000000 -D SPI_TOUCH_FREQUENCY=2500000 lib_deps = ${esp32s3.lib_deps} ${esp32.AR_lib_deps} ESP32 BLE Arduino bodmer/TFT_eSPI @ 2.5.43 axlan/pixels-dice-interface @ 1.2.0 # ------------------------------------------------------------------------------ # ESP32S3 dev board with 8MB flash and no extended RAM. # ------------------------------------------------------------------------------ [env:esp32s3dev_8MB_qspi_dice] board = esp32-s3-devkitc-1 ;; generic dev board; platform = ${esp32s3.platform} upload_speed = 921600 build_unflags = ${common.build_unflags} board_build.partitions = ${esp32.large_partitions} board_build.f_flash = 80000000L board_build.flash_mode = qio monitor_filters = esp32_exception_decoder build_flags = ${common.build_flags} ${esp32s3.build_flags} -D WLED_RELEASE_NAME=ESP32-S3_8MB_qspi -D CONFIG_LITTLEFS_FOR_IDF_3_2 -D WLED_WATCHDOG_TIMEOUT=0 -D ARDUINO_USB_CDC_ON_BOOT=1 -D ARDUINO_USB_MODE=1 ;; for boards with USB-OTG connector only (USBCDC or "TinyUSB") -D USERMOD_PIXELS_DICE_TRAY ;; Enables this UserMod ;-D WLED_DEBUG lib_deps = ${esp32s3.lib_deps} ${esp32.AR_lib_deps} ESP32 BLE Arduino axlan/pixels-dice-interface @ 1.2.0 # ------------------------------------------------------------------------------ # ESP32 dev board without screen # ------------------------------------------------------------------------------ # THIS DOES NOT WORK!!!!!! # While it builds and programs onto the device, I ran into a series of issues # trying to actually run. # Right after the AP init there's an allocation exception which claims to be in # the UDP server. There seems to be a ton of heap remaining, so the exact error # might be a red herring. # It appears that the BLE scanning task is conflicting with the networking tasks. # I was successfully running simple applications with the pixels-dice-interface # on ESP32 dev boards, so it may be an issue with too much being scheduled in # parallel. Also not clear exactly what difference between the ESP32 and the # ESP32S3 would be causing this, though they do run different BLE versions. # May be related to some of the issues discussed in: # https://github.com/wled-dev/WLED/issues/1382 ; [env:esp32dev_dice] ; extends = env:esp32dev ; build_flags = ${common.build_flags} ${esp32.build_flags} -D WLED_RELEASE_NAME=ESP32 ; ; Enable Pixels dice mod ; -D USERMOD_PIXELS_DICE_TRAY ; lib_deps = ${esp32.lib_deps} ; ESP32 BLE Arduino ; axlan/pixels-dice-interface @ 1.2.0 ; ; Tiny file system partition, no core dump to fit BLE library. ; board_build.partitions = usermods/pixels_dice_tray/WLED_ESP32_4MB_64KB_FS.csv ================================================ FILE: usermods/pixels_dice_tray/roll_info.h ================================================ #pragma once #include extern TFT_eSPI tft; // The following functions are generated by: // usermods/pixels_dice_tray/generate_roll_info.py // GENERATED static void PrintRoll0() { tft.setTextColor(63488); tft.println("Barb Chain"); tft.setTextColor(65535); tft.println("Atk/CMD 12"); tft.println("Range: 70"); tft.setTextSize(1); tft.println("Summon 3 chains. Make"); tft.println("a melee atk 1d6 or a "); tft.println("trip CMD=AT. On a hit"); tft.println("make Will save or sha"); tft.println("ken 1d4 rnds."); } static void PrintRoll1() { tft.setTextColor(2016); tft.println("Saves"); tft.setTextColor(65535); tft.println("FORT 8"); tft.println("REFLEX 8"); tft.println("WILL 9"); } static void PrintRoll2() { tft.println("Skill"); } static void PrintRoll3() { tft.println("Attack"); tft.println("Melee +9"); tft.println("Range +6"); } static void PrintRoll4() { tft.println("Cure"); tft.println("Lit 1d8+5"); tft.println("Mod 2d8+9"); tft.println("Ser 3d8+9"); } static void PrintRoll5() { tft.println("Concentrat"); tft.println("+15"); tft.setTextSize(1); tft.println("Defensive 15+2*SP_LV"); tft.println("Dmg 10+DMG+SP_LV"); tft.println("Grapple 10+CMB+SP_LV"); } static const char* GetRollName(uint8_t key) { switch (key) { case 0: return "Barb Chain"; case 1: return "Saves"; case 2: return "Skill"; case 3: return "Attack"; case 4: return "Cure"; case 5: return "Concentrate"; } return ""; } static void PrintRollInfo(uint8_t key) { tft.setTextColor(TFT_WHITE); tft.setCursor(0, 0); tft.setTextSize(2); switch (key) { case 0: PrintRoll0(); return; case 1: PrintRoll1(); return; case 2: PrintRoll2(); return; case 3: PrintRoll3(); return; case 4: PrintRoll4(); return; case 5: PrintRoll5(); return; } tft.setTextColor(TFT_RED); tft.setCursor(0, 60); tft.println("Unknown"); } static constexpr size_t NUM_ROLL_INFOS = 6; ================================================ FILE: usermods/pixels_dice_tray/tft_menu.h ================================================ /** * Code for using the 128x128 LCD and two buttons on the T-QT Pro as a GUI. */ #pragma once #ifndef TFT_WIDTH #warning TFT parameters not specified, not using screen. #else #include #include // https://github.com/axlan/arduino-pixels-dice #include "wled.h" #include "dice_state.h" #include "roll_info.h" #define USING_TFT_DISPLAY 1 #ifndef TFT_BL #define TFT_BL -1 #endif // Bitmask for icon const uint8_t LIGHTNING_ICON_8X8[] PROGMEM = { 0b00001111, 0b00010010, 0b00100100, 0b01001111, 0b10000001, 0b11110010, 0b00010100, 0b00011000, }; TFT_eSPI tft = TFT_eSPI(TFT_WIDTH, TFT_HEIGHT); /** * Print text with box surrounding it. * * @param txt Text to draw * @param color Color for box lines */ static void PrintLnInBox(const char* txt, uint32_t color) { int16_t sx = tft.getCursorX(); int16_t sy = tft.getCursorY(); tft.setCursor(sx + 2, sy); tft.print(txt); int16_t w = tft.getCursorX() - sx + 1; tft.println(); int16_t h = tft.getCursorY() - sy - 1; tft.drawRect(sx, sy, w, h, color); } /** * Override the current colors for the selected segment to the defaults for the * selected die effect. */ void SetDefaultColors(uint8_t mode) { Segment& seg = strip.getFirstSelectedSeg(); if (mode == FX_MODE_SIMPLE_D20) { seg.setColor(0, GREEN); seg.setColor(1, 0); } else if (mode == FX_MODE_PULSE_D20) { seg.setColor(0, GREEN); seg.setColor(1, RED); } else if (mode == FX_MODE_CHECK_D20) { seg.setColor(0, RED); seg.setColor(1, 0); seg.setColor(2, GREEN); } } /** * Get the pointer to the custom2 value for the current LED segment. This is * used to set the target roll for relevant effects. */ static uint8_t* GetCurrentRollTarget() { return &strip.getFirstSelectedSeg().custom2; } /** * Class for drawing a histogram of roll results. */ class RollCountWidget { private: int16_t xs = 0; int16_t ys = 0; uint16_t border_color = TFT_RED; uint16_t bar_color = TFT_GREEN; uint16_t bar_width = 6; uint16_t max_bar_height = 60; unsigned roll_counts[20] = {0}; unsigned total = 0; unsigned max_count = 0; public: RollCountWidget(int16_t xs = 0, int16_t ys = 0, uint16_t border_color = TFT_RED, uint16_t bar_color = TFT_GREEN, uint16_t bar_width = 6, uint16_t max_bar_height = 60) : xs(xs), ys(ys), border_color(border_color), bar_color(bar_color), bar_width(bar_width), max_bar_height(max_bar_height) {} void Clear() { memset(roll_counts, 0, sizeof(roll_counts)); total = 0; max_count = 0; } unsigned GetNumRolls() const { return total; } void AddRoll(unsigned val) { if (val > 19) { return; } roll_counts[val]++; total++; max_count = max(roll_counts[val], max_count); } void Draw() { // Add 2 pixels to lengths for boarder width. tft.drawRect(xs, ys, bar_width * 20 + 2, max_bar_height + 2, border_color); for (size_t i = 0; i < 20; i++) { if (roll_counts[i] > 0) { // Scale bar by highest count. uint16_t bar_height = round(float(roll_counts[i]) / float(max_count) * float(max_bar_height)); // Add space between bars uint16_t padding = (bar_width > 1) ? 1 : 0; // Need to start from top of bar and draw down tft.fillRect(xs + 1 + bar_width * i, ys + 1 + max_bar_height - bar_height, bar_width - padding, bar_height, bar_color); } } } }; enum class ButtonType { SINGLE, DOUBLE, LONG }; // Base class for different menu pages. class MenuBase { public: /** * Handle new die events and connections. Called even when menu isn't visible. */ virtual void Update(const DiceUpdate& dice_update) = 0; /** * Draw menu to the screen. */ virtual void Draw(const DiceUpdate& dice_update, bool force_redraw) = 0; /** * Handle button presses if the menu is currently active. */ virtual void HandleButton(ButtonType type, uint8_t b) = 0; protected: static DiceSettings* settings; friend class MenuController; }; DiceSettings* MenuBase::settings = nullptr; /** * Menu to show connection status and roll histograms. */ class DiceStatusMenu : public MenuBase { public: DiceStatusMenu() : die_roll_counts{RollCountWidget{0, 20, TFT_BLUE, TFT_GREEN, 6, 40}, RollCountWidget{0, SECTION_HEIGHT + 20, TFT_BLUE, TFT_GREEN, 6, 40}} {} void Update(const DiceUpdate& dice_update) override { for (size_t i = 0; i < MAX_NUM_DICE; i++) { const auto die_id = dice_update.connected_die_ids[i]; const auto connected = die_id != 0; // Redraw if connection status changed. die_updated[i] |= die_id != last_die_ids[i]; last_die_ids[i] = die_id; if (connected) { bool charging = false; for (const auto& battery : dice_update.battery_updates) { if (battery.first == die_id) { if (die_battery[i].battery_level == INVALID_BATTERY || battery.second.is_charging != die_battery[i].is_charging) { die_updated[i] = true; } die_battery[i] = battery.second; } } for (const auto& roll : dice_update.roll_updates) { if (roll.first == die_id && roll.second.state == pixels::RollState::ON_FACE) { die_roll_counts[i].AddRoll(roll.second.current_face); die_updated[i] = true; } } } } } void Draw(const DiceUpdate& dice_update, bool force_redraw) override { // This could probably be optimized for partial redraws. for (size_t i = 0; i < MAX_NUM_DICE; i++) { const int16_t ys = SECTION_HEIGHT * i; const auto die_id = dice_update.connected_die_ids[i]; const auto connected = die_id != 0; // Screen updates might be slow, yield in case network task needs to do // work. yield(); bool battery_update = connected && (millis() - last_update[i] > BATTERY_REFRESH_RATE_MS); if (force_redraw || die_updated[i] || battery_update) { last_update[i] = millis(); tft.fillRect(0, ys, TFT_WIDTH, SECTION_HEIGHT, TFT_BLACK); tft.drawRect(0, ys, TFT_WIDTH, SECTION_HEIGHT, TFT_BLUE); if (settings->configured_die_names[i].empty()) { tft.setTextColor(TFT_RED); tft.setCursor(2, ys + 4); tft.setTextSize(2); tft.println("Connection"); tft.setCursor(2, tft.getCursorY()); tft.println("Disabled"); } else if (!connected) { tft.setTextColor(TFT_RED); tft.setCursor(2, ys + 4); tft.setTextSize(2); tft.println(settings->configured_die_names[i].c_str()); tft.setCursor(2, tft.getCursorY()); tft.print("Waiting..."); } else { tft.setTextColor(TFT_WHITE); tft.setCursor(0, ys + 2); tft.setTextSize(1); tft.println(settings->configured_die_names[i].c_str()); tft.print("Cnt "); tft.print(die_roll_counts[i].GetNumRolls()); if (die_battery[i].battery_level != INVALID_BATTERY) { tft.print(" Bat "); tft.print(die_battery[i].battery_level); tft.print("%"); if (die_battery[i].is_charging) { tft.drawBitmap(tft.getCursorX(), tft.getCursorY(), LIGHTNING_ICON_8X8, 8, 8, TFT_YELLOW); } } die_roll_counts[i].Draw(); } die_updated[i] = false; } } } void HandleButton(ButtonType type, uint8_t b) override { if (type == ButtonType::LONG) { for (size_t i = 0; i < MAX_NUM_DICE; i++) { die_roll_counts[i].Clear(); die_updated[i] = true; } } }; private: static constexpr long BATTERY_REFRESH_RATE_MS = 60 * 1000; static constexpr int16_t SECTION_HEIGHT = TFT_HEIGHT / MAX_NUM_DICE; static constexpr uint8_t INVALID_BATTERY = 0xFF; std::array last_update{0, 0}; std::array last_die_ids{0, 0}; std::array die_updated{false, false}; std::array die_battery = { pixels::BatteryEvent{INVALID_BATTERY, false}, pixels::BatteryEvent{INVALID_BATTERY, false}}; std::array die_roll_counts; }; /** * Some limited controls for setting the die effects on the current LED * segment. */ class EffectMenu : public MenuBase { public: EffectMenu() = default; void Update(const DiceUpdate& dice_update) override {} void Draw(const DiceUpdate& dice_update, bool force_redraw) override { // NOTE: This doesn't update automatically if the effect is updated on the // web UI and vice-versa. if (force_redraw) { tft.fillScreen(TFT_BLACK); uint8_t mode = strip.getFirstSelectedSeg().mode; if (Contains(DIE_LED_MODES, mode)) { char lineBuffer[CHAR_WIDTH_BIG + 1]; extractModeName(mode, JSON_mode_names, lineBuffer, CHAR_WIDTH_BIG); tft.setTextColor(TFT_WHITE); tft.setCursor(0, 0); tft.setTextSize(2); PrintLnInBox(lineBuffer, (field_idx == 0) ? TFT_BLUE : TFT_BLACK); if (mode == FX_MODE_CHECK_D20) { snprintf(lineBuffer, sizeof(lineBuffer), "PASS: %u", *GetCurrentRollTarget()); PrintLnInBox(lineBuffer, (field_idx == 1) ? TFT_BLUE : TFT_BLACK); } } else { char lineBuffer[CHAR_WIDTH_SMALL + 1]; extractModeName(mode, JSON_mode_names, lineBuffer, CHAR_WIDTH_SMALL); tft.setTextColor(TFT_WHITE); tft.setCursor(0, 0); tft.setTextSize(1); tft.println(lineBuffer); } } } /** * Button 0 navigates up and down the settings for the effect. * Button 1 changes the value for the selected settings. * Long pressing a button resets the effect parameters to their defaults for * the current die effect. */ void HandleButton(ButtonType type, uint8_t b) override { Segment& seg = strip.getFirstSelectedSeg(); auto mode_itr = std::find(DIE_LED_MODES.begin(), DIE_LED_MODES.end(), seg.mode); if (mode_itr != DIE_LED_MODES.end()) { mode_idx = mode_itr - DIE_LED_MODES.begin(); } if (mode_itr == DIE_LED_MODES.end()) { seg.setMode(DIE_LED_MODES[mode_idx]); } else { if (type == ButtonType::LONG) { // Need to set mode to different value so defaults are actually loaded. seg.setMode(0); seg.setMode(DIE_LED_MODES[mode_idx], true); SetDefaultColors(DIE_LED_MODES[mode_idx]); } else if (b == 0) { field_idx = (field_idx + 1) % DIE_LED_MODE_NUM_FIELDS[mode_idx]; } else { if (field_idx == 0) { mode_idx = (mode_idx + 1) % DIE_LED_MODES.size(); seg.setMode(DIE_LED_MODES[mode_idx]); } else if (DIE_LED_MODES[mode_idx] == FX_MODE_CHECK_D20 && field_idx == 1) { *GetCurrentRollTarget() = GetLastRoll().current_face + 1; } } } }; private: static constexpr std::array DIE_LED_MODE_NUM_FIELDS = {1, 1, 2}; static constexpr size_t CHAR_WIDTH_BIG = 10; static constexpr size_t CHAR_WIDTH_SMALL = 21; size_t mode_idx = 0; size_t field_idx = 0; }; constexpr std::array EffectMenu::DIE_LED_MODE_NUM_FIELDS; /** * Menu for setting the roll label and some info for that roll type. */ class InfoMenu : public MenuBase { public: InfoMenu() = default; void Update(const DiceUpdate& dice_update) override {} void Draw(const DiceUpdate& dice_update, bool force_redraw) override { if (force_redraw) { tft.fillScreen(TFT_BLACK); if (settings->roll_label != INVALID_ROLL_VALUE) { PrintRollInfo(settings->roll_label); } else { tft.setTextColor(TFT_RED); tft.setCursor(0, 60); tft.setTextSize(2); tft.println("Set Roll"); } } } /** * Single clicking navigates through the roll types. Button 0 goes down, and * button 1 goes up with wrapping. */ void HandleButton(ButtonType type, uint8_t b) override { if (settings->roll_label >= NUM_ROLL_INFOS) { settings->roll_label = 0; } else if (b == 0) { settings->roll_label = (settings->roll_label == 0) ? NUM_ROLL_INFOS - 1 : settings->roll_label - 1; } else if (b == 1) { settings->roll_label = (settings->roll_label + 1) % NUM_ROLL_INFOS; } if (WLED_MQTT_CONNECTED) { char mqtt_topic_buffer[MQTT_MAX_TOPIC_LEN + 16]; snprintf(mqtt_topic_buffer, sizeof(mqtt_topic_buffer), PSTR("%s/%s"), mqttDeviceTopic, "dice/settings->roll_label"); mqtt->publish(mqtt_topic_buffer, 0, false, GetRollName(settings->roll_label)); } }; }; /** * Interface for the rest of the app to update the menus. */ class MenuController { public: MenuController(DiceSettings* settings) { MenuBase::settings = settings; } void Init(unsigned rotation) { tft.init(); tft.setRotation(rotation); tft.fillScreen(TFT_BLACK); tft.setTextColor(TFT_RED); tft.setCursor(0, 60); tft.setTextDatum(MC_DATUM); tft.setTextSize(2); EnableBacklight(true); force_redraw = true; } // Set the pin to turn the backlight on or off if available. static void EnableBacklight(bool enable) { #if TFT_BL > 0 #if USERMOD_PIXELS_DICE_TRAY_BL_ACTIVE_LOW enable = !enable; #endif digitalWrite(TFT_BL, enable); #endif } /** * Double clicking navigates between menus. Button 0 goes down, and button 1 * goes up with wrapping. */ void HandleButton(ButtonType type, uint8_t b) { force_redraw = true; // Switch menus with double click if (ButtonType::DOUBLE == type) { if (b == 0) { current_index = (current_index == 0) ? menu_ptrs.size() - 1 : current_index - 1; } else { current_index = (current_index + 1) % menu_ptrs.size(); } } else { menu_ptrs[current_index]->HandleButton(type, b); } } void Update(const DiceUpdate& dice_update) { for (auto menu_ptr : menu_ptrs) { menu_ptr->Update(dice_update); } menu_ptrs[current_index]->Draw(dice_update, force_redraw); force_redraw = false; } void Redraw() { force_redraw = true; } private: size_t current_index = 0; bool force_redraw = true; DiceStatusMenu status_menu; EffectMenu effect_menu; InfoMenu info_menu; const std::array menu_ptrs = {&status_menu, &effect_menu, &info_menu}; }; #endif ================================================ FILE: usermods/platformio_override.usermods.ini ================================================ [platformio] default_envs = usermods_esp32, usermods_esp32c3, usermods_esp32s2, usermods_esp32s3 [env:usermods_esp32] extends = env:esp32dev custom_usermods = ${usermods.custom_usermods} board_build.partitions = ${esp32.extreme_partitions} ; We're gonna need a bigger boat [env:usermods_esp32c3] extends = env:esp32c3dev board = esp32-c3-devkitm-1 custom_usermods = ${usermods.custom_usermods} board_build.partitions = ${esp32.extreme_partitions} ; We're gonna need a bigger boat [env:usermods_esp32s2] extends = env:lolin_s2_mini custom_usermods = ${usermods.custom_usermods} board_build.partitions = ${esp32.extreme_partitions} ; We're gonna need a bigger boat [env:usermods_esp32s3] extends = env:esp32s3dev_16MB_opi custom_usermods = ${usermods.custom_usermods} board_build.partitions = ${esp32.extreme_partitions} ; We're gonna need a bigger boat [usermods] # Added in CI ================================================ FILE: usermods/pov_display/README.md ================================================ ## POV Display usermod This usermod adds a new effect called “POV Image”. ![the usermod at work](pov_display.gif?raw=true) ###How does it work? With proper configuration (see below) the main segment will display a single row of pixels from an image stored on the ESP. It displays the image row by row at a high refresh rate. If you move the pixel segment at the right speed, you will see the full image floating in the air thanks to the persistence of vision. RGB LEDs only (no RGBW), with grouping set to 1 and spacing set to 0. Best results with high-density strips (e.g., 144 LEDs/m). To get it working: - Resize your image. The height must match the number of LEDs in your strip/segment. - Rotate your image 90° clockwise (height becomes width). - Upload a BMP image (24-bit, uncompressed) to the ESP filesystem using the “/edit” URL. - Select the “POV Image” effect. - Set the segment name to the absolute filesystem path of the image (e.g., “/myimage.bmp”). - The path is case-sensitive and must start with “/”. - Rotate the pixel strip at approximately 20 RPM. - Tune as needed so that one full revolution maps to the image width (if the image appears stretched or compressed, adjust RPM slightly). - Enjoy the show! Notes: - Only 24-bit uncompressed BMP files are supported. - The image must fit into ~64 KB of RAM (width × height × 3 bytes, plus row padding to a 4-byte boundary). - Examples (approximate, excluding row padding): - 128×128 (49,152 bytes) fits. - 160×160 (76,800 bytes) does NOT fit. - 96×192 (55,296 bytes) fits; padding may add a small overhead. - If the rendered image appears mirrored or upside‑down, rotate 90° the other way or flip horizontally in your editor and try again. - The path must be absolute. ### Requirements - 1D rotating LED strip/segment (POV setup). Ensure the segment length equals the number of physical LEDs. - BMP image saved as 24‑bit, uncompressed (no alpha, no palette). - Sufficient free RAM (~64 KB) for the image buffer. ### Troubleshooting - Nothing displays: verify the file exists at the exact absolute path (case‑sensitive) and is a 24‑bit uncompressed BMP. - Garbled colors or wrong orientation: re‑export as 24‑bit BMP and retry the rotation/flip guidance above. - Image too large: reduce width and/or height until it fits within ~64 KB (see examples). - Path issues: confirm you uploaded the file via the “/edit” URL and can see it in the filesystem browser. ### Safety - Secure the rotating assembly and keep clear of moving parts. - Balance the strip/hub to minimize vibration before running at speed. ================================================ FILE: usermods/pov_display/bmpimage.cpp ================================================ #include "bmpimage.h" #define BUF_SIZE 64000 byte * _buffer = nullptr; uint16_t read16(File &f) { uint16_t result; f.read((uint8_t *)&result,2); return result; } uint32_t read32(File &f) { uint32_t result; f.read((uint8_t *)&result,4); return result; } bool BMPimage::init(const char * fn) { File bmpFile; int bmpDepth; //first, check if filename exists if (!WLED_FS.exists(fn)) { return false; } bmpFile = WLED_FS.open(fn); if (!bmpFile) { _valid=false; return false; } //so, the file exists and is opened // Parse BMP header uint16_t header = read16(bmpFile); if(header != 0x4D42) { // BMP signature _valid=false; bmpFile.close(); return false; } //read and ingnore file size read32(bmpFile); (void)read32(bmpFile); // Read & ignore creator bytes _imageOffset = read32(bmpFile); // Start of image data // Read DIB header read32(bmpFile); _width = read32(bmpFile); _height = read32(bmpFile); if(read16(bmpFile) != 1) { // # planes -- must be '1' _valid=false; bmpFile.close(); return false; } bmpDepth = read16(bmpFile); // bits per pixel if((bmpDepth != 24) || (read32(bmpFile) != 0)) { // 0 = uncompressed { _width=0; _valid=false; bmpFile.close(); return false; } // If _height is negative, image is in top-down order. // This is not canon but has been observed in the wild. if(_height < 0) { _height = -_height; } //now, we have successfully got all the basics // BMP rows are padded (if needed) to 4-byte boundary _rowSize = (_width * 3 + 3) & ~3; //check image size - if it is too large, it will be unusable if (_rowSize*_height>BUF_SIZE) { _valid=false; bmpFile.close(); return false; } bmpFile.close(); // Ensure filename fits our buffer (segment name length constraint). size_t len = strlen(fn); if (len > WLED_MAX_SEGNAME_LEN) { return false; } strncpy(filename, fn, sizeof(filename)); filename[sizeof(filename) - 1] = '\0'; _valid = true; return true; } void BMPimage::clear(){ strcpy(filename, ""); _width=0; _height=0; _rowSize=0; _imageOffset=0; _loaded=false; _valid=false; } bool BMPimage::load(){ const size_t size = (size_t)_rowSize * (size_t)_height; if (size > BUF_SIZE) { return false; } File bmpFile = WLED_FS.open(filename); if (!bmpFile) { return false; } if (_buffer != nullptr) free(_buffer); _buffer = (byte*)malloc(size); if (_buffer == nullptr) return false; bmpFile.seek(_imageOffset); const size_t readBytes = bmpFile.read(_buffer, size); bmpFile.close(); if (readBytes != size) { _loaded = false; return false; } _loaded = true; return true; } byte* BMPimage::line(uint16_t n){ if (_loaded) { return (_buffer+n*_rowSize); } else { return NULL; } } uint32_t BMPimage::pixelColor(uint16_t x, uint16_t y){ uint32_t pos; byte b,g,r; //colors if (! _loaded) { return 0; } if ( (x>=_width) || (y>=_height) ) { return 0; } pos=y*_rowSize + 3*x; //get colors. Note that in BMP files, they go in BGR order b= _buffer[pos++]; g= _buffer[pos++]; r= _buffer[pos]; return (r<<16|g<<8|b); } ================================================ FILE: usermods/pov_display/bmpimage.h ================================================ #ifndef _BMPIMAGE_H #define _BMPIMAGE_H #include "Arduino.h" #include "wled.h" /* * This class describes a bitmap image. Each object refers to a bmp file on * filesystem fatfs. * To initialize, call init(), passign to it name of a bitmap file * at the root of fatfs filesystem: * * BMPimage myImage; * myImage.init("logo.bmp"); * * For performance reasons, before actually usign the image, you need to load * it from filesystem to RAM: * myImage.load(); * All load() operations use the same reserved buffer in RAM, so you can only * have one file loaded at a time. Before loading a new file, always unload the * previous one: * myImage.unload(); */ class BMPimage { public: int height() {return _height; } int width() {return _width; } int rowSize() {return _rowSize;} bool isLoaded() {return _loaded; } bool load(); void unload() {_loaded=false; } byte * line(uint16_t n); uint32_t pixelColor(uint16_t x,uint16_t y); bool init(const char* fn); void clear(); char * getFilename() {return filename;}; private: char filename[WLED_MAX_SEGNAME_LEN+1]=""; int _width=0; int _height=0; int _rowSize=0; int _imageOffset=0; bool _loaded=false; bool _valid=false; }; extern byte * _buffer; #endif ================================================ FILE: usermods/pov_display/library.json ================================================ { "name:": "pov_display", "build": { "libArchive": false}, "platforms": ["espressif32"] } ================================================ FILE: usermods/pov_display/pov.cpp ================================================ #include "pov.h" POV::POV() {} void POV::showLine(const byte * line, uint16_t size){ uint16_t i, pos; uint8_t r, g, b; if (!line) { // All-black frame on null input for (i = 0; i < SEGLEN; i++) { SEGMENT.setPixelColor(i, CRGB::Black); } strip.show(); lastLineUpdate = micros(); return; } for (i = 0; i < SEGLEN; i++) { if (i < size) { pos = 3 * i; // using bgr order b = line[pos++]; g = line[pos++]; r = line[pos]; SEGMENT.setPixelColor(i, CRGB(r, g, b)); } else { SEGMENT.setPixelColor(i, CRGB::Black); } } strip.show(); lastLineUpdate = micros(); } bool POV::loadImage(const char * filename){ if(!image.init(filename)) return false; if(!image.load()) return false; currentLine=0; return true; } int16_t POV::showNextLine(){ if (!image.isLoaded()) return 0; //move to next line showLine(image.line(currentLine), image.width()); currentLine++; if (currentLine == image.height()) {currentLine=0;} return currentLine; } ================================================ FILE: usermods/pov_display/pov.h ================================================ #ifndef _POV_H #define _POV_H #include "bmpimage.h" class POV { public: POV(); /* Shows one line. line should be pointer to array which holds pixel colors * (3 bytes per pixel, in BGR order). Note: 3, not 4!!! * size should be size of array (number of pixels, not number of bytes) */ void showLine(const byte * line, uint16_t size); /* Reads from file an image and making it current image */ bool loadImage(const char * filename); /* Show next line of active image Retunrs the index of next line to be shown (not yet shown!) If it retunrs 0, it means we have completed showing the image and next call will start again */ int16_t showNextLine(); //time since strip was last updated, in micro sec uint32_t timeSinceUpdate() {return (micros()-lastLineUpdate);} BMPimage * currentImage() {return ℑ} char * getFilename() {return image.getFilename();} private: BMPimage image; int16_t currentLine=0; //next line to be shown uint32_t lastLineUpdate=0; //time in microseconds }; #endif ================================================ FILE: usermods/pov_display/pov_display.cpp ================================================ #include "wled.h" #include "pov.h" static const char _data_FX_MODE_POV_IMAGE[] PROGMEM = "POV Image@!;;;;"; static POV s_pov; void mode_pov_image(void) { Segment& mainseg = strip.getMainSegment(); const char* segName = mainseg.name; if (!segName) { return; } // Only proceed for files ending with .bmp (case-insensitive) size_t segLen = strlen(segName); if (segLen < 4) return; const char* ext = segName + (segLen - 4); // compare case-insensitive to ".bmp" if (!((ext[0]=='.') && (ext[1]=='b' || ext[1]=='B') && (ext[2]=='m' || ext[2]=='M') && (ext[3]=='p' || ext[3]=='P'))) { return; } const char* current = s_pov.getFilename(); if (current && strcmp(segName, current) == 0) { s_pov.showNextLine(); return; } static unsigned long s_lastLoadAttemptMs = 0; unsigned long nowMs = millis(); // Retry at most twice per second if the image is not yet loaded. if (nowMs - s_lastLoadAttemptMs < 500) return; s_lastLoadAttemptMs = nowMs; s_pov.loadImage(segName); return; } class PovDisplayUsermod : public Usermod { protected: bool enabled = false; //WLEDMM const char *_name; //WLEDMM bool initDone = false; //WLEDMM unsigned long lastTime = 0; //WLEDMM public: PovDisplayUsermod(const char *name, bool enabled) : enabled(enabled) , _name(name) {} void setup() override { strip.addEffect(255, &mode_pov_image, _data_FX_MODE_POV_IMAGE); //initDone removed (unused) } void loop() override { // if usermod is disabled or called during strip updating just exit // NOTE: on very long strips strip.isUpdating() may always return true so update accordingly if (!enabled || strip.isUpdating()) return; // do your magic here if (millis() - lastTime > 1000) { lastTime = millis(); } } uint16_t getId() override { return USERMOD_ID_POV_DISPLAY; } }; static PovDisplayUsermod pov_display("POV Display", false); REGISTER_USERMOD(pov_display); ================================================ FILE: usermods/project_cars_shiftlight/readme.md ================================================ # Shift Light for Project Cars Turn your WLED lights into a rev light and shift indicator for Project Cars. It's easy to use. _1._ Make sure your WLED device and your PC/console are on the same network and can talk to each other _2._ Go to the gameplay settings menu in PCARS and enable UDP. There are 9 numbers you can choose from. This is the refresh rate. The lower the number, the better. However, you might run into problems at faster rates. | Number | Updates/Second | | ------ | -------------- | | 1 | 60 | | 2 | 50 | | 3 | 40 | | 4 | 30 | | 5 | 20 | | 6 | 15 | | 7 | 10 | | 8 | 05 | | 9 | 1 | _3._ Once you enter a race, WLED should automatically shift to PCARS mode. _4._ Done. ================================================ FILE: usermods/project_cars_shiftlight/wled06_usermod.ino ================================================ /* * Car rev display and shift indicator for Project Cars * * This works via the UDP telemetry function. You'll need to enable it in the settings of the game. * I've had good results with settings around 5 (20 fps). * */ #include "wled.h" const uint8_t PCARS_dimcolor = 20; WiFiUDP UDP; const unsigned int PCARS_localUdpPort = 5606; // local port to listen on char PCARS_packet[2048]; char PCARS_tempChar[2]; // Temporary array for u16 conversion u16 PCARS_RPM; u16 PCARS_maxRPM; long PCARS_lastRead = millis() - 2001; float PCARS_rpmRatio; void PCARS_readValues() { int PCARS_packetSize = UDP.parsePacket(); if (PCARS_packetSize) { int len = UDP.read(PCARS_packet, PCARS_packetSize); if (len > 0) { PCARS_packet[len] = 0; } if (len == 1367) { // Telemetry packet. Ignoring everything else. PCARS_lastRead = millis(); realtimeLock(realtimeTimeoutMs, REALTIME_MODE_GENERIC); // current RPM memcpy(&PCARS_tempChar, &PCARS_packet[124], 2); PCARS_RPM = (PCARS_tempChar[1] << 8) + PCARS_tempChar[0]; // max RPM memcpy(&PCARS_tempChar, &PCARS_packet[126], 2); PCARS_maxRPM = (PCARS_tempChar[1] << 8) + PCARS_tempChar[0]; if (PCARS_maxRPM) { PCARS_rpmRatio = constrain((float)PCARS_RPM / (float)PCARS_maxRPM, 0, 1); } else { PCARS_rpmRatio = 0.0; } } } } void PCARS_buildcolorbars() { boolean activated = false; float ledratio = 0; uint16_t totalLen = strip.getLengthTotal(); for (uint16_t i = 0; i < totalLen; i++) { if (PCARS_rpmRatio < .95 || (millis() % 100 > 70 )) { ledratio = (float)i / (float)totalLen; if (ledratio < PCARS_rpmRatio) { activated = true; } else { activated = false; } if (ledratio > 0.66) { setRealtimePixel(i, 0, 0, PCARS_dimcolor + ((255 - PCARS_dimcolor)*activated), 0); } else if (ledratio > 0.33) { setRealtimePixel(i, PCARS_dimcolor + ((255 - PCARS_dimcolor)*activated), 0, 0, 0); } else { setRealtimePixel(i, 0, PCARS_dimcolor + ((255 - PCARS_dimcolor)*activated), 0, 0); } } else { setRealtimePixel(i, 0, 0, 0, 0); } } colorUpdated(5); strip.show(); } void userSetup() { UDP.begin(PCARS_localUdpPort); } void userConnected() { // new wifi, who dis? } void userLoop() { PCARS_readValues(); if (PCARS_lastRead > millis() - 2000) { PCARS_buildcolorbars(); } } ================================================ FILE: usermods/pwm_outputs/library.json ================================================ { "name": "pwm_outputs", "build": { "libArchive": false } } ================================================ FILE: usermods/pwm_outputs/pwm_outputs.cpp ================================================ #include "wled.h" #ifndef ESP32 #error This usermod does not support the ESP8266. #endif #ifndef USERMOD_PWM_OUTPUT_PINS #define USERMOD_PWM_OUTPUT_PINS 3 #endif class PwmOutput { public: void open(int8_t pin, uint32_t freq) { if (enabled_) { if (pin == pin_ && freq == freq_) { return; // PWM output is already open } else { close(); // Config has changed, close and reopen } } pin_ = pin; freq_ = freq; if (pin_ < 0) return; DEBUG_PRINTF("pwm_output[%d]: setup to freq %d\n", pin_, freq_); if (!PinManager::allocatePin(pin_, true, PinOwner::UM_PWM_OUTPUTS)) return; channel_ = PinManager::allocateLedc(1); if (channel_ == 255) { DEBUG_PRINTF("pwm_output[%d]: failed to quire ledc\n", pin_); PinManager::deallocatePin(pin_, PinOwner::UM_PWM_OUTPUTS); return; } ledcSetup(channel_, freq_, bit_depth_); ledcAttachPin(pin_, channel_); DEBUG_PRINTF("pwm_output[%d]: init successful\n", pin_); enabled_ = true; } void close() { DEBUG_PRINTF("pwm_output[%d]: close\n", pin_); if (!enabled_) return; PinManager::deallocatePin(pin_, PinOwner::UM_PWM_OUTPUTS); if (channel_ != 255) PinManager::deallocateLedc(channel_, 1); channel_ = 255; duty_ = 0.0f; enabled_ = false; } void setDuty(const float duty) { DEBUG_PRINTF("pwm_output[%d]: set duty %f\n", pin_, duty); if (!enabled_) return; duty_ = min(1.0f, max(0.0f, duty)); const uint32_t value = static_cast((1 << bit_depth_) * duty_); ledcWrite(channel_, value); } void setDuty(const uint16_t duty) { setDuty(static_cast(duty) / 65535.0f); } bool isEnabled() const { return enabled_; } void addToJsonState(JsonObject& pwmState) const { pwmState[F("duty")] = duty_; } void readFromJsonState(JsonObject& pwmState) { if (pwmState.isNull()) { return; } float duty; if (getJsonValue(pwmState[F("duty")], duty)) { setDuty(duty); } } void addToJsonInfo(JsonObject& user) const { if (!enabled_) return; char buffer[12]; sprintf_P(buffer, PSTR("PWM pin %d"), pin_); JsonArray data = user.createNestedArray(buffer); data.add(1e2f * duty_); data.add(F("%")); } void addToConfig(JsonObject& pwmConfig) const { pwmConfig[F("pin")] = pin_; pwmConfig[F("freq")] = freq_; } bool readFromConfig(JsonObject& pwmConfig) { if (pwmConfig.isNull()) return false; bool configComplete = true; int8_t newPin = pin_; uint32_t newFreq = freq_; configComplete &= getJsonValue(pwmConfig[F("pin")], newPin); configComplete &= getJsonValue(pwmConfig[F("freq")], newFreq); open(newPin, newFreq); return configComplete; } private: int8_t pin_ {-1}; uint32_t freq_ {50}; static const uint8_t bit_depth_ {12}; uint8_t channel_ {255}; float duty_ {0.0f}; bool enabled_ {false}; }; class PwmOutputsUsermod : public Usermod { public: static const char USERMOD_NAME[]; static const char PWM_STATE_NAME[]; void setup() { // By default all PWM outputs are disabled, no setup do be done } void loop() { } void addToJsonState(JsonObject& root) { JsonObject pwmStates = root.createNestedObject(PWM_STATE_NAME); for (int i = 0; i < USERMOD_PWM_OUTPUT_PINS; i++) { const PwmOutput& pwm = pwms_[i]; if (!pwm.isEnabled()) continue; char buffer[4]; sprintf_P(buffer, PSTR("%d"), i); JsonObject pwmState = pwmStates.createNestedObject(buffer); pwm.addToJsonState(pwmState); } } void readFromJsonState(JsonObject& root) { JsonObject pwmStates = root[PWM_STATE_NAME]; if (pwmStates.isNull()) return; for (int i = 0; i < USERMOD_PWM_OUTPUT_PINS; i++) { PwmOutput& pwm = pwms_[i]; if (!pwm.isEnabled()) continue; char buffer[4]; sprintf_P(buffer, PSTR("%d"), i); JsonObject pwmState = pwmStates[buffer]; pwm.readFromJsonState(pwmState); } } void addToJsonInfo(JsonObject& root) { JsonObject user = root[F("u")]; if (user.isNull()) user = root.createNestedObject(F("u")); for (int i = 0; i < USERMOD_PWM_OUTPUT_PINS; i++) { const PwmOutput& pwm = pwms_[i]; pwm.addToJsonInfo(user); } } void addToConfig(JsonObject& root) { JsonObject top = root.createNestedObject(USERMOD_NAME); for (int i = 0; i < USERMOD_PWM_OUTPUT_PINS; i++) { const PwmOutput& pwm = pwms_[i]; char buffer[8]; sprintf_P(buffer, PSTR("PWM %d"), i); JsonObject pwmConfig = top.createNestedObject(buffer); pwm.addToConfig(pwmConfig); } } bool readFromConfig(JsonObject& root) { JsonObject top = root[USERMOD_NAME]; if (top.isNull()) return false; bool configComplete = true; for (int i = 0; i < USERMOD_PWM_OUTPUT_PINS; i++) { PwmOutput& pwm = pwms_[i]; char buffer[8]; sprintf_P(buffer, PSTR("PWM %d"), i); JsonObject pwmConfig = top[buffer]; configComplete &= pwm.readFromConfig(pwmConfig); } return configComplete; } uint16_t getId() { return USERMOD_ID_PWM_OUTPUTS; } private: PwmOutput pwms_[USERMOD_PWM_OUTPUT_PINS]; }; const char PwmOutputsUsermod::USERMOD_NAME[] PROGMEM = "PwmOutputs"; const char PwmOutputsUsermod::PWM_STATE_NAME[] PROGMEM = "pwm"; static PwmOutputsUsermod pwm_outputs; REGISTER_USERMOD(pwm_outputs); ================================================ FILE: usermods/pwm_outputs/readme.md ================================================ # PWM outputs v2 Usermod to add generic PWM outputs to WLED. Usermode could be used to control servo motors, LED brightness or any other device controlled by PWM signal. ## Installation Add the compile-time option `-D USERMOD_PWM_OUTPUTS` to your `platformio.ini` (or `platformio_override.ini`). By default upt to 3 PWM outputs could be configured, to increase that limit add build argument `-D USERMOD_PWM_OUTPUT_PINS=10` (replace 10 by desired amount). Currently only ESP32 is supported. ## Configuration By default PWM outputs are disabled, navigate to Usermods settings and configure desired PWM pins and frequencies. ## Usage If PWM output is configured, it starts to publish its duty cycle value (0-1) both to state JSON and to info JSON (visible in UI info panel). To set PWM duty cycle, use JSON api (over HTTP or over Serial) ```json { "pwm": { "0": {"duty": 0.1}, "1": {"duty": 0.2}, ... } } ``` ================================================ FILE: usermods/quinled-an-penta/library.json ================================================ { "name": "quinled-an-penta", "build": { "libArchive": false}, "dependencies": { "olikraus/U8g2":"~2.28.8", "robtillaart/SHT85":"~0.3.3" } } ================================================ FILE: usermods/quinled-an-penta/quinled-an-penta.cpp ================================================ #include "U8g2lib.h" #include "SHT85.h" #include "Wire.h" #include "wled.h" class QuinLEDAnPentaUsermod : public Usermod { private: bool enabled = false; bool firstRunDone = false; bool initDone = false; U8G2 *oledDisplay = nullptr; SHT *sht30TempHumidSensor; // Network info vars bool networkHasChanged = false; bool lastKnownNetworkConnected; IPAddress lastKnownIp; bool lastKnownWiFiConnected; String lastKnownSsid; bool lastKnownApActive; char *lastKnownApSsid; char *lastKnownApPass; byte lastKnownApChannel; int lastKnownEthType; bool lastKnownEthLinkUp; // Brightness / LEDC vars byte lastKnownBri = 0; int8_t currentBussesNumPins[5] = {0, 0, 0, 0, 0}; int8_t currentLedPins[5] = {0, 0, 0, 0, 0}; uint8_t currentLedcReads[5] = {0, 0, 0, 0, 0}; uint8_t lastKnownLedcReads[5] = {0, 0, 0, 0, 0}; // OLED vars bool oledEnabled = false; bool oledInitDone = false; bool oledUseProgressBars = false; bool oledFlipScreen = false; bool oledFixBuggedScreen = false; byte oledMaxPage = 3; byte oledCurrentPage = 3; // Start with the network page to help identifying the IP byte oledSecondsPerPage = 10; unsigned long oledLogoDrawn = 0; unsigned long oledLastTimeUpdated = 0; unsigned long oledLastTimePageChange = 0; unsigned long oledLastTimeFixBuggedScreen = 0; // SHT30 vars bool shtEnabled = false; bool shtInitDone = false; bool shtReadDataSuccess = false; byte shtI2cAddress = 0x44; unsigned long shtLastTimeUpdated = 0; bool shtDataRequested = false; float shtCurrentTemp = 0; float shtLastKnownTemp = 0; float shtCurrentHumidity = 0; float shtLastKnownHumidity = 0; // Pin/IO vars const int8_t anPentaLEDPins[5] = {14, 13, 12, 4, 2}; int8_t oledSpiClk = 15; int8_t oledSpiData = 16; int8_t oledSpiCs = 27; int8_t oledSpiDc = 32; int8_t oledSpiRst = 33; int8_t shtSda = 1; int8_t shtScl = 3; bool isAnPentaLedPin(int8_t pin) { for(int8_t i = 0; i <= 4; i++) { if(anPentaLEDPins[i] == pin) return true; } return false; } void getCurrentUsedLedPins() { for (int8_t lp = 0; lp <= 4; lp++) currentLedPins[lp] = 0; byte numBusses = BusManager::getNumBusses(); byte numUsedPins = 0; for (int8_t b = 0; b < numBusses; b++) { Bus* curBus = BusManager::getBus(b); if (curBus != nullptr) { uint8_t pins[5] = {0, 0, 0, 0, 0}; currentBussesNumPins[b] = curBus->getPins(pins); for (int8_t p = 0; p < currentBussesNumPins[b]; p++) { if (isAnPentaLedPin(pins[p])) { currentLedPins[numUsedPins] = pins[p]; numUsedPins++; } } } } } void getCurrentLedcValues() { byte numBusses = BusManager::getNumBusses(); byte numLedc = 0; for (int8_t b = 0; b < numBusses; b++) { Bus* curBus = BusManager::getBus(b); if (curBus != nullptr) { uint32_t curPixColor = curBus->getPixelColor(0); uint8_t _data[5] = {255, 255, 255, 255, 255}; _data[3] = curPixColor >> 24; _data[0] = curPixColor >> 16; _data[1] = curPixColor >> 8; _data[2] = curPixColor; for (uint8_t i = 0; i < currentBussesNumPins[b]; i++) { currentLedcReads[numLedc] = (_data[i] * bri) / 255; numLedc++; } } } } void initOledDisplay() { PinManagerPinType pins[5] = { { oledSpiClk, true }, { oledSpiData, true }, { oledSpiCs, true }, { oledSpiDc, true }, { oledSpiRst, true } }; if (!PinManager::allocateMultiplePins(pins, 5, PinOwner::UM_QuinLEDAnPenta)) { DEBUG_PRINTF("[%s] OLED pin allocation failed!\n", _name); oledEnabled = oledInitDone = false; return; } oledDisplay = (U8G2 *) new U8G2_SSD1306_128X64_NONAME_2_4W_SW_SPI(U8G2_R0, oledSpiClk, oledSpiData, oledSpiCs, oledSpiDc, oledSpiRst); if (oledDisplay == nullptr) { DEBUG_PRINTF("[%s] OLED init failed!\n", _name); oledEnabled = oledInitDone = false; return; } oledDisplay->begin(); oledDisplay->setBusClock(40 * 1000 * 1000); oledDisplay->setContrast(10); oledDisplay->setPowerSave(0); oledDisplay->setFont(u8g2_font_6x10_tf); oledDisplay->setFlipMode(oledFlipScreen); oledDisplay->firstPage(); do { oledDisplay->drawXBMP(0, 16, 128, 36, quinLedLogo); } while (oledDisplay->nextPage()); oledLogoDrawn = millis(); oledInitDone = true; } void cleanupOledDisplay() { if (oledInitDone) { oledDisplay->clear(); } PinManager::deallocatePin(oledSpiClk, PinOwner::UM_QuinLEDAnPenta); PinManager::deallocatePin(oledSpiData, PinOwner::UM_QuinLEDAnPenta); PinManager::deallocatePin(oledSpiCs, PinOwner::UM_QuinLEDAnPenta); PinManager::deallocatePin(oledSpiDc, PinOwner::UM_QuinLEDAnPenta); PinManager::deallocatePin(oledSpiRst, PinOwner::UM_QuinLEDAnPenta); delete oledDisplay; oledEnabled = false; oledInitDone = false; } bool isOledReady() { return oledEnabled && oledInitDone; } void initSht30TempHumiditySensor() { PinManagerPinType pins[2] = { { shtSda, true }, { shtScl, true } }; if (!PinManager::allocateMultiplePins(pins, 2, PinOwner::UM_QuinLEDAnPenta)) { DEBUG_PRINTF("[%s] SHT30 pin allocation failed!\n", _name); shtEnabled = shtInitDone = false; return; } TwoWire *wire = new TwoWire(1); wire->setClock(400000); sht30TempHumidSensor = (SHT *) new SHT30(); sht30TempHumidSensor->begin(shtI2cAddress, wire); // The SHT lib calls wire.begin() again without the SDA and SCL pins... So call it again here... wire->begin(shtSda, shtScl); if (sht30TempHumidSensor->readStatus() == 0xFFFF) { DEBUG_PRINTF("[%s] SHT30 init failed!\n", _name); shtEnabled = shtInitDone = false; return; } shtInitDone = true; } void cleanupSht30TempHumiditySensor() { if (shtInitDone) { sht30TempHumidSensor->reset(); } PinManager::deallocatePin(shtSda, PinOwner::UM_QuinLEDAnPenta); PinManager::deallocatePin(shtScl, PinOwner::UM_QuinLEDAnPenta); delete sht30TempHumidSensor; shtEnabled = false; shtInitDone = false; } void cleanup() { if (isOledReady()) { cleanupOledDisplay(); } if (isShtReady()) { cleanupSht30TempHumiditySensor(); } enabled = false; } bool oledCheckForNetworkChanges() { if (lastKnownNetworkConnected != Network.isConnected() || lastKnownIp != Network.localIP() || lastKnownWiFiConnected != WiFi.isConnected() || lastKnownSsid != WiFi.SSID() || lastKnownApActive != apActive || lastKnownApSsid != apSSID || lastKnownApPass != apPass || lastKnownApChannel != apChannel) { lastKnownNetworkConnected = Network.isConnected(); lastKnownIp = Network.localIP(); lastKnownWiFiConnected = WiFi.isConnected(); lastKnownSsid = WiFi.SSID(); lastKnownApActive = apActive; lastKnownApSsid = apSSID; lastKnownApPass = apPass; lastKnownApChannel = apChannel; return networkHasChanged = true; } #ifdef WLED_USE_ETHERNET if (lastKnownEthType != ethernetType || lastKnownEthLinkUp != ETH.linkUp()) { lastKnownEthType = ethernetType; lastKnownEthLinkUp = ETH.linkUp(); return networkHasChanged = true; } #endif return networkHasChanged = false; } byte oledGetNextPage() { return oledCurrentPage + 1 <= oledMaxPage ? oledCurrentPage + 1 : 1; } void oledShowPage(byte page, bool updateLastTimePageChange = false) { oledCurrentPage = page; updateOledDisplay(); oledLastTimeUpdated = millis(); if (updateLastTimePageChange) oledLastTimePageChange = oledLastTimeUpdated; } /* * Page 1: Overall brightness and LED outputs * Page 2: General info like temp, humidity and others * Page 3: Network info */ void updateOledDisplay() { if (!isOledReady()) return; oledDisplay->firstPage(); do { oledDisplay->setFont(u8g2_font_chroma48medium8_8r); oledDisplay->drawStr(0, 8, serverDescription); oledDisplay->drawHLine(0, 13, 127); oledDisplay->setFont(u8g2_font_6x10_tf); byte charPerRow = 21; byte oledRow = 23; switch (oledCurrentPage) { // LED Outputs case 1: { char charCurrentBrightness[charPerRow+1] = "Brightness:"; if (oledUseProgressBars) { oledDisplay->drawStr(0, oledRow, charCurrentBrightness); // There is no method to draw a filled box with rounded corners. So draw the rounded frame first, then fill that frame accordingly to LED percentage oledDisplay->drawRFrame(68, oledRow - 6, 60, 7, 2); oledDisplay->drawBox(69, oledRow - 5, int(round(58*getPercentageForBrightness(bri)) / 100), 5); } else { sprintf(charCurrentBrightness, "%s %d%%", charCurrentBrightness, getPercentageForBrightness(bri)); oledDisplay->drawStr(0, oledRow, charCurrentBrightness); } oledRow += 8; byte drawnLines = 0; for (int8_t app = 0; app <= 4; app++) { for (int8_t clp = 0; clp <= 4; clp++) { if (anPentaLEDPins[app] == currentLedPins[clp]) { char charCurrentLedcReads[17]; sprintf(charCurrentLedcReads, "LED %d:", app+1); if (oledUseProgressBars) { oledDisplay->drawStr(0, oledRow+(drawnLines*8), charCurrentLedcReads); oledDisplay->drawRFrame(38, oledRow - 6 + (drawnLines * 8), 90, 7, 2); oledDisplay->drawBox(39, oledRow - 5 + (drawnLines * 8), int(round(88*getPercentageForBrightness(currentLedcReads[clp])) / 100), 5); } else { sprintf(charCurrentLedcReads, "%s %d%%", charCurrentLedcReads, getPercentageForBrightness(currentLedcReads[clp])); oledDisplay->drawStr(0, oledRow+(drawnLines*8), charCurrentLedcReads); } drawnLines++; } } } break; } // Various info case 2: { if (isShtReady() && shtReadDataSuccess) { char charShtCurrentTemp[charPerRow+4]; // Reserve 3 more bytes than usual as we gonna have one UTF8 char which can be up to 4 bytes. sprintf(charShtCurrentTemp, "Temperature: %.02f°C", shtCurrentTemp); char charShtCurrentHumidity[charPerRow+1]; sprintf(charShtCurrentHumidity, "Humidity: %.02f RH", shtCurrentHumidity); oledDisplay->drawUTF8(0, oledRow, charShtCurrentTemp); oledDisplay->drawStr(0, oledRow + 10, charShtCurrentHumidity); oledRow += 20; } if (mqttEnabled && mqttServer[0] != 0) { char charMqttStatus[charPerRow+1]; sprintf(charMqttStatus, "MQTT: %s", (WLED_MQTT_CONNECTED ? "Connected" : "Disconnected")); oledDisplay->drawStr(0, oledRow, charMqttStatus); oledRow += 10; } // Always draw these two on the bottom char charUptime[charPerRow+1]; sprintf(charUptime, "Uptime: %ds", int(millis()/1000 + rolloverMillis*4294967)); // From json.cpp oledDisplay->drawStr(0, 53, charUptime); char charWledVersion[charPerRow+1]; sprintf(charWledVersion, "WLED v%s", versionString); oledDisplay->drawStr(0, 63, charWledVersion); break; } // Network Info case 3: #ifdef WLED_USE_ETHERNET if (lastKnownEthType == WLED_ETH_NONE) { oledDisplay->drawStr(0, oledRow, "Ethernet: No board selected"); oledRow += 10; } else if (!lastKnownEthLinkUp) { oledDisplay->drawStr(0, oledRow, "Ethernet: Link Down"); oledRow += 10; } #endif if (lastKnownNetworkConnected) { #ifdef WLED_USE_ETHERNET if (lastKnownEthLinkUp) { oledDisplay->drawStr(0, oledRow, "Ethernet: Link Up"); oledRow += 10; } else #endif // Wi-Fi can be active with ETH being connected, but we don't mind... if (lastKnownWiFiConnected) { #ifdef WLED_USE_ETHERNET if (!lastKnownEthLinkUp) { #endif oledDisplay->drawStr(0, oledRow, "Wi-Fi: Connected"); char currentSsidChar[lastKnownSsid.length() + 1]; lastKnownSsid.toCharArray(currentSsidChar, lastKnownSsid.length() + 1); char charCurrentSsid[50]; sprintf(charCurrentSsid, "SSID: %s", currentSsidChar); oledDisplay->drawStr(0, oledRow + 10, charCurrentSsid); oledRow += 20; #ifdef WLED_USE_ETHERNET } #endif } String currentIpStr = lastKnownIp.toString(); char currentIpChar[currentIpStr.length() + 1]; currentIpStr.toCharArray(currentIpChar, currentIpStr.length() + 1); char charCurrentIp[30]; sprintf(charCurrentIp, "IP: %s", currentIpChar); oledDisplay->drawStr(0, oledRow, charCurrentIp); } // If WLED AP is active. Theoretically, it can even be active with ETH being connected, but we don't mind... else if (lastKnownApActive) { char charCurrentApStatus[charPerRow+1]; sprintf(charCurrentApStatus, "WLED AP: %s (Ch: %d)", (lastKnownApActive ? "On" : "Off"), lastKnownApChannel); oledDisplay->drawStr(0, oledRow, charCurrentApStatus); char charCurrentApSsid[charPerRow+1]; sprintf(charCurrentApSsid, "SSID: %s", lastKnownApSsid); oledDisplay->drawStr(0, oledRow + 10, charCurrentApSsid); char charCurrentApPass[charPerRow+1]; sprintf(charCurrentApPass, "PW: %s", lastKnownApPass); oledDisplay->drawStr(0, oledRow + 20, charCurrentApPass); // IP is hardcoded / no var exists in WLED at the time this mod was coded, so also hardcode it here oledDisplay->drawStr(0, oledRow + 30, "IP: 4.3.2.1"); } break; } } while (oledDisplay->nextPage()); } bool isShtReady() { return shtEnabled && shtInitDone; } public: // strings to reduce flash memory usage (used more than twice) static const char _name[]; static const char _enabled[]; static const char _oledEnabled[]; static const char _oledUseProgressBars[]; static const char _oledFlipScreen[]; static const char _oledSecondsPerPage[]; static const char _oledFixBuggedScreen[]; static const char _shtEnabled[]; static const unsigned char quinLedLogo[]; static int8_t getPercentageForBrightness(byte brightness) { return int(((float)brightness / (float)255) * 100); } /* * 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() { if (enabled) { lastKnownBri = bri; if (oledEnabled) { initOledDisplay(); } if (shtEnabled) { initSht30TempHumiditySensor(); } getCurrentUsedLedPins(); initDone = true; } firstRunDone = true; } /* * 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() { if (!enabled || !initDone || strip.isUpdating()) return; if (isShtReady()) { if (millis() - shtLastTimeUpdated > 30000 && !shtDataRequested) { sht30TempHumidSensor->requestData(); shtDataRequested = true; shtLastTimeUpdated = millis(); } if (shtDataRequested) { if (sht30TempHumidSensor->dataReady()) { if (sht30TempHumidSensor->readData()) { shtCurrentTemp = sht30TempHumidSensor->getTemperature(); shtCurrentHumidity = sht30TempHumidSensor->getHumidity(); shtReadDataSuccess = true; } else { shtReadDataSuccess = false; } shtDataRequested = false; } } } if (isOledReady() && millis() - oledLogoDrawn > 3000) { // Check for changes on the current page and update the OLED if a change is detected if (millis() - oledLastTimeUpdated > 150) { // If there was a network change, force page 3 (network page) if (oledCheckForNetworkChanges()) { oledCurrentPage = 3; } // Only redraw a page if there was a change for that page switch (oledCurrentPage) { case 1: lastKnownBri = bri; // Probably causes lag to always do ledcRead(), so rather re-do the math, 'cause we can't easily get it... getCurrentLedcValues(); if (bri != lastKnownBri || lastKnownLedcReads[0] != currentLedcReads[0] || lastKnownLedcReads[1] != currentLedcReads[1] || lastKnownLedcReads[2] != currentLedcReads[2] || lastKnownLedcReads[3] != currentLedcReads[3] || lastKnownLedcReads[4] != currentLedcReads[4]) { lastKnownLedcReads[0] = currentLedcReads[0]; lastKnownLedcReads[1] = currentLedcReads[1]; lastKnownLedcReads[2] = currentLedcReads[2]; lastKnownLedcReads[3] = currentLedcReads[3]; lastKnownLedcReads[4] = currentLedcReads[4]; oledShowPage(1); } break; case 2: if (shtLastKnownTemp != shtCurrentTemp || shtLastKnownHumidity != shtCurrentHumidity) { shtLastKnownTemp = shtCurrentTemp; shtLastKnownHumidity = shtCurrentHumidity; oledShowPage(2); } break; case 3: if (networkHasChanged) { networkHasChanged = false; oledShowPage(3, true); } break; } } // Cycle through OLED pages if (millis() - oledLastTimePageChange > oledSecondsPerPage * 1000) { // Periodically fixing a "bugged out" OLED. More details in the ReadMe if (oledFixBuggedScreen && millis() - oledLastTimeFixBuggedScreen > 60000) { oledDisplay->begin(); oledLastTimeFixBuggedScreen = millis(); } oledShowPage(oledGetNextPage(), true); } } } void addToConfig(JsonObject &root) { JsonObject top = root.createNestedObject(FPSTR(_name)); // usermodname top[FPSTR(_enabled)] = enabled; top[FPSTR(_oledEnabled)] = oledEnabled; top[FPSTR(_oledUseProgressBars)] = oledUseProgressBars; top[FPSTR(_oledFlipScreen)] = oledFlipScreen; top[FPSTR(_oledSecondsPerPage)] = oledSecondsPerPage; top[FPSTR(_oledFixBuggedScreen)] = oledFixBuggedScreen; top[FPSTR(_shtEnabled)] = shtEnabled; // Update LED pins on config save getCurrentUsedLedPins(); } /** * readFromConfig() is called before setup() to populate properties from values stored in cfg.json * * The function should return true if configuration was successfully loaded or false if there was no configuration. */ bool readFromConfig(JsonObject &root) { JsonObject top = root[FPSTR(_name)]; if (top.isNull()) { DEBUG_PRINTF("[%s] No config found. (Using defaults.)\n", _name); return false; } bool oldEnabled = enabled; bool oldOledEnabled = oledEnabled; bool oldOledFlipScreen = oledFlipScreen; bool oldShtEnabled = shtEnabled; getJsonValue(top[FPSTR(_enabled)], enabled); getJsonValue(top[FPSTR(_oledEnabled)], oledEnabled); getJsonValue(top[FPSTR(_oledUseProgressBars)], oledUseProgressBars); getJsonValue(top[FPSTR(_oledFlipScreen)], oledFlipScreen); getJsonValue(top[FPSTR(_oledSecondsPerPage)], oledSecondsPerPage); getJsonValue(top[FPSTR(_oledFixBuggedScreen)], oledFixBuggedScreen); getJsonValue(top[FPSTR(_shtEnabled)], shtEnabled); // First run: reading from cfg.json, nothing to do here, will be all done in setup() if (!firstRunDone) { DEBUG_PRINTF("[%s] First run, nothing to do\n", _name); } // Check if mod has been en-/disabled else if (enabled != oldEnabled) { enabled ? setup() : cleanup(); DEBUG_PRINTF("[%s] Usermod has been en-/disabled\n", _name); } // Config has been changed, so adopt to changes else if (enabled) { if (oldOledEnabled != oledEnabled) { oledEnabled ? initOledDisplay() : cleanupOledDisplay(); } else if (oledEnabled && oldOledFlipScreen != oledFlipScreen) { oledDisplay->clear(); oledDisplay->setFlipMode(oledFlipScreen); oledShowPage(oledCurrentPage); } if (oldShtEnabled != shtEnabled) { shtEnabled ? initSht30TempHumiditySensor() : cleanupSht30TempHumiditySensor(); } DEBUG_PRINTF("[%s] Config (re)loaded\n", _name); } return true; } void addToJsonInfo(JsonObject& root) { if (!enabled && !isShtReady()) { return; } JsonObject user = root["u"]; if (user.isNull()) user = root.createNestedObject("u"); JsonArray jsonTemp = user.createNestedArray("Temperature"); JsonArray jsonHumidity = user.createNestedArray("Humidity"); if (shtLastTimeUpdated == 0 || !shtReadDataSuccess) { jsonTemp.add(0); jsonHumidity.add(0); if (shtLastTimeUpdated == 0) { jsonTemp.add(" Not read yet"); jsonHumidity.add(" Not read yet"); } else { jsonTemp.add(" Error"); jsonHumidity.add(" Error"); } return; } jsonHumidity.add(shtCurrentHumidity); jsonHumidity.add(" RH"); jsonTemp.add(shtCurrentTemp); jsonTemp.add(" °C"); } /* * 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_QUINLED_AN_PENTA; } }; // strings to reduce flash memory usage (used more than twice) // Config settings const char QuinLEDAnPentaUsermod::_name[] PROGMEM = "QuinLED-An-Penta"; const char QuinLEDAnPentaUsermod::_enabled[] PROGMEM = "Enabled"; const char QuinLEDAnPentaUsermod::_oledEnabled[] PROGMEM = "Enable-OLED"; const char QuinLEDAnPentaUsermod::_oledUseProgressBars[] PROGMEM = "OLED-Use-Progress-Bars"; const char QuinLEDAnPentaUsermod::_oledFlipScreen[] PROGMEM = "OLED-Flip-Screen-180"; const char QuinLEDAnPentaUsermod::_oledSecondsPerPage[] PROGMEM = "OLED-Seconds-Per-Page"; const char QuinLEDAnPentaUsermod::_oledFixBuggedScreen[] PROGMEM = "OLED-Fix-Bugged-Screen"; const char QuinLEDAnPentaUsermod::_shtEnabled[] PROGMEM = "Enable-SHT30-Temp-Humidity-Sensor"; // Other strings const unsigned char QuinLEDAnPentaUsermod::quinLedLogo[] PROGMEM = { 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x9F, 0xFD, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x03, 0xE0, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x7F, 0x00, 0x80, 0xFF, 0xFF, 0xFF, 0x7F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x3F, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0x1F, 0xFE, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x1F, 0xF0, 0x07, 0xFE, 0xFF, 0xFF, 0x0F, 0xFC, 0xFF, 0xFF, 0xF3, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x1F, 0xFC, 0x0F, 0xFE, 0xFF, 0xFF, 0x0F, 0xFC, 0xFF, 0xFF, 0xE3, 0xFF, 0xA5, 0xFF, 0xFF, 0xFF, 0x0F, 0xFC, 0x1F, 0xFE, 0xFF, 0xFF, 0x1F, 0xFC, 0xFF, 0xFF, 0xE1, 0xFF, 0x00, 0xF0, 0xE3, 0xFF, 0x0F, 0xFE, 0x1F, 0xFE, 0xFF, 0xFF, 0x3F, 0xFF, 0xFF, 0xFF, 0xE3, 0xFF, 0x00, 0xF0, 0x00, 0xFF, 0x07, 0xFE, 0x1F, 0xFC, 0xF9, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xE1, 0xFF, 0x00, 0xF0, 0x00, 0xFE, 0x07, 0xFF, 0x1F, 0xFC, 0xF0, 0xC7, 0x3F, 0xFF, 0xFF, 0xFF, 0xE3, 0xFF, 0xF1, 0xFF, 0x00, 0xFC, 0x07, 0xFF, 0x1F, 0xFE, 0xF0, 0xC3, 0x1F, 0xFE, 0x00, 0xFF, 0xE1, 0xFF, 0xF1, 0xFF, 0x30, 0xF8, 0x07, 0xFF, 0x1F, 0xFE, 0xF0, 0xC3, 0x1F, 0xFE, 0x00, 0xFC, 0xC3, 0xFF, 0xE1, 0xFF, 0xF0, 0xF0, 0x03, 0xFF, 0x0F, 0x7E, 0xF0, 0xC3, 0x1F, 0x7E, 0x00, 0xF8, 0xE3, 0xFF, 0xE1, 0xFF, 0xF1, 0xF1, 0x83, 0xFF, 0x0F, 0x7E, 0xF0, 0xC3, 0x1F, 0x7E, 0x00, 0xF0, 0xC3, 0xFF, 0xE1, 0xFF, 0xF1, 0xE1, 0x83, 0xFF, 0x0F, 0xFE, 0xF0, 0xC3, 0x1F, 0xFE, 0xF8, 0xF0, 0xC3, 0xFF, 0xA1, 0xFF, 0xF1, 0xE3, 0x81, 0xFF, 0x0F, 0x7E, 0xF0, 0xC1, 0x1F, 0x7E, 0xF0, 0xF0, 0xC3, 0xFF, 0x01, 0xF8, 0xE1, 0xC3, 0x83, 0xFF, 0x0F, 0x7F, 0xF8, 0xC3, 0x1F, 0x7E, 0xF8, 0xF0, 0xC3, 0xFF, 0x03, 0xF8, 0xE1, 0xC7, 0x81, 0xE4, 0x0F, 0x7F, 0xF0, 0xC3, 0x1F, 0xFE, 0xF8, 0xF0, 0xC3, 0xFF, 0x01, 0xF8, 0xE3, 0xC7, 0x01, 0xC0, 0x07, 0x7F, 0xF8, 0xC1, 0x1F, 0x7E, 0xF0, 0xE1, 0xC3, 0xFF, 0xC3, 0xFD, 0xE1, 0x87, 0x01, 0x00, 0x07, 0x7F, 0xF8, 0xC3, 0x1F, 0x7E, 0xF8, 0xF0, 0xC3, 0xFF, 0xE3, 0xFF, 0xE3, 0x87, 0x01, 0x00, 0x82, 0x3F, 0xF8, 0xE1, 0x1F, 0xFE, 0xF8, 0xE1, 0xC3, 0xFF, 0xC3, 0xFF, 0xC3, 0x87, 0x01, 0x00, 0x80, 0x3F, 0xF8, 0xC1, 0x1F, 0x7E, 0xF0, 0xF1, 0xC3, 0xFF, 0xC3, 0xFF, 0xC3, 0x87, 0x03, 0x0F, 0x80, 0x3F, 0xF8, 0xE1, 0x0F, 0x7E, 0xF8, 0xE1, 0x87, 0xFF, 0xC3, 0xFF, 0xC7, 0x87, 0x03, 0x04, 0xC0, 0x7F, 0xF0, 0xE1, 0x0F, 0xFF, 0xF8, 0xF1, 0x87, 0xFF, 0xC3, 0xFF, 0xC3, 0x87, 0x07, 0x00, 0xE0, 0x7F, 0x00, 0xE0, 0x1F, 0x7E, 0xF0, 0xE0, 0xC3, 0xFF, 0xC7, 0xFF, 0x87, 0x87, 0x0F, 0x00, 0xE0, 0x7F, 0x00, 0xE0, 0x0F, 0x7F, 0xF8, 0xE1, 0x07, 0x80, 0x07, 0xEA, 0x87, 0xC1, 0x0F, 0x00, 0x80, 0xFF, 0x00, 0xE0, 0x1F, 0x7E, 0xF0, 0xE1, 0x07, 0x00, 0x03, 0x80, 0x07, 0xC0, 0x7F, 0x00, 0x00, 0xFF, 0x01, 0xE0, 0x1F, 0xFF, 0xF8, 0xE1, 0x07, 0x00, 0x07, 0x00, 0x07, 0xE0, 0xFF, 0xF7, 0x01, 0xFF, 0x57, 0xF7, 0x9F, 0xFF, 0xFC, 0xF1, 0x0F, 0x00, 0x07, 0x80, 0x0F, 0xE0, 0xFF, 0xFF, 0x03, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xF7, 0xBF, 0xFE, 0xFF, 0xFF, 0x8F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, }; static QuinLEDAnPentaUsermod quinled_an_penta; REGISTER_USERMOD(quinled_an_penta); ================================================ FILE: usermods/quinled-an-penta/readme.md ================================================ # QuinLED-An-Penta The (un)official usermod to get the best out of the QuinLED-An-Penta (https://quinled.info/quinled-an-penta/), e.g. using the OLED and the SHT30 temperature/humidity sensor. ## Requirements * "u8g2" by olikraus, v2.28 or higher: https://github.com/olikraus/u8g2 * "SHT85" by Rob Tillaart, v0.2 or higher: https://github.com/RobTillaart/SHT85 ## Some words about the (optional) OLED This mod has been optimized for an SSD1306 driven 128x64 OLED. Using a smaller OLED or an OLED using a different driver will result in unexpected results. I highly recommend using these "two color monochromatic OLEDs", which have the first 16 pixels in a different color than the other 48, e.g. a yellow/blue OLED. Note: you _must_ use an **SPI** driven OLED, **not an i2c one**! ### Limitations combined with Ethernet The initial development of this mod was done with a beta version of the QuinLED-An-Penta, which had a different IO layout for the OLED: The CS pin _was_ IO_0, but has been changed to IO27 with the first v1 public release. Unfortunately, IO27 is used by Ethernet boards, so WLED will not let you enable the OLED screen, if you're using it with Ethernet. Unfortunately, that makes the development I've done to support/show Ethernet information invalid, as it cannot be used. However, (and I've not tried this, as I don't own a v1 board) you can modify this usermod and try to use IO27 for the OLED and share it with the Ethernet board. It is "just" the chip select pin, so there is a chance that both can coexist and use the same IO. You need to skip WLEDs PinManager for the CS pin, so WLED will not block using it. If you don't know how this works, don't change it. If you know what I'm talking about, try it and please let me know on the Intermit.Tech (QuinLED) Discord server: https://discord.gg/WdbAauG ### My OLED flickers after some time, what should I do? That's a tricky one. During development I saw that the OLED sometimes starts to "drop out" / flicker and won't work anymore. This seems to be caused by the high PWM interference the board produces. It seems to lose its settings then doesn't know how to draw anymore. Turns out the only way to fix this is to call the libraries `begin()` method again which re-initializes the display. If you're facing this issue, you can enable a setting which will call the `begin()` roughly every 60 seconds between page changes. This will make the page change take ~500ms, but will fix the display. ## Configuration Navigate to the "Config" and then to the "Usermods" section. If you compiled WLED with `-D QUINLED_AN_PENTA`, you will see the config for it there: * Enable-OLED: * What it does: Enables the optional SPI driven OLED that can be mounted to the 7-pin female header. Won't work with Ethernet, read above. * Possible values: Enabled/Disabled * Default: Disabled * OLED-Use-Progress-Bars: * What it does: Toggle between showing percentage numbers or a progress-bar-like visualization for overall brightness and each LED channels brightness level * Possible values: Enabled/Disabled * Default: Disabled * OLED-Flip-Screen-180: * What it does: Flips the screen 180° * Possible values: Enabled/Disabled * Default: Disabled * OLED-Seconds-Per-Page: * What it does: Number of seconds the OLED should stay on one page before changing pages * Possible values: Enabled/Disabled * Default: 10 * OLED-Fix-Bugged-Screen: * What it does: Enable this if your OLED flickers after some time. For more info read above under ["My OLED flickers after some time, what should I do?"](#My-OLED-flickers-after-some-time-what-should-I-do) * Possible values: Enabled/Disabled * Default: Disabled * Enable-SHT30-Temp-Humidity-Sensor: * What it does: Enables the onboard SHT30 temperature and humidity sensor * Possible values: Enabled/Disabled * Default: Disabled ## Change log 2021-12 * Adjusted IO layout to match An-Penta v1r1 2021-10 * First implementation. ## Credits ezcGman | Andy: Find me on the Intermit.Tech (QuinLED) Discord server: https://discord.gg/WdbAauG ================================================ FILE: usermods/readme.md ================================================ # Usermods This folder serves as a repository for usermods (custom `usermod.cpp` files)! If you have created a usermod you believe is useful (for example to support a particular sensor, display, feature...), feel free to contribute by opening a pull request! In order for other people to be able to have fun with your usermod, please keep these points in mind: * Create a folder in this folder with a descriptive name (for example `usermod_ds18b20_temp_sensor_mqtt`) * Include your custom files * If your usermod requires changes to other WLED files, please write a `readme.md` outlining the steps one needs to take * Create a pull request! * If your feature is useful for the majority of WLED users, I will consider adding it to the base code! While I do my best to not break too much, keep in mind that as WLED is updated, usermods might break. I am not actively maintaining any usermod in this directory, that is your responsibility as the creator of the usermod. For new usermods, I would recommend trying out the new v2 usermod API, which allows installing multiple usermods at once and new functions! You can take a look at `EXAMPLE_v2` for some documentation and at `Temperature` for a completed v2 usermod! Thank you for your help :) ================================================ FILE: usermods/rgb-rotary-encoder/library.json ================================================ { "name": "rgb-rotary-encoder", "build": { "libArchive": false}, "dependencies": { "lennarthennigs/ESP Rotary":"^2.1.1" } } ================================================ FILE: usermods/rgb-rotary-encoder/readme.md ================================================ # RGB Encoder Board This usermod-v2 adds support for the awesome RGB Rotary Encoder Board by Adam Zeloof / "Isotope Engineering" to control the overall brightness of your WLED instance: https://github.com/isotope-engineering/RGB-Encoder-Board. A great DIY rotary encoder with 20 tiny SK6805 / "NeoPixel Nano" LEDs. https://user-images.githubusercontent.com/3090131/124680599-0180ab80-dec7-11eb-9065-a6d08ebe0287.mp4 ## Credits The actual / original code that controls the LED modes is from Adam Zeloof. I take no credit for it. I ported it to WLED, which involved replacing the LED library he used, (because WLED already has one, so no need to add another one) plus the rotary encoder library because it was not compatible with ESP, only Arduino. It was quite a bit more work than I hoped, but I got there eventually :) ## How to connect the board to your ESP We'll need (minimum) three or (maximum) four GPIOs for the board: * "ea": reports the encoder direction * "eb": Same thing, opposite direction * "di": LED data in. * *(optional)* "sw": The integrated switch in the rotary encoder. Can be omitted for the bare functionality of controlling only the brightness We'll also need power: * "vdd": Needs to be connected to **+5V**. * "gnd": Ground. You can freely pick the GPIOs, it doesn't matter. Those will be configured in the "Usermods" section of the WLED web panel: ## Configuration Navigate to the "Config" and then to the "Usermods" section. If you compiled WLED with `-D RGB_ROTARY_ENCODER`, you will see the config for it there. The settings there are the aforementioned GPIOs, (*Note: The switch pin is not there, as this can just be configured the "normal" button on the "LED Preferences" page*) plus a few more: * LED pin: * Possible values: Any valid and available GPIO * Default: 3 * What it does: controls the LED ring * ea pin: * Possible values: Any valid and available GPIO * Default: 15 * What it does: First of the two rotary encoder pins * eb pin: * Possible values: Any valid and available GPIO * Default: 32 * What it does: Second of the two rotary encoder pins * LED Mode: * Possible values: 1-3 * Default: 3 * What it does: The usermod provides three different modes of how the LEDs can appear. Here's an example: https://github.com/isotope-engineering/RGB-Encoder-Board/blob/master/images/rgb-encoder-animations.gif * Up left is "1" * Up right is not supported / doesn't make sense for brightness control * Bottom left is "2" * Bottom right is "3" * LED Brightness: * Possible values: 1-255 * Default: 64 * What it does: sets LED ring Brightness * Steps per click: * Possible values: Any positive number * Default: 4 * What it does: With each "click", a rotary encoder actually increments its "steps". Most rotary encoders produce four "steps" per "click". Leave this at the default value unless your rotary encoder behaves strangely. e.g. with one click, it makes two LEDs light up, or you need two clicks for one LED. If that's the case, adjust this value or write a small sketch using the same "ESP Rotary" library and read out the steps it produce. * Increment per click: * Possible values: Any positive number * Default: 5 * What it does: Most rotary encoders have 20 "clicks" or positions. This value should be set to 100/`number of clicks` ## Change log 2021-07 * First implementation. ================================================ FILE: usermods/rgb-rotary-encoder/rgb-rotary-encoder.cpp ================================================ #include "ESPRotary.h" #include #include "wled.h" class RgbRotaryEncoderUsermod : public Usermod { private: bool enabled = false; bool initDone = false; bool isDirty = false; BusDigital *ledBus; /* * Green - eb - Q4 - 32 * Red - ea - Q1 - 15 * Black - sw - Q2 - 12 */ ESPRotary *rotaryEncoder; int8_t ledIo = 3; // GPIO to control the LEDs int8_t eaIo = 15; // "ea" from RGB Encoder Board int8_t ebIo = 32; // "eb" from RGB Encoder Board byte stepsPerClick = 4; // How many "steps" your rotary encoder does per click. This varies per rotary encoder /* This could vary per rotary encoder: Usually rotary encoders have 20 "clicks". If yours has less/more, adjust this to: 100% = 20 LEDs * incrementPerClick */ byte incrementPerClick = 5; byte ledMode = 3; byte ledBrightness = 64; // This is all needed to calculate the brightness, rotary position, etc. const byte minPos = 5; // minPos is not zero, because if we want to turn the LEDs off, we use the built-in button ;) const byte maxPos = 100; // maxPos=100, like 100% const byte numLeds = 20; byte lastKnownPos = 0; byte currentColors[3]; byte lastKnownBri = 0; void initRotaryEncoder() { PinManagerPinType pins[2] = { { eaIo, false }, { ebIo, false } }; if (!PinManager::allocateMultiplePins(pins, 2, PinOwner::UM_RGBRotaryEncoder)) { eaIo = -1; ebIo = -1; cleanup(); return; } // I don't know why, but setting the upper bound here does not work. It results into 1717922932 O_o rotaryEncoder = new ESPRotary(eaIo, ebIo, stepsPerClick, incrementPerClick, maxPos, currentPos, incrementPerClick); rotaryEncoder->setUpperBound(maxPos); // I have to again set it here and then it works / is actually 100... rotaryEncoder->setChangedHandler(RgbRotaryEncoderUsermod::cbRotate); } void initLedBus() { // Initialize all pins to the sentinel value first… byte _pins[OUTPUT_MAX_PINS]; std::fill(std::begin(_pins), std::end(_pins), 255); // …then set only the LED pin _pins[0] = static_cast(ledIo); BusConfig busCfg = BusConfig(TYPE_WS2812_RGB, _pins, 0, numLeds, COL_ORDER_GRB, false, 0); busCfg.iType = BusManager::getI(busCfg.type, busCfg.pins, busCfg.driverType); // assign internal bus type and output driver ledBus = new BusDigital(busCfg); if (!ledBus->isOk()) { cleanup(); return; } ledBus->setBrightness(ledBrightness); } void updateLeds() { switch (ledMode) { case 2: { currentColors[0] = 255; currentColors[1] = 0; currentColors[2] = 0; for (int i = 0; i < currentPos / incrementPerClick - 1; i++) { ledBus->setPixelColor(i, 0); } ledBus->setPixelColor(currentPos / incrementPerClick - 1, colorFromRgbw(currentColors)); for (int i = currentPos / incrementPerClick; i < numLeds; i++) { ledBus->setPixelColor(i, 0); } } break; default: case 1: case 3: // WLED orange (of course), which we will use in mode 1 currentColors[0] = 255; currentColors[1] = 160; currentColors[2] = 0; for (int i = 0; i < currentPos / incrementPerClick; i++) { if (ledMode == 3) { hsv2rgb((i) / float(numLeds), 1, .25); } ledBus->setPixelColor(i, colorFromRgbw(currentColors)); } for (int i = currentPos / incrementPerClick; i < numLeds; i++) { ledBus->setPixelColor(i, 0); } break; } isDirty = true; } void cleanup() { // Only deallocate pins if we allocated them ;) if (eaIo != -1) { PinManager::deallocatePin(eaIo, PinOwner::UM_RGBRotaryEncoder); eaIo = -1; } if (ebIo != -1) { PinManager::deallocatePin(ebIo, PinOwner::UM_RGBRotaryEncoder); ebIo = -1; } delete rotaryEncoder; delete ledBus; enabled = false; } int getPositionForBrightness() { return int(((float)bri / (float)255) * 100); } float fract(float x) { return x - int(x); } float mix(float a, float b, float t) { return a + (b - a) * t; } void hsv2rgb(float h, float s, float v) { currentColors[0] = int((v * mix(1.0, constrain(abs(fract(h + 1.0) * 6.0 - 3.0) - 1.0, 0.0, 1.0), s)) * 255); currentColors[1] = int((v * mix(1.0, constrain(abs(fract(h + 0.6666666) * 6.0 - 3.0) - 1.0, 0.0, 1.0), s)) * 255); currentColors[2] = int((v * mix(1.0, constrain(abs(fract(h + 0.3333333) * 6.0 - 3.0) - 1.0, 0.0, 1.0), s)) * 255); } public: static byte currentPos; // strings to reduce flash memory usage (used more than twice) static const char _name[]; static const char _enabled[]; static const char _ledIo[]; static const char _eaIo[]; static const char _ebIo[]; static const char _ledMode[]; static const char _ledBrightness[]; static const char _stepsPerClick[]; static const char _incrementPerClick[]; static void cbRotate(ESPRotary& r) { currentPos = r.getPosition(); } /** * Enable/Disable the usermod */ // inline void enable(bool enable) { enabled = enable; } /** * Get usermod enabled/disabled state */ // inline bool isEnabled() { return enabled; } /* * 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() { if (enabled) { currentPos = getPositionForBrightness(); lastKnownBri = bri; initRotaryEncoder(); initLedBus(); // No updating of LEDs here, as that's sometimes not working; loop() will take care of that initDone = true; } } /* * 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() { if (!enabled || strip.isUpdating()) return; rotaryEncoder->loop(); // If the rotary was changed if(lastKnownPos != currentPos) { lastKnownPos = currentPos; bri = min(int(round((2.55 * currentPos))), 255); lastKnownBri = bri; updateLeds(); colorUpdated(CALL_MODE_DIRECT_CHANGE); } // If the brightness is changed not with the rotary, update the rotary if (bri != lastKnownBri) { currentPos = lastKnownPos = getPositionForBrightness(); lastKnownBri = bri; rotaryEncoder->resetPosition(currentPos); updateLeds(); } // Update LEDs here in loop to also validate that we can update/show if (isDirty && ledBus->canShow()) { isDirty = false; ledBus->show(); } } void addToConfig(JsonObject &root) { JsonObject top = root.createNestedObject(FPSTR(_name)); // usermodname top[FPSTR(_enabled)] = enabled; top[FPSTR(_ledIo)] = ledIo; top[FPSTR(_eaIo)] = eaIo; top[FPSTR(_ebIo)] = ebIo; top[FPSTR(_ledMode)] = ledMode; top[FPSTR(_ledBrightness)] = ledBrightness; top[FPSTR(_stepsPerClick)] = stepsPerClick; top[FPSTR(_incrementPerClick)] = incrementPerClick; } /** * readFromConfig() is called before setup() to populate properties from values stored in cfg.json * * The function should return true if configuration was successfully loaded or false if there was no configuration. */ bool readFromConfig(JsonObject &root) { JsonObject top = root[FPSTR(_name)]; if (top.isNull()) { DEBUG_PRINTF("[%s] No config found. (Using defaults.)\n", _name); return false; } bool oldEnabled = enabled; int8_t oldLedIo = ledIo; int8_t oldEaIo = eaIo; int8_t oldEbIo = ebIo; byte oldLedMode = ledMode; byte oldStepsPerClick = stepsPerClick; byte oldIncrementPerClick = incrementPerClick; byte oldLedBrightness = ledBrightness; getJsonValue(top[FPSTR(_enabled)], enabled); getJsonValue(top[FPSTR(_ledIo)], ledIo); getJsonValue(top[FPSTR(_eaIo)], eaIo); getJsonValue(top[FPSTR(_ebIo)], ebIo); getJsonValue(top[FPSTR(_stepsPerClick)], stepsPerClick); getJsonValue(top[FPSTR(_incrementPerClick)], incrementPerClick); ledMode = top[FPSTR(_ledMode)] > 0 && top[FPSTR(_ledMode)] < 4 ? top[FPSTR(_ledMode)] : ledMode; ledBrightness = top[FPSTR(_ledBrightness)] > 0 && top[FPSTR(_ledBrightness)] <= 255 ? top[FPSTR(_ledBrightness)] : ledBrightness; if (!initDone) { // First run: reading from cfg.json // Nothing to do here, will be all done in setup() } // Mod was disabled, so run setup() else if (enabled && enabled != oldEnabled) { DEBUG_PRINTF("[%s] Usermod has been re-enabled\n", _name); setup(); } // Config has been changed, so adopt to changes else { if (!enabled) { DEBUG_PRINTF("[%s] Usermod has been disabled\n", _name); cleanup(); } else { DEBUG_PRINTF("[%s] Usermod is enabled\n", _name); if (ledIo != oldLedIo) { delete ledBus; initLedBus(); } if (ledBrightness != oldLedBrightness) { ledBus->setBrightness(ledBrightness); isDirty = true; } if (ledMode != oldLedMode) { updateLeds(); } if (eaIo != oldEaIo || ebIo != oldEbIo || stepsPerClick != oldStepsPerClick || incrementPerClick != oldIncrementPerClick) { PinManager::deallocatePin(oldEaIo, PinOwner::UM_RGBRotaryEncoder); PinManager::deallocatePin(oldEbIo, PinOwner::UM_RGBRotaryEncoder); delete rotaryEncoder; initRotaryEncoder(); } } DEBUG_PRINTF("[%s] Config (re)loaded\n", _name); } return true; } /* * 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_RGB_ROTARY_ENCODER; } //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! }; byte RgbRotaryEncoderUsermod::currentPos = 5; // strings to reduce flash memory usage (used more than twice) const char RgbRotaryEncoderUsermod::_name[] PROGMEM = "RGB-Rotary-Encoder"; const char RgbRotaryEncoderUsermod::_enabled[] PROGMEM = "Enabled"; const char RgbRotaryEncoderUsermod::_ledIo[] PROGMEM = "LED-pin"; const char RgbRotaryEncoderUsermod::_eaIo[] PROGMEM = "ea-pin"; const char RgbRotaryEncoderUsermod::_ebIo[] PROGMEM = "eb-pin"; const char RgbRotaryEncoderUsermod::_ledMode[] PROGMEM = "LED-Mode"; const char RgbRotaryEncoderUsermod::_ledBrightness[] PROGMEM = "LED-Brightness"; const char RgbRotaryEncoderUsermod::_stepsPerClick[] PROGMEM = "Steps-per-Click"; const char RgbRotaryEncoderUsermod::_incrementPerClick[] PROGMEM = "Increment-per-Click"; static RgbRotaryEncoderUsermod rgb_rotary_encoder; REGISTER_USERMOD(rgb_rotary_encoder); ================================================ FILE: usermods/rotary_encoder_change_effect/wled06_usermod.ino ================================================ //Use userVar0 and userVar1 (API calls &U0=,&U1=, uint16_t) long lastTime = 0; int delayMs = 10; const int pinA = D6; //data const int pinB = D7; //clk int oldA = LOW; //gets called once at boot. Do all initialization that doesn't depend on network here void userSetup() { pinMode(pinA, INPUT_PULLUP); pinMode(pinB, INPUT_PULLUP); } //gets called every time WiFi is (re-)connected. Initialize own network interfaces here void userConnected() { } //loop. You can use "if (WLED_CONNECTED)" to check for successful connection void userLoop() { if (millis()-lastTime > delayMs) { int A = digitalRead(pinA); int B = digitalRead(pinB); if (oldA == LOW && A == HIGH) { if (oldB == HIGH) { // bri += 10; // if (bri > 250) bri = 10; effectCurrent += 1; if (effectCurrent >= MODE_COUNT) effectCurrent = 0; } else { // bri -= 10; // if (bri < 10) bri = 250; effectCurrent -= 1; if (effectCurrent < 0) effectCurrent = (MODE_COUNT-1); } oldA = A; //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(CALL_MODE_FX_CHANGED); lastTime = millis(); } } ================================================ FILE: usermods/sd_card/library.json ================================================ { "name": "sd_card", "build": { "libArchive": false } } ================================================ FILE: usermods/sd_card/readme.md ================================================ # SD-card mod ## Build - modify `platformio.ini` and add to the `build_flags` of your configuration the following - choose the way your SD is connected 1. via `-D WLED_USE_SD_MMC` when connected via MMC 2. via `-D WLED_USE_SD_SPI` when connected via SPI (use usermod page to setup SPI pins) ### Test - enable `-D SD_PRINT_HOME_DIR` and `-D WLED_DEBUG` - this will print all files in `/` on boot via serial ## Configuration ### MMC - The MMC port / pins needs no configuration as they are specified by Espressif ### SPI - The SPI port / pins can be modified via the WLED web-UI: `Config → Usermod → SD Card` | option | effect | default | | ----------------- | ------------------------------------------------------------------------------------------------ | ------- | | `pinSourceSelect` | GPIO that is connected to SD's `SS`(source select) / `CS`(chip select) | 16 | | `pinSourceClock` | GPIO that is connected to SD's `SCLK` (source clock) / `CLK`(clock) | 14 | | `pinPoci` | GPIO that is connected to SD's `POCI` (Peripheral-Out-Ctrl-In) / `MISO` (deprecated) | 36 | | `pinPico` | GPIO that is connected to SD's `PICO` (Peripheral-In-Ctrl-Out) / `MOSI` (deprecated) | 15 | | `sdEnable` | Enable to read data from the SD-card | true | Following new naming convention of [OSHWA](https://www.oshwa.org/a-resolution-to-redefine-spi-signal-names/) ## Usage in other mods - creates a macro `SD_ADAPTER` which is either mapped to `SD` or `SD_MMC` (see `SD_Test.ino` how to use SD / SD_MMC functions) - checks if the specified file is available on the SD card ```cpp bool file_onSD(const char *filepath) {...} ``` ================================================ FILE: usermods/sd_card/sd_card.cpp ================================================ #include "wled.h" // SD connected via MMC / SPI #if defined(WLED_USE_SD_MMC) #define USED_STORAGE_FILESYSTEMS "SD MMC, LittleFS" #define SD_ADAPTER SD_MMC #include "SD_MMC.h" // SD connected via SPI (adjustable via usermod config) #elif defined(WLED_USE_SD_SPI) #define SD_ADAPTER SD #define USED_STORAGE_FILESYSTEMS "SD SPI, LittleFS" #include "SD.h" #include "SPI.h" #endif #ifdef WLED_USE_SD_MMC #elif defined(WLED_USE_SD_SPI) SPIClass spiPort = SPIClass(VSPI); #endif void listDir( const char * dirname, uint8_t levels); class UsermodSdCard : public Usermod { private: bool sdInitDone = false; #ifdef WLED_USE_SD_SPI int8_t configPinSourceSelect = 16; int8_t configPinSourceClock = 14; int8_t configPinPoci = 36; // confusing names? Then have a look :) int8_t configPinPico = 15; // https://www.oshwa.org/a-resolution-to-redefine-spi-signal-names/ //acquired and initialize the SPI port void init_SD_SPI() { if(!configSdEnabled) return; if(sdInitDone) return; PinManagerPinType pins[5] = { { configPinSourceSelect, true }, { configPinSourceClock, true }, { configPinPoci, false }, { configPinPico, true } }; if (!PinManager::allocateMultiplePins(pins, 4, PinOwner::UM_SdCard)) { DEBUG_PRINTF("[%s] SD (SPI) pin allocation failed!\n", _name); sdInitDone = false; return; } bool returnOfInitSD = false; #if defined(WLED_USE_SD_SPI) spiPort.begin(configPinSourceClock, configPinPoci, configPinPico, configPinSourceSelect); returnOfInitSD = SD_ADAPTER.begin(configPinSourceSelect, spiPort); #endif if(!returnOfInitSD) { DEBUG_PRINTF("[%s] SPI begin failed!\n", _name); sdInitDone = false; return; } sdInitDone = true; } //deinitialize the acquired SPI port void deinit_SD_SPI() { if(!sdInitDone) return; SD_ADAPTER.end(); DEBUG_PRINTF("[%s] deallocate pins!\n", _name); PinManager::deallocatePin(configPinSourceSelect, PinOwner::UM_SdCard); PinManager::deallocatePin(configPinSourceClock, PinOwner::UM_SdCard); PinManager::deallocatePin(configPinPoci, PinOwner::UM_SdCard); PinManager::deallocatePin(configPinPico, PinOwner::UM_SdCard); sdInitDone = false; } // some SPI pin was changed, while SPI was initialized, reinit to new port void reinit_SD_SPI() { deinit_SD_SPI(); init_SD_SPI(); } #endif #ifdef WLED_USE_SD_MMC void init_SD_MMC() { if(sdInitDone) return; bool returnOfInitSD = false; returnOfInitSD = SD_ADAPTER.begin(); DEBUG_PRINTF("[%s] MMC begin\n", _name); if(!returnOfInitSD) { DEBUG_PRINTF("[%s] MMC begin failed!\n", _name); sdInitDone = false; return; } sdInitDone = true; } #endif public: static bool configSdEnabled; static const char _name[]; void setup() { DEBUG_PRINTF("[%s] usermod loaded \n", _name); #if defined(WLED_USE_SD_SPI) init_SD_SPI(); #elif defined(WLED_USE_SD_MMC) init_SD_MMC(); #endif #if defined(SD_ADAPTER) && defined(SD_PRINT_HOME_DIR) listDir("/", 0); #endif } void loop(){ } uint16_t getId() { return USERMOD_ID_SD_CARD; } void addToConfig(JsonObject& root) { #ifdef WLED_USE_SD_SPI JsonObject top = root.createNestedObject(FPSTR(_name)); top["pinSourceSelect"] = configPinSourceSelect; top["pinSourceClock"] = configPinSourceClock; top["pinPoci"] = configPinPoci; top["pinPico"] = configPinPico; top["sdEnabled"] = configSdEnabled; #endif } bool readFromConfig(JsonObject &root) { #ifdef WLED_USE_SD_SPI JsonObject top = root[FPSTR(_name)]; if (top.isNull()) { DEBUG_PRINTF("[%s] No config found. (Using defaults.)\n", _name); return false; } uint8_t oldPinSourceSelect = configPinSourceSelect; uint8_t oldPinSourceClock = configPinSourceClock; uint8_t oldPinPoci = configPinPoci; uint8_t oldPinPico = configPinPico; bool oldSdEnabled = configSdEnabled; getJsonValue(top["pinSourceSelect"], configPinSourceSelect); getJsonValue(top["pinSourceClock"], configPinSourceClock); getJsonValue(top["pinPoci"], configPinPoci); getJsonValue(top["pinPico"], configPinPico); getJsonValue(top["sdEnabled"], configSdEnabled); if(configSdEnabled != oldSdEnabled) { configSdEnabled ? init_SD_SPI() : deinit_SD_SPI(); DEBUG_PRINTF("[%s] SD card %s\n", _name, configSdEnabled ? "enabled" : "disabled"); } if( configSdEnabled && ( oldPinSourceSelect != configPinSourceSelect || oldPinSourceClock != configPinSourceClock || oldPinPoci != configPinPoci || oldPinPico != configPinPico) ) { DEBUG_PRINTF("[%s] Init SD card based of config\n", _name); DEBUG_PRINTF("[%s] Config changes \n - SS: %d -> %d\n - MI: %d -> %d\n - MO: %d -> %d\n - En: %d -> %d\n", _name, oldPinSourceSelect, configPinSourceSelect, oldPinSourceClock, configPinSourceClock, oldPinPoci, configPinPoci, oldPinPico, configPinPico); reinit_SD_SPI(); } #endif return true; } }; const char UsermodSdCard::_name[] PROGMEM = "SD Card"; bool UsermodSdCard::configSdEnabled = true; #ifdef SD_ADAPTER //checks if the file is available on SD card bool file_onSD(const char *filepath) { #ifdef WLED_USE_SD_SPI if(!UsermodSdCard::configSdEnabled) return false; #endif uint8_t cardType = SD_ADAPTER.cardType(); if(cardType == CARD_NONE) { DEBUG_PRINTF("[%s] not attached / cardType none\n", UsermodSdCard::_name); return false; // no SD card attached } if(cardType == CARD_MMC || cardType == CARD_SD || cardType == CARD_SDHC) { return SD_ADAPTER.exists(filepath); } return false; // unknown card type } void listDir( const char * dirname, uint8_t levels){ DEBUG_PRINTF("Listing directory: %s\n", dirname); File root = SD_ADAPTER.open(dirname); if(!root){ DEBUG_PRINTF("Failed to open directory\n"); return; } if(!root.isDirectory()){ DEBUG_PRINTF("Not a directory\n"); return; } File file = root.openNextFile(); while(file){ if(file.isDirectory()){ DEBUG_PRINTF(" DIR : %s\n",file.name()); if(levels){ listDir(file.name(), levels -1); } } else { DEBUG_PRINTF(" FILE: %s SIZE: %d\n",file.name(), file.size()); } file = root.openNextFile(); } } #endif static UsermodSdCard sd_card; REGISTER_USERMOD(sd_card); ================================================ FILE: usermods/sensors_to_mqtt/library.json ================================================ { "name": "sensors_to_mqtt", "build": { "libArchive": false}, "dependencies": { "adafruit/Adafruit BMP280 Library":"2.6.8", "adafruit/Adafruit CCS811 Library":"1.1.3", "adafruit/Adafruit Si7021 Library":"1.5.3", "adafruit/Adafruit Unified Sensor":"^1.1.15" } } ================================================ FILE: usermods/sensors_to_mqtt/readme.md ================================================ # Send sensor data To Home Assistant Publishes BMP280, CCS811 and Si7021 measurements to Home Assistant via MQTT. Uses Home Assistant Automatic Device Discovery. The use of Home Assistant is not mandatory. The mod will publish sensor values via MQTT just fine without it. Uses the MQTT connection set in the WLED web user interface. ## Maintainer twitter.com/mpronk89 ## Features - Reads BMP280, CCS811 and Si7021 senors - Publishes via MQTT, configured via WLED webUI - Announces device in Home Assistant for easy setup - Efficient energy usage - Updates every 60 seconds ## Example MQTT topics: `$mqttDeviceTopic` is set in webui of WLED! ``` temperature: $mqttDeviceTopic/temperature pressure: $mqttDeviceTopic/pressure humidity: $mqttDeviceTopic/humidity tvoc: $mqttDeviceTopic/tvoc eCO2: $mqttDeviceTopic/eco2 IAQ: $mqttDeviceTopic/iaq ``` # Installation ## Hardware ### Requirements 1. BMP280/CCS811/Si7021 sensor. E.g. https://aliexpress.com/item/32979998543.html 2. A microcontroller that supports i2c. e.g. esp32 ### installation Attach the sensor to the i2c interface. Default PINs esp32: ``` SCL_PIN = 22; SDA_PIN = 21; ``` Default PINs ESP8266: ``` SCL_PIN = 5; SDA_PIN = 4; ``` # Credits - Aircoookie for making WLED - Other usermod creators for example code - Bouke_Regnerus for https://community.home-assistant.io/t/example-indoor-air-quality-text-sensor-using-ccs811-sensor/125854 - You, for reading this ================================================ FILE: usermods/sensors_to_mqtt/sensors_to_mqtt.cpp ================================================ #include "wled.h" #include #include #include #include #include #ifdef WLED_DISABLE_MQTT #error "This user mod requires MQTT to be enabled." #endif static Adafruit_BMP280 bmp; static Adafruit_Si7021 si7021; static Adafruit_CCS811 ccs811; class UserMod_SensorsToMQTT : public Usermod { private: bool initialized = false; bool mqttInitialized = false; float SensorPressure = 0; float SensorTemperature = 0; float SensorHumidity = 0; const char *SensorIaq = "Unknown"; String mqttTemperatureTopic = ""; String mqttHumidityTopic = ""; String mqttPressureTopic = ""; String mqttTvocTopic = ""; String mqttEco2Topic = ""; String mqttIaqTopic = ""; unsigned int SensorTvoc = 0; unsigned int SensorEco2 = 0; unsigned long nextMeasure = 0; void _initialize() { initialized = bmp.begin(BMP280_ADDRESS_ALT); bmp.setSampling(Adafruit_BMP280::MODE_NORMAL, /* Operating Mode. */ Adafruit_BMP280::SAMPLING_X16, /* Temp. oversampling */ Adafruit_BMP280::SAMPLING_X16, /* Pressure oversampling */ Adafruit_BMP280::FILTER_X16, /* Filtering. */ Adafruit_BMP280::STANDBY_MS_2000); /* Refresh values every 20 seconds */ initialized &= si7021.begin(); initialized &= ccs811.begin(); ccs811.setDriveMode(CCS811_DRIVE_MODE_10SEC); /* Refresh values every 10s */ Serial.print(initialized); } void _mqttInitialize() { mqttTemperatureTopic = String(mqttDeviceTopic) + "/temperature"; mqttPressureTopic = String(mqttDeviceTopic) + "/pressure"; mqttHumidityTopic = String(mqttDeviceTopic) + "/humidity"; mqttTvocTopic = String(mqttDeviceTopic) + "/tvoc"; mqttEco2Topic = String(mqttDeviceTopic) + "/eco2"; mqttIaqTopic = String(mqttDeviceTopic) + "/iaq"; String t = String("homeassistant/sensor/") + mqttClientID + "/temperature/config"; _createMqttSensor("temperature", mqttTemperatureTopic, "temperature", "°C"); _createMqttSensor("pressure", mqttPressureTopic, "pressure", "hPa"); _createMqttSensor("humidity", mqttHumidityTopic, "humidity", "%"); _createMqttSensor("tvoc", mqttTvocTopic, "", "ppb"); _createMqttSensor("eco2", mqttEco2Topic, "", "ppm"); _createMqttSensor("iaq", mqttIaqTopic, "", ""); } void _createMqttSensor(const String &name, const String &topic, const String &deviceClass, const String &unitOfMeasurement) { String t = String("homeassistant/sensor/") + mqttClientID + "/" + name + "/config"; StaticJsonDocument<300> doc; doc["name"] = name; doc["state_topic"] = topic; doc["unique_id"] = String(mqttClientID) + name; if (unitOfMeasurement != "") doc["unit_of_measurement"] = unitOfMeasurement; if (deviceClass != "") doc["device_class"] = deviceClass; doc["expire_after"] = 1800; JsonObject device = doc.createNestedObject("device"); // attach the sensor to the same device device["identifiers"] = String("wled-sensor-") + mqttClientID; device["manufacturer"] = F(WLED_BRAND); device["model"] = F(WLED_PRODUCT_NAME); device["sw_version"] = VERSION; device["name"] = mqttClientID; String temp; serializeJson(doc, temp); Serial.println(t); Serial.println(temp); mqtt->publish(t.c_str(), 0, true, temp.c_str()); } void _updateSensorData() { SensorTemperature = bmp.readTemperature(); SensorHumidity = si7021.readHumidity(); SensorPressure = (bmp.readPressure() / 100.0F); ccs811.setEnvironmentalData(SensorHumidity, SensorTemperature); ccs811.readData(); SensorTvoc = ccs811.getTVOC(); SensorEco2 = ccs811.geteCO2(); SensorIaq = _getIaqIndex(SensorHumidity, SensorTvoc, SensorEco2); Serial.printf("%f c, %f humidity, %f hPA, %u tvoc, %u Eco2, %s iaq\n", SensorTemperature, SensorHumidity, SensorPressure, SensorTvoc, SensorEco2, SensorIaq); } /** * Credits: Bouke_Regnerus @ https://community.home-assistant.io/t/example-indoor-air-quality-text-sensor-using-ccs811-sensor/125854 */ const char *_getIaqIndex(float humidity, int tvoc, int eco2) { int iaq_index = 0; /* * Transform indoor humidity values to IAQ points according to Indoor Air Quality UK: * http://www.iaquk.org.uk/ */ if (humidity < 10 or humidity > 90) { iaq_index += 1; } else if (humidity < 20 or humidity > 80) { iaq_index += 2; } else if (humidity < 30 or humidity > 70) { iaq_index += 3; } else if (humidity < 40 or humidity > 60) { iaq_index += 4; } else if (humidity >= 40 and humidity <= 60) { iaq_index += 5; } /* * Transform eCO2 values to IAQ points according to Indoor Air Quality UK: * http://www.iaquk.org.uk/ */ if (eco2 <= 600) { iaq_index += 5; } else if (eco2 <= 800) { iaq_index += 4; } else if (eco2 <= 1500) { iaq_index += 3; } else if (eco2 <= 1800) { iaq_index += 2; } else if (eco2 > 1800) { iaq_index += 1; } /* * Transform TVOC values to IAQ points according to German environmental guidelines: * https://www.repcomsrl.com/wp-content/uploads/2017/06/Environmental_Sensing_VOC_Product_Brochure_EN.pdf */ if (tvoc <= 65) { iaq_index += 5; } else if (tvoc <= 220) { iaq_index += 4; } else if (tvoc <= 660) { iaq_index += 3; } else if (tvoc <= 2200) { iaq_index += 2; } else if (tvoc > 2200) { iaq_index += 1; } if (iaq_index <= 6) { return "Unhealty"; } else if (iaq_index <= 9) { return "Poor"; } else if (iaq_index <= 12) { return "Moderate"; } else if (iaq_index <= 14) { return "Good"; } else if (iaq_index > 14) { return "Excellent"; } return "Unknown"; } public: void setup() { Serial.println("Starting!"); Serial.println("Initializing sensors.. "); _initialize(); } // gets called every time WiFi is (re-)connected. void connected() { nextMeasure = millis() + 5000; // Schedule next measure in 5 seconds } void loop() { unsigned long tempTimer = millis(); if (tempTimer > nextMeasure) { nextMeasure = tempTimer + 60000; // Schedule next measure in 60 seconds if (!initialized) { Serial.println("Error! Sensors not initialized in loop()!"); _initialize(); return; // lets try again next loop } if (mqtt != nullptr && mqtt->connected()) { if (!mqttInitialized) { _mqttInitialize(); mqttInitialized = true; } // Update sensor data _updateSensorData(); // Create string populated with user defined device topic from the UI, // and the read temperature, humidity and pressure. // Then publish to MQTT server. mqtt->publish(mqttTemperatureTopic.c_str(), 0, true, String(SensorTemperature).c_str()); mqtt->publish(mqttPressureTopic.c_str(), 0, true, String(SensorPressure).c_str()); mqtt->publish(mqttHumidityTopic.c_str(), 0, true, String(SensorHumidity).c_str()); mqtt->publish(mqttTvocTopic.c_str(), 0, true, String(SensorTvoc).c_str()); mqtt->publish(mqttEco2Topic.c_str(), 0, true, String(SensorEco2).c_str()); mqtt->publish(mqttIaqTopic.c_str(), 0, true, String(SensorIaq).c_str()); } else { Serial.println("Missing MQTT connection. Not publishing data"); mqttInitialized = false; } } } }; static UserMod_SensorsToMQTT sensors_to_mqtt; REGISTER_USERMOD(sensors_to_mqtt); ================================================ FILE: usermods/seven_segment_display/library.json ================================================ { "name": "seven_segment_display", "build": { "libArchive": false } } ================================================ FILE: usermods/seven_segment_display/readme.md ================================================ # Seven Segment Display Uses the overlay feature to create a configurable seven segment display. This has only been tested on a single configuration. Colon support has _not_ been tested. ## Installation Add the compile-time option `-D USERMOD_SEVEN_SEGMENT` to your `platformio.ini` (or `platformio_override.ini`) or use `#define USERMOD_SEVEN_SEGMENT` in `my_config.h`. ## Settings Settings can be controlled via both the usermod setting page and through MQTT with a raw payload. ##### Example Topic ```/sevenSeg/perSegment/set``` Payload ```3``` #### perSegment -- ssLEDPerSegment The number of individual LEDs per segment. 7 segments per digit. #### perPeriod -- ssLEDPerPeriod The number of individual LEDs per period. A ':' (colon) has two periods. #### startIdx -- ssStartLED Index of the LED the display starts at. Enables a seven segment display to be in the middle of a string. #### timeEnable -- ssTimeEnabled When true, when displayMask is configured for a time output and no message is set, the time will be displayed. #### scrollSpd -- ssScrollSpeed Time, in milliseconds, between message shifts when the length of displayMsg exceeds the length of the displayMask. #### displayMask -- ssDisplayMask This should represent the configuration of the physical display.
HH - 0-23. hh - 1-12, kk - 1-24 hours  
MM or mm - 0-59 minutes  
SS or ss = 0-59 seconds  
: for a colon  
All others for alpha numeric, (will be blank when displaying time)
##### Example ```HHMMSS ``` ```hh:MM:SS ``` #### displayMsg -- ssDisplayMessage Message to be displayed. If the message length exceeds the length of displayMask, the message will scroll at scrollSpd. To 'remove' a message or revert back to time, if timeEnabled is true, set the message to '~'. #### displayCfg -- ssDisplayConfig The order your LEDs are configured in. All segments in the display need to be wired the same way.
           -------
         /   A   /          0 - EDCGFAB
        / F     / B         1 - EDCBAFG
       /       /            2 - GCDEFAB
       -------              3 - GBAFEDC
     /   G   /              4 - FABGEDC
    / E     / C             5 - FABCDEG
   /       /
   -------
      D
## Version 20211009 - Initial release ================================================ FILE: usermods/seven_segment_display/seven_segment_display.cpp ================================================ #include "wled.h" #ifdef WLED_DISABLE_MQTT #error "This user mod requires MQTT to be enabled." #endif class SevenSegmentDisplay : public Usermod { #define WLED_SS_BUFFLEN 6 #define REFRESHTIME 497 private: //Runtime variables. unsigned long lastRefresh = 0; unsigned long lastCharacterStep = 0; String ssDisplayBuffer = ""; char ssCharacterMask[36] = {0x77, 0x11, 0x6B, 0x3B, 0x1D, 0x3E, 0x7E, 0x13, 0x7F, 0x1F, 0x5F, 0x7C, 0x66, 0x79, 0x6E, 0x4E, 0x76, 0x5D, 0x44, 0x71, 0x5E, 0x64, 0x27, 0x58, 0x77, 0x4F, 0x1F, 0x48, 0x3E, 0x6C, 0x75, 0x25, 0x7D, 0x2A, 0x3D, 0x6B}; int ssDisplayMessageIdx = 0; //Position of the start of the message to be physically displayed. bool ssDoDisplayTime = true; int ssVirtualDisplayMessageIdxStart = 0; int ssVirtualDisplayMessageIdxEnd = 0; unsigned long resfreshTime = 497; // set your config variables to their boot default value (this can also be done in readFromConfig() or a constructor if you prefer) int ssLEDPerSegment = 1; //The number of LEDs in each segment of the 7 seg (total per digit is 7 * ssLedPerSegment) int ssLEDPerPeriod = 1; //A Period will have 1x and a Colon will have 2x int ssStartLED = 0; //The pixel that the display starts at. /* HH - 0-23. hh - 1-12, kk - 1-24 hours // MM or mm - 0-59 minutes // SS or ss = 0-59 seconds // : for a colon // All others for alpha numeric, (will be blank when displaying time) */ String ssDisplayMask = "HHMMSS"; //Physical Display Mask, this should reflect physical equipment. /* ssDisplayConfig // ------- // / A / 0 - EDCGFAB // / F / B 1 - EDCBAFG // / / 2 - GCDEFAB // ------- 3 - GBAFEDC // / G / 4 - FABGEDC // / E / C 5 - FABCDEG // / / // ------- // D */ int ssDisplayConfig = 5; //Physical configuration of the Seven segment display String ssDisplayMessage = "~"; bool ssTimeEnabled = true; //If not, display message. unsigned int ssScrollSpeed = 1000; //Time between advancement of extended message scrolling, in milliseconds. //String to reduce flash memory usage static const char _str_perSegment[]; static const char _str_perPeriod[]; static const char _str_startIdx[]; static const char _str_displayCfg[]; static const char _str_timeEnabled[]; static const char _str_scrollSpd[]; static const char _str_displayMask[]; static const char _str_displayMsg[]; static const char _str_sevenSeg[]; static const char _str_subFormat[]; static const char _str_topicFormat[]; unsigned long _overlaySevenSegmentProcess() { //Do time for now. if (ssDoDisplayTime) { //Format the ssDisplayBuffer based on ssDisplayMask int displayMaskLen = static_cast(ssDisplayMask.length()); for (int index = 0; index < displayMaskLen; index++) { //Only look for time formatting if there are at least 2 characters left in the buffer. if ((index < displayMaskLen - 1) && (ssDisplayMask[index] == ssDisplayMask[index + 1])) { int timeVar = 0; switch (ssDisplayMask[index]) { case 'h': timeVar = hourFormat12(localTime); break; case 'H': timeVar = hour(localTime); break; case 'k': timeVar = hour(localTime) + 1; break; case 'M': case 'm': timeVar = minute(localTime); break; case 'S': case 's': timeVar = second(localTime); break; } //Only want to leave a blank in the hour formatting. if ((ssDisplayMask[index] == 'h' || ssDisplayMask[index] == 'H' || ssDisplayMask[index] == 'k') && timeVar < 10) ssDisplayBuffer[index] = ' '; else ssDisplayBuffer[index] = 0x30 + (timeVar / 10); ssDisplayBuffer[index + 1] = 0x30 + (timeVar % 10); //Need to increment the index because of the second digit. index++; } else { ssDisplayBuffer[index] = (ssDisplayMask[index] == ':' ? ':' : ' '); } } return REFRESHTIME; } else { /* This will handle displaying a message and the scrolling of the message if its longer than the buffer length */ //Check to see if the message has scrolled completely int len = static_cast(ssDisplayMessage.length()); if (ssDisplayMessageIdx > len) { //If it has scrolled the whole message, reset it. setSevenSegmentMessage(ssDisplayMessage); return REFRESHTIME; } //Display message int displayMaskLen = static_cast(ssDisplayMask.length()); for (int index = 0; index < displayMaskLen; index++) { if (ssDisplayMessageIdx + index < len && ssDisplayMessageIdx + index >= 0) ssDisplayBuffer[index] = ssDisplayMessage[ssDisplayMessageIdx + index]; else ssDisplayBuffer[index] = ' '; } //Increase the displayed message index to progress it one character if the length exceeds the display length. if (len > displayMaskLen) ssDisplayMessageIdx++; return ssScrollSpeed; } } void _overlaySevenSegmentDraw() { //Start pixels at ssStartLED, Use ssLEDPerSegment, ssLEDPerPeriod, ssDisplayBuffer int indexLED = ssStartLED; int displayMaskLen = static_cast(ssDisplayMask.length()); for (int indexBuffer = 0; indexBuffer < displayMaskLen; indexBuffer++) { if (ssDisplayBuffer[indexBuffer] == 0) break; else if (ssDisplayBuffer[indexBuffer] == '.') { //Won't ever turn off LED lights for a period. (or will we?) indexLED += ssLEDPerPeriod; continue; } else if (ssDisplayBuffer[indexBuffer] == ':') { //Turn off colon if odd second? indexLED += ssLEDPerPeriod * 2; } else if (ssDisplayBuffer[indexBuffer] == ' ') { //Turn off all 7 segments. _overlaySevenSegmentLEDOutput(0, indexLED); indexLED += ssLEDPerSegment * 7; } else { //Turn off correct segments. _overlaySevenSegmentLEDOutput(_overlaySevenSegmentGetCharMask(ssDisplayBuffer[indexBuffer]), indexLED); indexLED += ssLEDPerSegment * 7; } } } void _overlaySevenSegmentLEDOutput(char mask, int indexLED) { for (char index = 0; index < 7; index++) { if ((mask & (0x40 >> index)) != (0x40 >> index)) { for (int numPerSeg = 0; numPerSeg < ssLEDPerSegment; numPerSeg++) { strip.setPixelColor(indexLED + numPerSeg, 0x000000); } } indexLED += ssLEDPerSegment; } } char _overlaySevenSegmentGetCharMask(char var) { if (var >= 0x30 && var <= 0x39) { /*If its a number, shift to index 0.*/ var -= 0x30; } else if (var >= 0x41 && var <= 0x5a) { /*If its an Upper case, shift to index 0xA.*/ var -= 0x37; } else if (var >= 0x61 && var <= 0x7A) { /*If its a lower case, shift to index 0xA.*/ var -= 0x57; } else { /* Else unsupported, return 0; */ return 0; } char mask = ssCharacterMask[static_cast(var)]; /* 0 - EDCGFAB 1 - EDCBAFG 2 - GCDEFAB 3 - GBAFEDC 4 - FABGEDC 5 - FABCDEG */ switch (ssDisplayConfig) { case 1: mask = _overlaySevenSegmentSwapBits(mask, 0, 3, 1); mask = _overlaySevenSegmentSwapBits(mask, 1, 2, 1); break; case 2: mask = _overlaySevenSegmentSwapBits(mask, 3, 6, 1); mask = _overlaySevenSegmentSwapBits(mask, 4, 5, 1); break; case 3: mask = _overlaySevenSegmentSwapBits(mask, 0, 4, 3); mask = _overlaySevenSegmentSwapBits(mask, 3, 6, 1); mask = _overlaySevenSegmentSwapBits(mask, 4, 5, 1); break; case 4: mask = _overlaySevenSegmentSwapBits(mask, 0, 4, 3); break; case 5: mask = _overlaySevenSegmentSwapBits(mask, 0, 4, 3); mask = _overlaySevenSegmentSwapBits(mask, 0, 3, 1); mask = _overlaySevenSegmentSwapBits(mask, 1, 2, 1); break; } return mask; } char _overlaySevenSegmentSwapBits(char x, char p1, char p2, char n) { /* Move all bits of first set to rightmost side */ char set1 = (x >> p1) & ((1U << n) - 1); /* Move all bits of second set to rightmost side */ char set2 = (x >> p2) & ((1U << n) - 1); /* Xor the two sets */ char Xor = (set1 ^ set2); /* Put the Xor bits back to their original positions */ Xor = (Xor << p1) | (Xor << p2); /* Xor the 'Xor' with the original number so that the two sets are swapped */ char result = x ^ Xor; return result; } void _publishMQTTint_P(const char *subTopic, int value) { if(mqtt == NULL) return; char buffer[64]; char valBuffer[12]; sprintf_P(buffer, PSTR("%s/%S/%S"), mqttDeviceTopic, _str_sevenSeg, subTopic); sprintf_P(valBuffer, PSTR("%d"), value); mqtt->publish(buffer, 2, true, valBuffer); } void _publishMQTTstr_P(const char *subTopic, String Value) { if(mqtt == NULL) return; char buffer[64]; sprintf_P(buffer, PSTR("%s/%S/%S"), mqttDeviceTopic, _str_sevenSeg, subTopic); mqtt->publish(buffer, 2, true, Value.c_str(), Value.length()); } void _updateMQTT() { _publishMQTTint_P(_str_perSegment, ssLEDPerSegment); _publishMQTTint_P(_str_perPeriod, ssLEDPerPeriod); _publishMQTTint_P(_str_startIdx, ssStartLED); _publishMQTTint_P(_str_displayCfg, ssDisplayConfig); _publishMQTTint_P(_str_timeEnabled, ssTimeEnabled); _publishMQTTint_P(_str_scrollSpd, ssScrollSpeed); _publishMQTTstr_P(_str_displayMask, ssDisplayMask); _publishMQTTstr_P(_str_displayMsg, ssDisplayMessage); } bool _cmpIntSetting_P(char *topic, char *payload, const char *setting, void *value) { if (strcmp_P(topic, setting) == 0) { *((int *)value) = strtol(payload, NULL, 10); _publishMQTTint_P(setting, *((int *)value)); return true; } return false; } bool _handleSetting(char *topic, char *payload) { if (_cmpIntSetting_P(topic, payload, _str_perSegment, &ssLEDPerSegment)) return true; if (_cmpIntSetting_P(topic, payload, _str_perPeriod, &ssLEDPerPeriod)) return true; if (_cmpIntSetting_P(topic, payload, _str_startIdx, &ssStartLED)) return true; if (_cmpIntSetting_P(topic, payload, _str_displayCfg, &ssDisplayConfig)) return true; if (_cmpIntSetting_P(topic, payload, _str_timeEnabled, &ssTimeEnabled)) return true; if (_cmpIntSetting_P(topic, payload, _str_scrollSpd, &ssScrollSpeed)) return true; if (strcmp_P(topic, _str_displayMask) == 0) { ssDisplayMask = String(payload); ssDisplayBuffer = ssDisplayMask; _publishMQTTstr_P(_str_displayMask, ssDisplayMask); return true; } if (strcmp_P(topic, _str_displayMsg) == 0) { setSevenSegmentMessage(String(payload)); return true; } return false; } public: void setSevenSegmentMessage(String message) { //If the message isn't blank display it otherwise show time, if enabled. if (message.length() < 1 || message == "~") ssDoDisplayTime = ssTimeEnabled; else ssDoDisplayTime = false; //Determine is the message is longer than the display, if it is configure it to scroll the message. if (message.length() > ssDisplayMask.length()) ssDisplayMessageIdx = -ssDisplayMask.length(); else ssDisplayMessageIdx = 0; //If the message isn't the same, update runtime/mqtt (most calls will be resetting message scroll) if (!ssDisplayMessage.equals(message)) { _publishMQTTstr_P(_str_displayMsg, message); ssDisplayMessage = message; } } //Functions called by WLED /* * 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() { ssDisplayBuffer = ssDisplayMask; } /* * loop() is called continuously. Here you can check for events, read sensors, etc. */ void loop() { if (millis() - lastRefresh > resfreshTime) { //In theory overlaySevenSegmentProcess should return the amount of time until it changes next. //So we should be okay to trigger the stripi on every process loop. resfreshTime = _overlaySevenSegmentProcess(); lastRefresh = millis(); strip.trigger(); } } void handleOverlayDraw() { _overlaySevenSegmentDraw(); } void onMqttConnect(bool sessionPresent) { char subBuffer[48]; if (mqttDeviceTopic[0] != 0) { _updateMQTT(); //subscribe for sevenseg messages on the device topic sprintf_P(subBuffer, PSTR("%s/%S/+/set"), mqttDeviceTopic, _str_sevenSeg); mqtt->subscribe(subBuffer, 2); } if (mqttGroupTopic[0] != 0) { //subscribe for sevenseg messages on the group topic sprintf_P(subBuffer, PSTR("%s/%S/+/set"), mqttGroupTopic, _str_sevenSeg); mqtt->subscribe(subBuffer, 2); } } bool onMqttMessage(char *topic, char *payload) { //If topic beings with sevenSeg cut it off, otherwise not our message. size_t topicPrefixLen = strlen_P(PSTR("/sevenSeg/")); if (strncmp_P(topic, PSTR("/sevenSeg/"), topicPrefixLen) == 0) topic += topicPrefixLen; else return false; //We only care if the topic ends with /set size_t topicLen = strlen(topic); if (topicLen > 4 && topic[topicLen - 4] == '/' && topic[topicLen - 3] == 's' && topic[topicLen - 2] == 'e' && topic[topicLen - 1] == 't') { //Trim /set and handle it topic[topicLen - 4] = '\0'; _handleSetting(topic, payload); } return true; } void addToConfig(JsonObject &root) { JsonObject top = root[FPSTR(_str_sevenSeg)]; if (top.isNull()) { top = root.createNestedObject(FPSTR(_str_sevenSeg)); } top[FPSTR(_str_perSegment)] = ssLEDPerSegment; top[FPSTR(_str_perPeriod)] = ssLEDPerPeriod; top[FPSTR(_str_startIdx)] = ssStartLED; top[FPSTR(_str_displayMask)] = ssDisplayMask; top[FPSTR(_str_displayCfg)] = ssDisplayConfig; top[FPSTR(_str_displayMsg)] = ssDisplayMessage; top[FPSTR(_str_timeEnabled)] = ssTimeEnabled; top[FPSTR(_str_scrollSpd)] = ssScrollSpeed; } bool readFromConfig(JsonObject &root) { JsonObject top = root[FPSTR(_str_sevenSeg)]; bool configComplete = !top.isNull(); //if sevenseg section doesn't exist return if (!configComplete) return configComplete; configComplete &= getJsonValue(top[FPSTR(_str_perSegment)], ssLEDPerSegment); configComplete &= getJsonValue(top[FPSTR(_str_perPeriod)], ssLEDPerPeriod); configComplete &= getJsonValue(top[FPSTR(_str_startIdx)], ssStartLED); configComplete &= getJsonValue(top[FPSTR(_str_displayMask)], ssDisplayMask); configComplete &= getJsonValue(top[FPSTR(_str_displayCfg)], ssDisplayConfig); String newDisplayMessage; configComplete &= getJsonValue(top[FPSTR(_str_displayMsg)], newDisplayMessage); setSevenSegmentMessage(newDisplayMessage); configComplete &= getJsonValue(top[FPSTR(_str_timeEnabled)], ssTimeEnabled); configComplete &= getJsonValue(top[FPSTR(_str_scrollSpd)], ssScrollSpeed); return configComplete; } /* * 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_SEVEN_SEGMENT_DISPLAY; } }; const char SevenSegmentDisplay::_str_perSegment[] PROGMEM = "perSegment"; const char SevenSegmentDisplay::_str_perPeriod[] PROGMEM = "perPeriod"; const char SevenSegmentDisplay::_str_startIdx[] PROGMEM = "startIdx"; const char SevenSegmentDisplay::_str_displayCfg[] PROGMEM = "displayCfg"; const char SevenSegmentDisplay::_str_timeEnabled[] PROGMEM = "timeEnabled"; const char SevenSegmentDisplay::_str_scrollSpd[] PROGMEM = "scrollSpd"; const char SevenSegmentDisplay::_str_displayMask[] PROGMEM = "displayMask"; const char SevenSegmentDisplay::_str_displayMsg[] PROGMEM = "displayMsg"; const char SevenSegmentDisplay::_str_sevenSeg[] PROGMEM = "sevenSeg"; static SevenSegmentDisplay seven_segment_display; REGISTER_USERMOD(seven_segment_display); ================================================ FILE: usermods/seven_segment_display_reloaded/library.json ================================================ { "name": "seven_segment_display_reloaded", "build": { "libArchive": false, "extraScript": "setup_deps.py" } } ================================================ FILE: usermods/seven_segment_display_reloaded/readme.md ================================================ # Seven Segment Display Reloaded Uses the overlay feature to create a configurable seven segment display. Optimized for maximum configurability and use with seven segment clocks by parallyze (https://www.instructables.com/member/parallyze/instructables/) Very loosely based on the existing usermod "seven segment display". ## Installation Add the compile-time option `-D USERMOD_SSDR` to your `platformio.ini` (or `platformio_override.ini`) or use `#define USERMOD_SSDR` in `my_config.h`. For the auto brightness option, the usermod SN_Photoresistor or BH1750_V2 has to be installed as well. See SN_Photoresistor/readme.md or BH1750_V2/readme.md for instructions. ## Settings All settings can be controlled via the usermod settings page. Part of the settings can be controlled through MQTT with a raw payload or through a json request to /json/state. ### enabled Enables/disables this usermod ### inverted Enables the inverted mode in which the background should be enabled and the digits should be black (LEDs off) ### Colon-blinking Enables the blinking colon(s) if they are defined ### Leading-Zero Shows the leading zero of the hour if it exists (i.e. shows `07` instead of `7`) ### enable-auto-brightness Enables the auto brightness feature. Can be used only when the usermods SN_Photoresistor or BH1750_V2 are installed. ### auto-brightness-min / auto-brightness-max The lux value calculated from usermod SN_Photoresistor or BH1750_V2 will be mapped to the values defined here. The mapping, 0 - 1000 lux, will be mapped to auto-brightness-min and auto-brightness-max WLED current protection will override the calculated value if it is too high. ### Display-Mask Defines the type of the time/date display. For example "H:m" (default) - H - 00-23 hours - h - 01-12 hours - k - 01-24 hours - m - 00-59 minutes - s - 00-59 seconds - d - 01-31 day of month - M - 01-12 month - y - 21 last two positions of year - Y - 2021 year - : for a colon ### LED-Numbers - LED-Numbers-Hours - LED-Numbers-Minutes - LED-Numbers-Seconds - LED-Numbers-Colons - LED-Numbers-Day - LED-Numbers-Month - LED-Numbers-Year See following example for usage. ## Example Example of an LED definition: ``` < A > /\ /\ F B \/ \/ < G > /\ /\ E C \/ \/ < D > ``` LEDs or Range of LEDs are separated by a comma "," Segments are separated by a semicolon ";" and are read as A;B;C;D;E;F;G Digits are separated by colon ":" -> A;B;C;D;E;F;G:A;B;C;D;E;F;G Ranges are defined as lower to higher (lower first) For example, a clock definition for the following clock (https://www.instructables.com/Lazy-7-Quick-Build-Edition/) is - hour "59,46;47-48;50-51;52-53;54-55;57-58;49,56:0,13;1-2;4-5;6-7;8-9;11-12;3,10" - minute "37-38;39-40;42-43;44,31;32-33;35-36;34,41:21-22;23-24;26-27;28,15;16-17;19-20;18,25" or - hour "6,7;8,9;11,12;13,0;1,2;4,5;3,10:52,53;54,55;57,58;59,46;47,48;50,51;49,56" - minute "15,28;16,17;19,20;21,22;23,24;26,27;18,25:31,44;32,33;35,36;37,38;39,40;42,43;34,41" depending on the orientation. # Example details: hour "59,46;47-48;50-51;52-53;54-55;57-58;49,56:0,13;1-2;4-5;6-7;8-9;11-12;3,10" there are two digits separated by ":" - 59,46;47-48;50-51;52-53;54-55;57-58;49,56 - 0,13;1-2;4-5;6-7;8-9;11-12;3,10 In the first digit, the **segment A** consists of the LEDs number **59 and 46**., **segment B** consists of the LEDs number **47, 48** and so on The second digit starts again with **segment A** and LEDs **0 and 13**, **segment B** consists of the LEDs number **1 and 2** and so on ### first digit of the hour - Segment A: 59, 46 - Segment B: 47, 48 - Segment C: 50, 51 - Segment D: 52, 53 - Segment E: 54, 55 - Segment F: 57, 58 - Segment G: 49, 56 ### second digit of the hour - Segment A: 0, 13 - Segment B: 1, 2 - Segment C: 4, 5 - Segment D: 6, 7 - Segment E: 8, 9 - Segment F: 11, 12 - Segment G: 3, 10 ================================================ FILE: usermods/seven_segment_display_reloaded/setup_deps.py ================================================ from platformio.package.meta import PackageSpec Import('env') libs = [PackageSpec(lib).name for lib in env.GetProjectOption("lib_deps",[])] # Check for partner usermods if "SN_Photoresistor" in libs: env.Append(CPPDEFINES=[("USERMOD_SN_PHOTORESISTOR")]) if any(mod in ("BH1750_v2", "BH1750") for mod in libs): env.Append(CPPDEFINES=[("USERMOD_BH1750")]) ================================================ FILE: usermods/seven_segment_display_reloaded/seven_segment_display_reloaded.cpp ================================================ #include "wled.h" #ifdef USERMOD_SN_PHOTORESISTOR #include "SN_Photoresistor.h" #endif #ifdef USERMOD_BH1750 #include "BH1750_v2.h" #endif #ifdef WLED_DISABLE_MQTT #error "This user mod requires MQTT to be enabled." #endif class UsermodSSDR : public Usermod { //#define REFRESHTIME 497 private: //Runtime variables. unsigned long umSSDRLastRefresh = 0; unsigned long umSSDRResfreshTime = 3000; bool umSSDRDisplayTime = false; bool umSSDRInverted = false; bool umSSDRColonblink = true; bool umSSDRLeadingZero = false; bool umSSDREnableLDR = false; String umSSDRHours = ""; String umSSDRMinutes = ""; String umSSDRSeconds = ""; String umSSDRColons = ""; String umSSDRDays = ""; String umSSDRMonths = ""; String umSSDRYears = ""; uint16_t umSSDRLength = 0; uint16_t umSSDRBrightnessMin = 0; uint16_t umSSDRBrightnessMax = 255; bool* umSSDRMask = 0; /*// H - 00-23 hours // h - 01-12 hours // k - 01-24 hours // m - 00-59 minutes // s - 00-59 seconds // d - 01-31 day of month // M - 01-12 month // y - 21 last two positions of year // Y - 2021 year // : for a colon */ String umSSDRDisplayMask = "H:m"; //This should reflect physical equipment. /* Segment order, seen from the front: < A > /\ /\ F B \/ \/ < G > /\ /\ E C \/ \/ < D > */ uint8_t umSSDRNumbers[11][7] = { // A B C D E F G { 1, 1, 1, 1, 1, 1, 0 }, // 0 { 0, 1, 1, 0, 0, 0, 0 }, // 1 { 1, 1, 0, 1, 1, 0, 1 }, // 2 { 1, 1, 1, 1, 0, 0, 1 }, // 3 { 0, 1, 1, 0, 0, 1, 1 }, // 4 { 1, 0, 1, 1, 0, 1, 1 }, // 5 { 1, 0, 1, 1, 1, 1, 1 }, // 6 { 1, 1, 1, 0, 0, 0, 0 }, // 7 { 1, 1, 1, 1, 1, 1, 1 }, // 8 { 1, 1, 1, 1, 0, 1, 1 }, // 9 { 0, 0, 0, 0, 0, 0, 0 } // blank }; //String to reduce flash memory usage static const char _str_name[]; static const char _str_ldrEnabled[]; static const char _str_timeEnabled[]; static const char _str_inverted[]; static const char _str_colonblink[]; static const char _str_leadingZero[]; static const char _str_displayMask[]; static const char _str_hours[]; static const char _str_minutes[]; static const char _str_seconds[]; static const char _str_colons[]; static const char _str_days[]; static const char _str_months[]; static const char _str_years[]; static const char _str_minBrightness[]; static const char _str_maxBrightness[]; #ifdef USERMOD_SN_PHOTORESISTOR Usermod_SN_Photoresistor *ptr; #else void* ptr = nullptr; #endif #ifdef USERMOD_BH1750 Usermod_BH1750* bh1750 = nullptr; #else void* bh1750 = nullptr; #endif void _overlaySevenSegmentDraw() { int displayMaskLen = static_cast(umSSDRDisplayMask.length()); bool colonsDone = false; _setAllFalse(); for (int index = 0; index < displayMaskLen; index++) { int timeVar = 0; switch (umSSDRDisplayMask[index]) { case 'h': timeVar = hourFormat12(localTime); _showElements(&umSSDRHours, timeVar, 0, !umSSDRLeadingZero); break; case 'H': timeVar = hour(localTime); _showElements(&umSSDRHours, timeVar, 0, !umSSDRLeadingZero); break; case 'k': timeVar = hour(localTime) + 1; _showElements(&umSSDRHours, timeVar, 0, !umSSDRLeadingZero); break; case 'm': timeVar = minute(localTime); _showElements(&umSSDRMinutes, timeVar, 0, 0); break; case 's': timeVar = second(localTime); _showElements(&umSSDRSeconds, timeVar, 0, 0); break; case 'd': timeVar = day(localTime); _showElements(&umSSDRDays, timeVar, 0, 0); break; case 'M': timeVar = month(localTime); _showElements(&umSSDRMonths, timeVar, 0, 0); break; case 'y': timeVar = second(localTime); _showElements(&umSSDRYears, timeVar, 0, 0); break; case 'Y': timeVar = year(localTime); _showElements(&umSSDRYears, timeVar, 0, 0); break; case ':': if (!colonsDone) { // only call _setColons once as all colons are printed when the first colon is found _setColons(); colonsDone = true; } break; } } _setMaskToLeds(); } void _setColons() { if ( umSSDRColonblink ) { if ( second(localTime) % 2 == 0 ) { _showElements(&umSSDRColons, 0, 1, 0); } } else { _showElements(&umSSDRColons, 0, 1, 0); } } void _showElements(String *map, int timevar, bool isColon, bool removeZero ) { if ((map != nullptr) && (*map != nullptr) && !(*map).equals("")) { int length = String(timevar).length(); bool addZero = false; if (length == 1) { length = 2; addZero = true; } int timeArr[length]; if(addZero) { if(removeZero) { timeArr[1] = 10; timeArr[0] = timevar; } else { timeArr[1] = 0; timeArr[0] = timevar; } } else { int count = 0; while (timevar) { timeArr[count] = timevar%10; timevar /= 10; count++; }; } int colonsLen = static_cast((*map).length()); int count = 0; int countSegments = 0; int countDigit = 0; bool range = false; int lastSeenLedNr = 0; for (int index = 0; index < colonsLen; index++) { switch ((*map)[index]) { case '-': lastSeenLedNr = _checkForNumber(count, index, map); count = 0; range = true; break; case ':': _setLeds(_checkForNumber(count, index, map), lastSeenLedNr, range, countSegments, timeArr[countDigit], isColon); count = 0; range = false; countDigit++; countSegments = 0; break; case ';': _setLeds(_checkForNumber(count, index, map), lastSeenLedNr, range, countSegments, timeArr[countDigit], isColon); count = 0; range = false; countSegments++; break; case ',': _setLeds(_checkForNumber(count, index, map), lastSeenLedNr, range, countSegments, timeArr[countDigit], isColon); count = 0; range = false; break; default: count++; break; } } _setLeds(_checkForNumber(count, colonsLen, map), lastSeenLedNr, range, countSegments, timeArr[countDigit], isColon); } } void _setLeds(int lednr, int lastSeenLedNr, bool range, int countSegments, int number, bool colon) { if ((lednr < 0) || (lednr >= umSSDRLength)) return; // prevent array bounds violation if (!(colon && umSSDRColonblink) && ((number < 0) || (countSegments < 0))) return; if ((colon && umSSDRColonblink) || umSSDRNumbers[number][countSegments]) { if (range) { for(int i = max(0, lastSeenLedNr); i <= lednr; i++) { umSSDRMask[i] = true; } } else { umSSDRMask[lednr] = true; } } } void _setMaskToLeds() { for(int i = 0; i <= umSSDRLength; i++) { if ((!umSSDRInverted && !umSSDRMask[i]) || (umSSDRInverted && umSSDRMask[i])) { strip.setPixelColor(i, 0x000000); } } } void _setAllFalse() { for(int i = 0; i <= umSSDRLength; i++) { umSSDRMask[i] = false; } } int _checkForNumber(int count, int index, String *map) { String number = (*map).substring(index - count, index); return number.toInt(); } void _publishMQTTint_P(const char *subTopic, int value) { if(mqtt == NULL) return; char buffer[64]; char valBuffer[12]; sprintf_P(buffer, PSTR("%s/%S/%S"), mqttDeviceTopic, _str_name, subTopic); sprintf_P(valBuffer, PSTR("%d"), value); mqtt->publish(buffer, 2, true, valBuffer); } void _publishMQTTstr_P(const char *subTopic, String Value) { if(mqtt == NULL) return; char buffer[64]; sprintf_P(buffer, PSTR("%s/%S/%S"), mqttDeviceTopic, _str_name, subTopic); mqtt->publish(buffer, 2, true, Value.c_str(), Value.length()); } bool _cmpIntSetting_P(char *topic, char *payload, const char *setting, void *value) { if (strcmp_P(topic, setting) == 0) { *((int *)value) = strtol(payload, NULL, 10); _publishMQTTint_P(setting, *((int *)value)); return true; } return false; } bool _handleSetting(char *topic, char *payload) { if (_cmpIntSetting_P(topic, payload, _str_timeEnabled, &umSSDRDisplayTime)) { return true; } if (_cmpIntSetting_P(topic, payload, _str_ldrEnabled, &umSSDREnableLDR)) { return true; } if (_cmpIntSetting_P(topic, payload, _str_inverted, &umSSDRInverted)) { return true; } if (_cmpIntSetting_P(topic, payload, _str_colonblink, &umSSDRColonblink)) { return true; } if (_cmpIntSetting_P(topic, payload, _str_leadingZero, &umSSDRLeadingZero)) { return true; } if (strcmp_P(topic, _str_displayMask) == 0) { umSSDRDisplayMask = String(payload); _publishMQTTstr_P(_str_displayMask, umSSDRDisplayMask); return true; } return false; } void _updateMQTT() { _publishMQTTint_P(_str_timeEnabled, umSSDRDisplayTime); _publishMQTTint_P(_str_ldrEnabled, umSSDREnableLDR); _publishMQTTint_P(_str_inverted, umSSDRInverted); _publishMQTTint_P(_str_colonblink, umSSDRColonblink); _publishMQTTint_P(_str_leadingZero, umSSDRLeadingZero); _publishMQTTstr_P(_str_hours, umSSDRHours); _publishMQTTstr_P(_str_minutes, umSSDRMinutes); _publishMQTTstr_P(_str_seconds, umSSDRSeconds); _publishMQTTstr_P(_str_colons, umSSDRColons); _publishMQTTstr_P(_str_days, umSSDRDays); _publishMQTTstr_P(_str_months, umSSDRMonths); _publishMQTTstr_P(_str_years, umSSDRYears); _publishMQTTstr_P(_str_displayMask, umSSDRDisplayMask); _publishMQTTint_P(_str_minBrightness, umSSDRBrightnessMin); _publishMQTTint_P(_str_maxBrightness, umSSDRBrightnessMax); } void _addJSONObject(JsonObject& root) { JsonObject ssdrObj = root[FPSTR(_str_name)]; if (ssdrObj.isNull()) { ssdrObj = root.createNestedObject(FPSTR(_str_name)); } ssdrObj[FPSTR(_str_timeEnabled)] = umSSDRDisplayTime; ssdrObj[FPSTR(_str_ldrEnabled)] = umSSDREnableLDR; ssdrObj[FPSTR(_str_inverted)] = umSSDRInverted; ssdrObj[FPSTR(_str_colonblink)] = umSSDRColonblink; ssdrObj[FPSTR(_str_leadingZero)] = umSSDRLeadingZero; ssdrObj[FPSTR(_str_displayMask)] = umSSDRDisplayMask; ssdrObj[FPSTR(_str_hours)] = umSSDRHours; ssdrObj[FPSTR(_str_minutes)] = umSSDRMinutes; ssdrObj[FPSTR(_str_seconds)] = umSSDRSeconds; ssdrObj[FPSTR(_str_colons)] = umSSDRColons; ssdrObj[FPSTR(_str_days)] = umSSDRDays; ssdrObj[FPSTR(_str_months)] = umSSDRMonths; ssdrObj[FPSTR(_str_years)] = umSSDRYears; ssdrObj[FPSTR(_str_minBrightness)] = umSSDRBrightnessMin; ssdrObj[FPSTR(_str_maxBrightness)] = umSSDRBrightnessMax; } public: //Functions called by WLED /* * 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() { umSSDRLength = strip.getLengthTotal(); if (umSSDRMask != 0) { umSSDRMask = (bool*) realloc(umSSDRMask, umSSDRLength * sizeof(bool)); } else { umSSDRMask = (bool*) malloc(umSSDRLength * sizeof(bool)); } _setAllFalse(); #ifdef USERMOD_SN_PHOTORESISTOR ptr = (Usermod_SN_Photoresistor*) UsermodManager::lookup(USERMOD_ID_SN_PHOTORESISTOR); #endif #ifdef USERMOD_BH1750 bh1750 = (Usermod_BH1750*) UsermodManager::lookup(USERMOD_ID_BH1750); #endif DEBUG_PRINTLN(F("Setup done")); } /* * loop() is called continuously. Here you can check for events, read sensors, etc. */ void loop() { if (!umSSDRDisplayTime || strip.isUpdating()) { return; } #ifdef USERMOD_SN_PHOTORESISTOR if(bri != 0 && umSSDREnableLDR && (millis() - umSSDRLastRefresh > umSSDRResfreshTime)) { if (ptr != nullptr) { uint16_t lux = ptr->getLastLDRValue(); uint16_t brightness = map(lux, 0, 1000, umSSDRBrightnessMin, umSSDRBrightnessMax); if (bri != brightness) { bri = brightness; stateUpdated(1); } } umSSDRLastRefresh = millis(); } #endif #ifdef USERMOD_BH1750 if(bri != 0 && umSSDREnableLDR && (millis() - umSSDRLastRefresh > umSSDRResfreshTime)) { if (bh1750 != nullptr) { float lux = bh1750->getIlluminance(); uint16_t brightness = map(lux, 0, 1000, umSSDRBrightnessMin, umSSDRBrightnessMax); if (bri != brightness) { DEBUG_PRINTF("Adjusting brightness based on lux value: %.2f lx, new brightness: %d\n", lux, brightness); bri = brightness; stateUpdated(1); } } umSSDRLastRefresh = millis(); } #endif } void handleOverlayDraw() { if (umSSDRDisplayTime) { _overlaySevenSegmentDraw(); } } /* * 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) { JsonObject user = root[F("u")]; if (user.isNull()) { user = root.createNestedObject(F("u")); } JsonArray enabled = user.createNestedArray("Time enabled"); enabled.add(umSSDRDisplayTime); JsonArray invert = user.createNestedArray("Time inverted"); invert.add(umSSDRInverted); JsonArray blink = user.createNestedArray("Blinking colon"); blink.add(umSSDRColonblink); JsonArray zero = user.createNestedArray("Show the hour leading zero"); zero.add(umSSDRLeadingZero); JsonArray ldrEnable = user.createNestedArray("Auto Brightness enabled"); ldrEnable.add(umSSDREnableLDR); } /* * 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) { JsonObject user = root[F("u")]; if (user.isNull()) { user = root.createNestedObject(F("u")); } _addJSONObject(user); } /* * 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) { JsonObject user = root[F("u")]; if (!user.isNull()) { JsonObject ssdrObj = user[FPSTR(_str_name)]; umSSDRDisplayTime = ssdrObj[FPSTR(_str_timeEnabled)] | umSSDRDisplayTime; umSSDREnableLDR = ssdrObj[FPSTR(_str_ldrEnabled)] | umSSDREnableLDR; umSSDRInverted = ssdrObj[FPSTR(_str_inverted)] | umSSDRInverted; umSSDRColonblink = ssdrObj[FPSTR(_str_colonblink)] | umSSDRColonblink; umSSDRLeadingZero = ssdrObj[FPSTR(_str_leadingZero)] | umSSDRLeadingZero; umSSDRDisplayMask = ssdrObj[FPSTR(_str_displayMask)] | umSSDRDisplayMask; } } void onMqttConnect(bool sessionPresent) { char subBuffer[48]; if (mqttDeviceTopic[0] != 0) { _updateMQTT(); //subscribe for sevenseg messages on the device topic sprintf_P(subBuffer, PSTR("%s/%S/+/set"), mqttDeviceTopic, _str_name); mqtt->subscribe(subBuffer, 2); } if (mqttGroupTopic[0] != 0) { //subscribe for sevenseg messages on the group topic sprintf_P(subBuffer, PSTR("%s/%S/+/set"), mqttGroupTopic, _str_name); mqtt->subscribe(subBuffer, 2); } } bool onMqttMessage(char *topic, char *payload) { //If topic begins with sevenSeg cut it off, otherwise not our message. size_t topicPrefixLen = strlen_P(PSTR("/wledSS/")); if (strncmp_P(topic, PSTR("/wledSS/"), topicPrefixLen) == 0) { topic += topicPrefixLen; } else { return false; } //We only care if the topic ends with /set size_t topicLen = strlen(topic); if (topicLen > 4 && topic[topicLen - 4] == '/' && topic[topicLen - 3] == 's' && topic[topicLen - 2] == 'e' && topic[topicLen - 1] == 't') { //Trim /set and handle it topic[topicLen - 4] = '\0'; _handleSetting(topic, payload); } return true; } void addToConfig(JsonObject &root) { _addJSONObject(root); } bool readFromConfig(JsonObject &root) { JsonObject top = root[FPSTR(_str_name)]; if (top.isNull()) { DEBUG_PRINT(FPSTR(_str_name)); DEBUG_PRINTLN(F(": No config found. (Using defaults.)")); return false; } umSSDRDisplayTime = (top[FPSTR(_str_timeEnabled)] | umSSDRDisplayTime); umSSDREnableLDR = (top[FPSTR(_str_ldrEnabled)] | umSSDREnableLDR); umSSDRInverted = (top[FPSTR(_str_inverted)] | umSSDRInverted); umSSDRColonblink = (top[FPSTR(_str_colonblink)] | umSSDRColonblink); umSSDRLeadingZero = (top[FPSTR(_str_leadingZero)] | umSSDRLeadingZero); umSSDRDisplayMask = top[FPSTR(_str_displayMask)] | umSSDRDisplayMask; umSSDRHours = top[FPSTR(_str_hours)] | umSSDRHours; umSSDRMinutes = top[FPSTR(_str_minutes)] | umSSDRMinutes; umSSDRSeconds = top[FPSTR(_str_seconds)] | umSSDRSeconds; umSSDRColons = top[FPSTR(_str_colons)] | umSSDRColons; umSSDRDays = top[FPSTR(_str_days)] | umSSDRDays; umSSDRMonths = top[FPSTR(_str_months)] | umSSDRMonths; umSSDRYears = top[FPSTR(_str_years)] | umSSDRYears; umSSDRBrightnessMin = top[FPSTR(_str_minBrightness)] | umSSDRBrightnessMin; umSSDRBrightnessMax = top[FPSTR(_str_maxBrightness)] | umSSDRBrightnessMax; DEBUG_PRINT(FPSTR(_str_name)); DEBUG_PRINTLN(F(" config (re)loaded.")); return true; } /* * 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_SSDR; } }; const char UsermodSSDR::_str_name[] PROGMEM = "UsermodSSDR"; const char UsermodSSDR::_str_timeEnabled[] PROGMEM = "enabled"; const char UsermodSSDR::_str_inverted[] PROGMEM = "inverted"; const char UsermodSSDR::_str_colonblink[] PROGMEM = "Colon-blinking"; const char UsermodSSDR::_str_leadingZero[] PROGMEM = "Leading-Zero"; const char UsermodSSDR::_str_displayMask[] PROGMEM = "Display-Mask"; const char UsermodSSDR::_str_hours[] PROGMEM = "LED-Numbers-Hours"; const char UsermodSSDR::_str_minutes[] PROGMEM = "LED-Numbers-Minutes"; const char UsermodSSDR::_str_seconds[] PROGMEM = "LED-Numbers-Seconds"; const char UsermodSSDR::_str_colons[] PROGMEM = "LED-Numbers-Colons"; const char UsermodSSDR::_str_days[] PROGMEM = "LED-Numbers-Day"; const char UsermodSSDR::_str_months[] PROGMEM = "LED-Numbers-Month"; const char UsermodSSDR::_str_years[] PROGMEM = "LED-Numbers-Year"; const char UsermodSSDR::_str_ldrEnabled[] PROGMEM = "enable-auto-brightness"; const char UsermodSSDR::_str_minBrightness[] PROGMEM = "auto-brightness-min"; const char UsermodSSDR::_str_maxBrightness[] PROGMEM = "auto-brightness-max"; static UsermodSSDR seven_segment_display_reloaded; REGISTER_USERMOD(seven_segment_display_reloaded); ================================================ FILE: usermods/sht/ShtUsermod.h ================================================ #pragma once #include "wled.h" #ifdef WLED_DISABLE_MQTT #error "This user mod requires MQTT to be enabled." #endif #define USERMOD_SHT_TYPE_SHT30 0 #define USERMOD_SHT_TYPE_SHT31 1 #define USERMOD_SHT_TYPE_SHT35 2 #define USERMOD_SHT_TYPE_SHT85 3 class SHT; class ShtUsermod : public Usermod { private: bool enabled = false; // Is usermod enabled or not bool firstRunDone = false; // Remembers if the first config load run had been done bool initDone = false; // Remembers if the mod has been completely initialised bool haMqttDiscovery = false; // Is MQTT discovery enabled or not bool haMqttDiscoveryDone = false; // Remembers if we already published the HA discovery topics // SHT vars SHT *shtTempHumidSensor = nullptr; // Instance of SHT lib byte shtType = 0; // SHT sensor type to be used. Default: SHT30 byte unitOfTemp = 0; // Temperature unit to be used. Default: Celsius (0 = Celsius, 1 = Fahrenheit) bool shtInitDone = false; // Remembers if SHT sensor has been initialised bool shtReadDataSuccess = false; // Did we have a successful data read and is a valid temperature and humidity available? const byte shtI2cAddress = 0x44; // i2c address of the sensor. 0x44 is the default for all SHT sensors. Change this, if needed unsigned long shtLastTimeUpdated = 0; // Remembers when we read data the last time bool shtDataRequested = false; // Reading data is done async. This remembers if we asked the sensor to read data float shtCurrentTempC = 0.0f; // Last read temperature in Celsius float shtCurrentHumidity = 0.0f; // Last read humidity in RH% void initShtTempHumiditySensor(); void cleanupShtTempHumiditySensor(); void cleanup(); inline bool isShtReady() { return shtInitDone; } // Checks if the SHT sensor has been initialised. void publishTemperatureAndHumidityViaMqtt(); void publishHomeAssistantAutodiscovery(); void appendDeviceToMqttDiscoveryMessage(JsonDocument& root); public: // Strings to reduce flash memory usage (used more than twice) static const char _name[]; static const char _enabled[]; static const char _shtType[]; static const char _unitOfTemp[]; static const char _haMqttDiscovery[]; void setup(); void loop(); void onMqttConnect(bool sessionPresent); void appendConfigData(); void addToConfig(JsonObject &root); bool readFromConfig(JsonObject &root); void addToJsonInfo(JsonObject& root); bool isEnabled() { return enabled; } float getTemperature(); float getTemperatureC() { return roundf(shtCurrentTempC * 10.0f) / 10.0f; } float getTemperatureF() { return (getTemperatureC() * 1.8f) + 32.0f; } float getHumidity() { return roundf(shtCurrentHumidity * 10.0f) / 10.0f; } const char* getUnitString(); uint16_t getId() { return USERMOD_ID_SHT; } }; ================================================ FILE: usermods/sht/library.json ================================================ { "name": "sht", "build": { "libArchive": false }, "dependencies": { "robtillaart/SHT85": "~0.3.3" } } ================================================ FILE: usermods/sht/readme.md ================================================ # SHT Usermod to support various SHT i2c sensors like the SHT30, SHT31, SHT35 and SHT85 ## Requirements * "SHT85" by Rob Tillaart, v0.2 or higher: ## Usermod installation Simply copy the below block (build task) to your `platformio_override.ini` and compile WLED using this new build task. Or use an existing one, add the custom_usermod `sht`. ESP32: ```ini [env:custom_esp32dev_usermod_sht] extends = env:esp32dev custom_usermods = ${env:esp32dev.custom_usermods} sht ``` ESP8266: ```ini [env:custom_d1_mini_usermod_sht] extends = env:d1_mini custom_usermods = ${env:d1_mini.custom_usermods} sht ``` ## MQTT Discovery for Home Assistant If you're using Home Assistant and want to have the temperature and humidity available as entities in HA, you can tick the "Add-To-Home-Assistant-MQTT-Discovery" option in the usermod settings. If you have an MQTT broker configured under "Sync Settings" and it is connected, the mod will publish the auto discovery message to your broker and HA will instantly find it and create an entity each for the temperature and humidity. ### Publishing readings via MQTT Regardless of having MQTT discovery ticked or not, the mod will always report temperature and humidity to the WLED MQTT topic of that instance, if you have a broker configured and it's connected. ## Configuration Navigate to the "Config" and then to the "Usermods" section. If you compiled WLED with `-D USERMOD_SHT`, you will see the config for it there: * SHT-Type: * What it does: Select the SHT sensor type you want to use * Possible values: SHT30, SHT31, SHT35, SHT85 * Default: SHT30 * Unit: * What it does: Select which unit should be used to display the temperature in the info section. Also used when sending via MQTT discovery, see below. * Possible values: Celsius, Fahrenheit * Default: Celsius * Add-To-HA-MQTT-Discovery: * What it does: Makes the temperature and humidity available via MQTT discovery, so they're automatically added to Home Assistant, because that way it's typesafe. * Possible values: Enabled/Disabled * Default: Disabled ## Change log 2022-12 * First implementation. ## Credits ezcGman | Andy: Find me on the Intermit.Tech (QuinLED) Discord server: ================================================ FILE: usermods/sht/sht.cpp ================================================ #include "ShtUsermod.h" #include "SHT85.h" // Strings to reduce flash memory usage (used more than twice) const char ShtUsermod::_name[] PROGMEM = "SHT-Sensor"; const char ShtUsermod::_enabled[] PROGMEM = "Enabled"; const char ShtUsermod::_shtType[] PROGMEM = "SHT-Type"; const char ShtUsermod::_unitOfTemp[] PROGMEM = "Unit"; const char ShtUsermod::_haMqttDiscovery[] PROGMEM = "Add-To-HA-MQTT-Discovery"; /** * Initialise SHT sensor. * * Using the correct constructor according to config and initialises it using the * global i2c pins. * * @return void */ void ShtUsermod::initShtTempHumiditySensor() { switch (shtType) { case USERMOD_SHT_TYPE_SHT30: shtTempHumidSensor = (SHT *) new SHT30(); break; case USERMOD_SHT_TYPE_SHT31: shtTempHumidSensor = (SHT *) new SHT31(); break; case USERMOD_SHT_TYPE_SHT35: shtTempHumidSensor = (SHT *) new SHT35(); break; case USERMOD_SHT_TYPE_SHT85: shtTempHumidSensor = (SHT *) new SHT85(); break; } shtTempHumidSensor->begin(shtI2cAddress); // uses &Wire if (shtTempHumidSensor->readStatus() == 0xFFFF) { DEBUG_PRINTF("[%s] SHT init failed!\n", _name); cleanup(); return; } shtInitDone = true; } /** * Cleanup the SHT sensor. * * Properly calls "reset" for the sensor then releases it from memory. * * @return void */ void ShtUsermod::cleanupShtTempHumiditySensor() { if (isShtReady()) { shtTempHumidSensor->reset(); delete shtTempHumidSensor; shtTempHumidSensor = nullptr; } shtInitDone = false; } /** * Cleanup the mod completely. * * Calls ::cleanupShtTempHumiditySensor() to cleanup the SHT sensor and * deallocates pins. * * @return void */ void ShtUsermod::cleanup() { cleanupShtTempHumiditySensor(); enabled = false; } /** * Publish temperature and humidity to WLED device topic. * * Will add a "/temperature" and "/humidity" topic to the WLED device topic. * Temperature will be written in configured unit. * * @return void */ void ShtUsermod::publishTemperatureAndHumidityViaMqtt() { if (!WLED_MQTT_CONNECTED) return; char buf[128]; snprintf_P(buf, 127, PSTR("%s/temperature"), mqttDeviceTopic); mqtt->publish(buf, 0, false, String(getTemperature()).c_str()); snprintf_P(buf, 127, PSTR("%s/humidity"), mqttDeviceTopic); mqtt->publish(buf, 0, false, String(getHumidity()).c_str()); } /** * If enabled, publishes HA MQTT device discovery topics. * * Will make Home Assistant add temperature and humidity as entities automatically. * * Note: Whenever usermods are part of the WLED integration in HA, this can be dropped. * * @return void */ void ShtUsermod::publishHomeAssistantAutodiscovery() { if (!WLED_MQTT_CONNECTED) return; char json_str[1024], buf[128]; size_t payload_size; StaticJsonDocument<1024> json; snprintf_P(buf, 127, PSTR("%s Temperature"), serverDescription); json[F("name")] = buf; snprintf_P(buf, 127, PSTR("%s/temperature"), mqttDeviceTopic); json[F("stat_t")] = buf; json[F("dev_cla")] = F("temperature"); json[F("stat_cla")] = F("measurement"); snprintf_P(buf, 127, PSTR("%s-temperature"), escapedMac.c_str()); json[F("uniq_id")] = buf; json[F("unit_of_meas")] = unitOfTemp ? F("°F") : F("°C"); appendDeviceToMqttDiscoveryMessage(json); payload_size = serializeJson(json, json_str); snprintf_P(buf, 127, PSTR("homeassistant/sensor/%s/%s-temperature/config"), escapedMac.c_str(), escapedMac.c_str()); mqtt->publish(buf, 0, true, json_str, payload_size); json.clear(); snprintf_P(buf, 127, PSTR("%s Humidity"), serverDescription); json[F("name")] = buf; snprintf_P(buf, 127, PSTR("%s/humidity"), mqttDeviceTopic); json[F("stat_t")] = buf; json[F("dev_cla")] = F("humidity"); json[F("stat_cla")] = F("measurement"); snprintf_P(buf, 127, PSTR("%s-humidity"), escapedMac.c_str()); json[F("uniq_id")] = buf; json[F("unit_of_meas")] = F("%"); appendDeviceToMqttDiscoveryMessage(json); payload_size = serializeJson(json, json_str); snprintf_P(buf, 127, PSTR("homeassistant/sensor/%s/%s-humidity/config"), escapedMac.c_str(), escapedMac.c_str()); mqtt->publish(buf, 0, true, json_str, payload_size); haMqttDiscoveryDone = true; } /** * Helper to add device information to MQTT discovery topic. * * @return void */ void ShtUsermod::appendDeviceToMqttDiscoveryMessage(JsonDocument& root) { JsonObject device = root.createNestedObject(F("dev")); device[F("ids")] = escapedMac.c_str(); device[F("name")] = serverDescription; device[F("sw")] = versionString; device[F("mdl")] = ESP.getChipModel(); device[F("mf")] = F("espressif"); } /** * Setup the mod. * * Allocates i2c pins as PinOwner::HW_I2C, so they can be allocated multiple times. * And calls ::initShtTempHumiditySensor() to initialise the sensor. * * @see Usermod::setup() * @see UsermodManager::setup() * * @return void */ void ShtUsermod::setup() { if (enabled) { // GPIOs can be set to -1 , so check they're gt zero if (i2c_sda < 0 || i2c_scl < 0) { DEBUG_PRINTF("[%s] I2C bus not initialised!\n", _name); cleanup(); return; } initShtTempHumiditySensor(); initDone = true; } firstRunDone = true; } /** * Actually reading data (async) from the sensor every 30 seconds. * * If last reading is at least 30 seconds, it will trigger a reading using * SHT::requestData(). We will then continiously check SHT::dataReady() if * data is ready to be read. If so, it's read, stored locally and published * via MQTT. * * @see Usermod::loop() * @see UsermodManager::loop() * * @return void */ void ShtUsermod::loop() { if (!enabled || !initDone || strip.isUpdating()) return; if (isShtReady()) { if (millis() - shtLastTimeUpdated > 30000 && !shtDataRequested) { shtTempHumidSensor->requestData(); shtDataRequested = true; shtLastTimeUpdated = millis(); } if (shtDataRequested) { if (shtTempHumidSensor->dataReady()) { if (shtTempHumidSensor->readData(false)) { shtCurrentTempC = shtTempHumidSensor->getTemperature(); shtCurrentHumidity = shtTempHumidSensor->getHumidity(); publishTemperatureAndHumidityViaMqtt(); shtReadDataSuccess = true; } else { shtReadDataSuccess = false; } shtDataRequested = false; } } } } /** * Whenever MQTT is connected, publish HA autodiscovery topics. * * Is only done once. * * @see Usermod::onMqttConnect() * @see UsermodManager::onMqttConnect() * * @return void */ void ShtUsermod::onMqttConnect(bool sessionPresent) { if (haMqttDiscovery && !haMqttDiscoveryDone) publishHomeAssistantAutodiscovery(); } /** * Add dropdown for sensor type and unit to UM config page. * * @see Usermod::appendConfigData() * @see UsermodManager::appendConfigData() * * @return void */ void ShtUsermod::appendConfigData() { oappend(F("dd=addDropdown('")); oappend(_name); oappend(F("','")); oappend(_shtType); oappend(F("');")); oappend(F("addOption(dd,'SHT30',0);")); oappend(F("addOption(dd,'SHT31',1);")); oappend(F("addOption(dd,'SHT35',2);")); oappend(F("addOption(dd,'SHT85',3);")); oappend(F("dd=addDropdown('")); oappend(_name); oappend(F("','")); oappend(_unitOfTemp); oappend(F("');")); oappend(F("addOption(dd,'Celsius',0);")); oappend(F("addOption(dd,'Fahrenheit',1);")); } /** * Add config data to be stored in cfg.json. * * @see Usermod::addToConfig() * @see UsermodManager::addToConfig() * * @return void */ void ShtUsermod::addToConfig(JsonObject &root) { JsonObject top = root.createNestedObject(FPSTR(_name)); // usermodname top[FPSTR(_enabled)] = enabled; top[FPSTR(_shtType)] = shtType; top[FPSTR(_unitOfTemp)] = unitOfTemp; top[FPSTR(_haMqttDiscovery)] = haMqttDiscovery; } /** * Apply config on boot or save of UM config page. * * This is called whenever WLED boots and loads cfg.json, or when the UM config * page is saved. Will properly re-instantiate the SHT class upon type change and * publish HA discovery after enabling. * * @see Usermod::readFromConfig() * @see UsermodManager::readFromConfig() * * @return bool */ bool ShtUsermod::readFromConfig(JsonObject &root) { JsonObject top = root[FPSTR(_name)]; if (top.isNull()) { DEBUG_PRINTF("[%s] No config found. (Using defaults.)\n", _name); return false; } bool oldEnabled = enabled; byte oldShtType = shtType; byte oldUnitOfTemp = unitOfTemp; bool oldHaMqttDiscovery = haMqttDiscovery; getJsonValue(top[FPSTR(_enabled)], enabled); getJsonValue(top[FPSTR(_shtType)], shtType); getJsonValue(top[FPSTR(_unitOfTemp)], unitOfTemp); getJsonValue(top[FPSTR(_haMqttDiscovery)], haMqttDiscovery); // First run: reading from cfg.json, nothing to do here, will be all done in setup() if (!firstRunDone) { DEBUG_PRINTF("[%s] First run, nothing to do\n", _name); } // Check if mod has been en-/disabled else if (enabled != oldEnabled) { enabled ? setup() : cleanup(); DEBUG_PRINTF("[%s] Usermod has been en-/disabled\n", _name); } // Config has been changed, so adopt to changes else if (enabled) { if (oldShtType != shtType) { cleanupShtTempHumiditySensor(); initShtTempHumiditySensor(); } if (oldUnitOfTemp != unitOfTemp) { publishTemperatureAndHumidityViaMqtt(); publishHomeAssistantAutodiscovery(); } if (oldHaMqttDiscovery != haMqttDiscovery && haMqttDiscovery) { publishHomeAssistantAutodiscovery(); } DEBUG_PRINTF("[%s] Config (re)loaded\n", _name); } return true; } /** * Adds the temperature and humidity actually to the info section and /json info. * * This is called every time the info section is opened ot /json is called. * * @see Usermod::addToJsonInfo() * @see UsermodManager::addToJsonInfo() * * @return void */ void ShtUsermod::addToJsonInfo(JsonObject& root) { if (!enabled && !isShtReady()) { return; } JsonObject user = root["u"]; if (user.isNull()) user = root.createNestedObject("u"); JsonArray jsonTemp = user.createNestedArray(F("Temperature")); JsonArray jsonHumidity = user.createNestedArray(F("Humidity")); if (shtLastTimeUpdated == 0 || !shtReadDataSuccess) { jsonTemp.add(0); jsonHumidity.add(0); if (shtLastTimeUpdated == 0) { jsonTemp.add(F(" Not read yet")); jsonHumidity.add(F(" Not read yet")); } else { jsonTemp.add(F(" Error")); jsonHumidity.add(F(" Error")); } return; } jsonHumidity.add(getHumidity()); jsonHumidity.add(F(" RH")); jsonTemp.add(getTemperature()); jsonTemp.add(getUnitString()); // sensor object JsonObject sensor = root[F("sensor")]; if (sensor.isNull()) sensor = root.createNestedObject(F("sensor")); jsonTemp = sensor.createNestedArray(F("temp")); jsonTemp.add(getTemperature()); jsonTemp.add(getUnitString()); jsonHumidity = sensor.createNestedArray(F("humidity")); jsonHumidity.add(getHumidity()); jsonHumidity.add(F(" RH")); } /** * Getter for last read temperature for configured unit. * * @return float */ float ShtUsermod::getTemperature() { return unitOfTemp ? getTemperatureF() : getTemperatureC(); } /** * Returns the current configured unit as human readable string. * * @return const char* */ const char* ShtUsermod::getUnitString() { return unitOfTemp ? "°F" : "°C"; } static ShtUsermod sht; REGISTER_USERMOD(sht); ================================================ FILE: usermods/smartnest/library.json ================================================ { "name": "smartnest", "build": { "libArchive": false } } ================================================ FILE: usermods/smartnest/readme.md ================================================ # Smartnest Enables integration with `smartnest.cz` service which provides MQTT integration with voice assistants, for example Google Home, Alexa, Siri, Home Assistant and more! In order to setup Smartnest follow the [documentation](https://www.docu.smartnest.cz/). - You can create up to 5 different devices - To add the project to Google Home you can find the information [here](https://www.docu.smartnest.cz/google-home-integration) - To add the project to Alexa you can find the information [here](https://www.docu.smartnest.cz/alexa-integration) ## MQTT API The API is described in the Smartnest [Github repo](https://github.com/aososam/Smartnest/blob/master/Devices/lightRgb/lightRgb.ino). ## Usermod installation 1. Use `#define USERMOD_SMARTNEST` in wled.h or `-D USERMOD_SMARTNEST` in your platformio.ini (recommended). ## Configuration Usermod has no configuration, but it relies on the MQTT configuration.\ Under Config > Sync Interfaces > MQTT: * Enable `MQTT` check box. * Set the `Broker` field to: `smartnest.cz` or `3.122.209.170`(both work). * Set the `Port` field to: `1883` * The `Username` and `Password` fields are the login information from the `smartnest.cz` website (It is located above in the 3 points). * `Client ID` field is obtained from the device configuration panel in `smartnest.cz`. * `Device Topic` is obtained by entering the ClientID/report , remember to replace ClientId with your real information (Because they can ban your device). * `Group Topic` keep the same Group Topic. Wait `1 minute` after turning it on, as it usually takes a while. ## Change log 2022-09 * First implementation. 2024-05 * Solved code. * Updated documentation. * Second implementation. ================================================ FILE: usermods/smartnest/smartnest.cpp ================================================ #include "wled.h" #ifdef WLED_DISABLE_MQTT #error "This user mod requires MQTT to be enabled." #endif class Smartnest : public Usermod { private: bool initialized = false; unsigned long lastMqttReport = 0; unsigned long mqttReportInterval = 60000; // Report every minute void sendToBroker(const char *const topic, const char *const message) { if (!WLED_MQTT_CONNECTED) { return; } String topic_ = String(mqttClientID) + "/" + String(topic); mqtt->publish(topic_.c_str(), 0, true, message); } void turnOff() { setBrightness(0); turnOnAtBoot = false; offMode = true; sendToBroker("report/powerState", "OFF"); } void turnOn() { setBrightness(briLast); turnOnAtBoot = true; offMode = false; sendToBroker("report/powerState", "ON"); } void setBrightness(int value) { if (value == 0 && bri > 0) briLast = bri; bri = value; stateUpdated(CALL_MODE_DIRECT_CHANGE); } void setColor(int r, int g, int b) { strip.getMainSegment().setColor(0, RGBW32(r, g, b, 0)); stateUpdated(CALL_MODE_DIRECT_CHANGE); char msg[18] {}; sprintf(msg, "rgb(%d,%d,%d)", r, g, b); sendToBroker("report/color", msg); } int splitColor(const char *const color, int * const rgb) { char *color_ = NULL; const char delim[] = ","; char *cxt = NULL; char *token = NULL; int position = 0; // We need to copy the string in order to keep it read only as strtok_r function requires mutable string color_ = (char *)malloc(strlen(color) + 1); if (NULL == color_) { return -1; } strcpy(color_, color); token = strtok_r(color_, delim, &cxt); while (token != NULL) { rgb[position++] = (int)strtoul(token, NULL, 10); token = strtok_r(NULL, delim, &cxt); } free(color_); return position; } public: // Functions called by WLED /** * handling of MQTT message * topic should look like: /// */ bool onMqttMessage(char *topic, char *message) { String topic_{topic}; String topic_prefix{mqttClientID + String("/directive/")}; if (!topic_.startsWith(topic_prefix)) { return false; } String subtopic = topic_.substring(topic_prefix.length()); String message_(message); if (subtopic == "powerState") { if (strcmp(message, "ON") == 0) { turnOn(); } else if (strcmp(message, "OFF") == 0) { turnOff(); } return true; } if (subtopic == "percentage") { int val = (int)strtoul(message, NULL, 10); if (val >= 0 && val <= 100) { setBrightness(map(val, 0, 100, 0, 255)); } return true; } if (subtopic == "color") { // Parse the message which is in the format "rgb(<0-255>,<0-255>,<0-255>)" int rgb[3] = {}; String colors = message_.substring(String("rgb(").length(), message_.lastIndexOf(')')); if (3 != splitColor(colors.c_str(), rgb)) { return false; } setColor(rgb[0], rgb[1], rgb[2]); return true; } return false; } /** * subscribe to MQTT topic and send publish current status. */ void onMqttConnect(bool sessionPresent) { String topic = String(mqttClientID) + "/#"; mqtt->subscribe(topic.c_str(), 0); sendToBroker("report/online", (bri ? "true" : "false")); // Reports that the device is online delay(100); sendToBroker("report/firmware", versionString); // Reports the firmware version delay(100); sendToBroker("report/ip", (char *)WiFi.localIP().toString().c_str()); // Reports the IP delay(100); sendToBroker("report/network", (char *)WiFi.SSID().c_str()); // Reports the network name delay(100); String signal(WiFi.RSSI(), 10); sendToBroker("report/signal", signal.c_str()); // Reports the signal strength delay(100); } /** * 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_SMARTNEST; } /** * setup() is called once at startup to initialize the usermod. */ void setup() { DEBUG_PRINTF("Smartnest usermod setup initializing..."); // Publish initial status sendToBroker("report/status", "Smartnest usermod initialized"); } /** * loop() is called continuously to keep the usermod running. */ void loop() { // Periodically report status to MQTT broker unsigned long currentMillis = millis(); if (currentMillis - lastMqttReport >= mqttReportInterval) { lastMqttReport = currentMillis; // Report current brightness char brightnessMsg[11]; sprintf(brightnessMsg, "%u", bri); sendToBroker("report/brightness", brightnessMsg); // Report current signal strength String signal(WiFi.RSSI(), 10); sendToBroker("report/signal", signal.c_str()); } } }; static Smartnest smartnest; REGISTER_USERMOD(smartnest); ================================================ FILE: usermods/stairway_wipe_basic/library.json ================================================ { "name": "stairway_wipe_basic", "build": { "libArchive": false } } ================================================ FILE: usermods/stairway_wipe_basic/readme.md ================================================ # Stairway lighting ## Install Add the buildflag `-D USERMOD_STAIRCASE_WIPE` to your enviroment to activate it. ### Configuration `-D STAIRCASE_WIPE_OFF`
Have the LEDs wipe off instead of fading out ## Description Quick usermod to accomplish something similar to [this video](https://www.youtube.com/watch?v=NHkju5ncC4A). This usermod enables you to add a lightstrip alongside or on the steps of a staircase. When the `userVar0` variable is set, the LEDs will gradually turn on in a Wipe effect. Both directions are supported by setting userVar0 to 1 and 2, respectively (HTTP API commands `U0=1` and `U0=2`). After the Wipe is complete, the light will either stay on (Solid effect) indefinitely or extinguish after `userVar1` seconds have elapsed. If userVar0 is updated (e.g. by triggering a second sensor) the light will fade slowly until it's off. This could be extended to also run a Wipe effect in reverse order to turn the LEDs off. This is just a basic version to accomplish this using HTTP API calls `U0` and `U1` and/or macros. It should be easy to adapt this code to interface with motion sensors or other input devices. ================================================ FILE: usermods/stairway_wipe_basic/stairway_wipe_basic.cpp ================================================ #include "wled.h" /* * Usermods allow you to add own functionality to WLED more easily * See: https://github.com/wled-dev/WLED/wiki/Add-own-functionality * * This is Stairway-Wipe as a v2 usermod. * * Using this usermod: * 1. Copy the usermod into the sketch folder (same folder as wled00.ino) * 2. Register the usermod by adding #include "stairway-wipe-usermod-v2.h" in the top and registerUsermod(new StairwayWipeUsermod()) in the bottom of usermods_list.cpp */ class StairwayWipeUsermod : public Usermod { private: //Private class members. You can declare variables and functions only accessible to your usermod here unsigned long lastTime = 0; byte wipeState = 0; //0: inactive 1: wiping 2: solid unsigned long timeStaticStart = 0; uint16_t previousUserVar0 = 0; //moved to buildflag //comment this out if you want the turn off effect to be just fading out instead of reverse wipe //#define STAIRCASE_WIPE_OFF public: void setup() { } void loop() { //userVar0 (U0 in HTTP API): //has to be set to 1 if movement is detected on the PIR that is the same side of the staircase as the ESP8266 //has to be set to 2 if movement is detected on the PIR that is the opposite side //can be set to 0 if no movement is detected. Otherwise LEDs will turn off after a configurable timeout (userVar1 seconds) if (userVar0 > 0) { if ((previousUserVar0 == 1 && userVar0 == 2) || (previousUserVar0 == 2 && userVar0 == 1)) wipeState = 3; //turn off if other PIR triggered previousUserVar0 = userVar0; if (wipeState == 0) { startWipe(); wipeState = 1; } else if (wipeState == 1) { //wiping uint32_t cycleTime = 360 + (255 - effectSpeed)*75; //this is how long one wipe takes (minus 25 ms to make sure we switch in time) if (millis() + strip.timebase > (cycleTime - 25)) { //wipe complete effectCurrent = FX_MODE_STATIC; timeStaticStart = millis(); colorUpdated(CALL_MODE_NOTIFICATION); wipeState = 2; } } else if (wipeState == 2) { //static if (userVar1 > 0) //if U1 is not set, the light will stay on until second PIR or external command is triggered { if (millis() - timeStaticStart > userVar1*1000) wipeState = 3; } } else if (wipeState == 3) { //switch to wipe off #ifdef STAIRCASE_WIPE_OFF effectCurrent = FX_MODE_COLOR_WIPE; strip.timebase = 360 + (255 - effectSpeed)*75 - millis(); //make sure wipe starts fully lit colorUpdated(CALL_MODE_NOTIFICATION); wipeState = 4; #else turnOff(); #endif } else { //wiping off if (millis() + strip.timebase > (725 + (255 - effectSpeed)*150)) turnOff(); //wipe complete } } else { wipeState = 0; //reset for next time if (previousUserVar0) { #ifdef STAIRCASE_WIPE_OFF userVar0 = previousUserVar0; wipeState = 3; #else turnOff(); #endif } previousUserVar0 = 0; } } 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!")); } uint16_t getId() { return USERMOD_ID_STAIRWAY_WIPE; } void startWipe() { bri = briLast; //turn on jsonTransitionOnce = true; strip.setTransition(0); //no transition effectCurrent = FX_MODE_COLOR_WIPE; strip.resetTimebase(); //make sure wipe starts from beginning //set wipe direction Segment& seg = strip.getSegment(0); bool doReverse = (userVar0 == 2); seg.setOption(1, doReverse); colorUpdated(CALL_MODE_NOTIFICATION); } void turnOff() { jsonTransitionOnce = true; #ifdef STAIRCASE_WIPE_OFF strip.setTransition(0); //turn off immediately after wipe completed #else strip.setTransition(4000); //fade out slowly #endif bri = 0; stateUpdated(CALL_MODE_NOTIFICATION); wipeState = 0; userVar0 = 0; previousUserVar0 = 0; } //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! }; static StairwayWipeUsermod stairway_wipe_basic; REGISTER_USERMOD(stairway_wipe_basic); ================================================ FILE: usermods/udp_name_sync/library.json ================================================ { "name": "udp_name_sync", "build": { "libArchive": false }, "dependencies": {} } ================================================ FILE: usermods/udp_name_sync/udp_name_sync.cpp ================================================ #include "wled.h" class UdpNameSync : public Usermod { private: bool enabled = false; char segmentName[WLED_MAX_SEGNAME_LEN] = {0}; static constexpr uint8_t kPacketType = 200; // custom usermod packet type static const char _name[]; static const char _enabled[]; public: /** * Enable/Disable the usermod */ inline void enable(bool value) { enabled = value; } /** * Get usermod enabled/disabled state */ inline bool isEnabled() const { return enabled; } void setup() override { // Enabled when this usermod is compiled, set to false if you prefer runtime opt-in enable(true); } void loop() override { if (!enabled) return; if (!WLED_CONNECTED) return; if (!udpConnected) return; Segment& mainseg = strip.getMainSegment(); if (segmentName[0] == '\0' && !mainseg.name) return; //name was never set, do nothing const char* curName = mainseg.name ? mainseg.name : ""; if (strncmp(curName, segmentName, sizeof(segmentName)) == 0) return; // same name, do nothing IPAddress broadcastIp = uint32_t(Network.localIP()) | ~uint32_t(Network.subnetMask()); byte udpOut[WLED_MAX_SEGNAME_LEN + 2]; udpOut[0] = kPacketType; // custom usermod packet type (avoid 0..5 used by core protocols) if (segmentName[0] != '\0' && !mainseg.name) { // name cleared notifierUdp.beginPacket(broadcastIp, udpPort); segmentName[0] = '\0'; DEBUG_PRINTLN(F("UdpNameSync: sending empty name")); udpOut[1] = 0; // explicit empty string notifierUdp.write(udpOut, 2); notifierUdp.endPacket(); return; } notifierUdp.beginPacket(broadcastIp, udpPort); DEBUG_PRINT(F("UdpNameSync: saving segment name ")); DEBUG_PRINTLN(curName); strlcpy(segmentName, curName, sizeof(segmentName)); strlcpy((char *)&udpOut[1], segmentName, sizeof(udpOut) - 1); // leave room for header byte size_t nameLen = strnlen((char *)&udpOut[1], sizeof(udpOut) - 1); notifierUdp.write(udpOut, 2 + nameLen); notifierUdp.endPacket(); DEBUG_PRINT(F("UdpNameSync: Sent segment name : ")); DEBUG_PRINTLN(segmentName); return; } bool onUdpPacket(uint8_t * payload, size_t len) override { DEBUG_PRINT(F("UdpNameSync: Received packet")); if (!enabled) return false; if (receiveDirect) return false; if (len < 2) return false; // need type + at least 1 byte for name (can be 0) if (payload[0] != kPacketType) return false; Segment& mainseg = strip.getMainSegment(); char tmp[WLED_MAX_SEGNAME_LEN] = {0}; size_t copyLen = len - 1; if (copyLen > sizeof(tmp) - 1) copyLen = sizeof(tmp) - 1; memcpy(tmp, &payload[1], copyLen); tmp[copyLen] = '\0'; mainseg.setName(tmp); DEBUG_PRINT(F("UdpNameSync: set segment name")); return true; } }; static UdpNameSync udp_name_sync; REGISTER_USERMOD(udp_name_sync); ================================================ FILE: usermods/user_fx/README.md ================================================ # Usermod user FX This usermod is a common place to put various users’ WLED effects. It lets you load your own custom effects or bring back deprecated ones—without touching core WLED source code. Multiple Effects can be specified inside this single usermod, as we will illustrate below. You will be able to define them with custom names, sliders, etc. as with any other Effect. * [Installation](./README.md#installation) * [How The Usermod Works](./README.md#how-the-usermod-works) * [Basic Syntax for WLED Effect Creation](./README.md#basic-syntax-for-wled-effect-creation) * [Understanding 2D WLED Effects](./README.md#understanding-2d-wled-effects) * [The Metadata String](./README.md#the-metadata-string) * [Understanding 1D WLED Effects](./README.md#understanding-1d-wled-effects) * [Combining Multiple Effects in this Usermod](./README.md#combining-multiple-effects-in-this-usermod) * [Compiling](./README.md#compiling) * [Change Log](./README.md#change-log) * [Contact Us](./README.md#contact-us) ## Installation To activate the usermod, add the following line to your platformio_override.ini ```ini custom_usermods = user_fx ``` Or if you are already using a usermod, append user_fx to the list ```ini custom_usermods = audioreactive user_fx ``` ## How The Usermod Works The `user_fx.cpp` file can be broken down into four main parts: * **static effect definition** - This is a static LED setting that is displayed if an effect fails to initialize. * **User FX function definition(s)** - This area is where you place the FX code for all of the custom effects you want to use. This mainly includes the FX code and the static variable containing the [metadata string](https://kno.wled.ge/interfaces/json-api/#effect-metadata). * **Usermod Class definition(s)** - The class definition defines the blueprint from which all your custom Effects (or any usermod, for that matter) are created. * **Usermod registration** - All usermods have to be registered so that they are able to be compiled into your binary. We will go into greater detail on how custom effects work in the usermod and how to go about creating your own in the section below. ## Basic Syntax for WLED Effect Creation WLED effects generally follow a certain procedure for their operation: 1. Determine dimension of segment 2. Calculate new state if needed 3. Implement a loop that calculates color for each pixel and sets it using `SEGMENT.setPixelColor()` 4. The function is called at current frame rate. Below are some helpful variables and functions to know as you start your journey towards WLED effect creation: | Syntax Element | Size | Description | | :---------------------------------------------- | :----- | :---------- | | [`SEGMENT.speed / intensity / custom1 / custom2`](https://github.com/wled/WLED/blob/75f6de9dc29fc7da5f301fc1388ada228dcb3b6e/wled00/FX.h#L450) | 8-bit | These read-only variables help you control aspects of your custom effect using the UI sliders. You can edit these variables through the UI sliders when WLED is running your effect. (These variables can be controlled by the API as well.) Note that while `SEGMENT.intensity` through `SEGMENT.custom2` are 8-bit variables, `SEGMENT.custom3` is actually 5-bit. The other three bits are used by the boolean parameters `SEGMENT.check1` through `SEGMENT.check3` and are bit-packed to conserve data size and memory. | | [`SEGMENT.custom3`](https://github.com/wled/WLED/blob/75f6de9dc29fc7da5f301fc1388ada228dcb3b6e/wled00/FX.h#L454) | 5-bit | Another optional UI slider for custom effect control. While `SEGMENT.speed` through `SEGMENT.custom2` are 8-bit variables, `SEGMENT.custom3` is actually 5-bit. | | [`SEGMENT.check1 / check2 / check3`](https://github.com/wled/WLED/blob/75f6de9dc29fc7da5f301fc1388ada228dcb3b6e/wled00/FX.h#L455) | 1-bit | These variables are boolean parameters which show up as checkbox options in the User Interface. They are bit-packed along with `SEGMENT.custom3` to conserve data size and memory. | | [`SEGENV.aux0 / aux1`](https://github.com/wled/WLED/blob/75f6de9dc29fc7da5f301fc1388ada228dcb3b6e/wled00/FX.h#L467) | 16-bit | These are state variables that persists between function calls, and they are free to be overwritten by the user for any use case. | | [`SEGENV.step`](https://github.com/wled/WLED/blob/75f6de9dc29fc7da5f301fc1388ada228dcb3b6e/wled00/FX.h#L465) | 32-bit | This is a timestamp variable that contains the last update time. It is initially set during effect initialization to 0, and then it updates with the elapsed time after each frame runs. | | [`SEGENV.call`](https://github.com/wled/WLED/blob/75f6de9dc29fc7da5f301fc1388ada228dcb3b6e/wled00/FX.h#L466) | 32-bit | A counter for how many times this effect function has been invoked since it started. | | [`strip.now`](https://github.com/wled/WLED/blob/main/wled00/FX.h) | 32-bit | Current timestamp in milliseconds. (Equivalent to `millis()`, but use `strip.now()` instead.) `strip.now` respects the timebase, which can be used to advance or reset effects in a preset. This can be useful to sync multiple segments. | | [`SEGLEN / SEG_W / SEG_H`](https://github.com/wled/WLED/blob/75f6de9dc29fc7da5f301fc1388ada228dcb3b6e/wled00/FX.h#L116) | 16-bit | These variables are macros that help define the length and width of your LED strip/matrix segment. | | [`SEGPALETTE`](https://github.com/wled/WLED/blob/75f6de9dc29fc7da5f301fc1388ada228dcb3b6e/wled00/FX.h#L115) | --- | Macro that gets the currently selected palette for the currently processing segment. | | [`hw_random8()`](https://github.com/wled/WLED/blob/7b0075d3754fa883fc1bbc9fbbe82aa23a9b97b8/wled00/fcn_declare.h#L548) | 8-bit | One of several functions that generates a random integer. (All of the "hw_" functions are similar to the FastLED library's random functions, but in WLED they use true hardware-based randomness instead of a pseudo random number. In short, they are better and faster.) | | [`SEGCOLOR(x)`](https://github.com/wled/WLED/blob/75f6de9dc29fc7da5f301fc1388ada228dcb3b6e/wled00/FX.h#L114) | 32-bit | Macro that gets user-selected colors from UI, where x is an integer 1, 2, or 3 for primary, secondary, and tertiary colors, respectively. | | [`SEGMENT.setPixelColor`](https://github.com/WLED/WLED/blob/75f6de9dc29fc7da5f301fc1388ada228dcb3b6e/wled00/FX_fcn.cpp) / [`setPixelColorXY`](https://github.com/WLED/WLED/blob/75f6de9dc29fc7da5f301fc1388ada228dcb3b6e/wled00/FX_2Dfcn.cpp) | 32-bit | Function that paints one pixel. `setPixelColor` is 1‑D; `setPixelColorXY` expects `(x, y)` and an RGBW color value. | | [`SEGMENT.color_wheel()`](https://github.com/wled/WLED/blob/75f6de9dc29fc7da5f301fc1388ada228dcb3b6e/wled00/FX_fcn.cpp#L1092) | 32-bit | Input 0–255 to get a color. Transitions r→g→b→r. In HSV terms, `pos` is H. Note: only returns palette color unless the Default palette is selected. | | [`SEGMENT.color_from_palette()`](https://github.com/wled/WLED/blob/75f6de9dc29fc7da5f301fc1388ada228dcb3b6e/wled00/FX_fcn.cpp#L1093) | 32-bit | Gets a single color from the currently selected palette for a segment. (This function which should be favoured over `ColorFromPalette()` because this function returns an RGBW color with white from the `SEGCOLOR` passed, while also respecting the setting for palette wrapping. On the other hand, `ColorFromPalette()` simply gets the RGB palette color.) | | [`fade_out()`](https://github.com/wled/WLED/blob/75f6de9dc29fc7da5f301fc1388ada228dcb3b6e/wled00/FX_fcn.cpp#L1012) | --- | fade out function, higher rate = quicker fade. fading is highly dependent on frame rate (higher frame rates, faster fading). each frame will fade at max 9% or as little as 0.8%. | | [`fadeToBlackBy()`](https://github.com/wled/WLED/blob/75f6de9dc29fc7da5f301fc1388ada228dcb3b6e/wled00/FX_fcn.cpp#L1043) | --- | can be used to fade all pixels to black. | | [`fadeToSecondaryBy()`](https://github.com/wled/WLED/blob/75f6de9dc29fc7da5f301fc1388ada228dcb3b6e/wled00/FX_fcn.cpp#L1043) | --- | fades all pixels to secondary color. | | [`move()`](https://github.com/WLED/WLED/blob/75f6de9dc29fc7da5f301fc1388ada228dcb3b6e/wled00/FX_fcn.cpp) | --- | Moves/shifts pixels in the desired direction. | | [`blur / blur2d`](https://github.com/wled/WLED/blob/75f6de9dc29fc7da5f301fc1388ada228dcb3b6e/wled00/FX_fcn.cpp#L1053) | --- | Blurs all pixels for the desired segment. Blur also has the boolean option `smear`, which, when activated, does not fade the blurred pixel(s). | You will see how these syntax elements work in the examples below. ## Understanding 2D WLED Effects In this section we give some advice to those who are new to WLED Effect creation. We will illustrate how to load in multiple Effects using this single usermod, and we will do a deep dive into the anatomy of a 1D Effect as well as a 2D Effect. (Special thanks to @mryndzionek for offering this "Diffusion Fire" 2D Effect for this tutorial.) ### Imports The first line of the code imports the [wled.h](https://github.com/wled/WLED/blob/main/wled00/wled.h) file into this module. Importing `wled.h` brings all of the variables, files, and functions listed in the table above (and more) into your custom effect for you to use. ```cpp #include "wled.h" ``` ### Static Effect Definition The next code block is the `mode_static` definition. This is usually left as `SEGMENT.fill(SEGCOLOR(0));` to leave all pixels off if the effect fails to load, but in theory one could use this as a 'fallback effect' to take on a different behavior, such as displaying some other color instead of leaving the pixels off. `FX_FALLBACK_STATIC` is a macro that calls `mode_static()` and then returns. ### User Effect Definitions Pre-loaded in this template is an example 2D Effect called "Diffusion Fire". (This is the name that would be shown in the UI once the binary is compiled and run on your device, as defined in the metadata string.) The effect starts off by checking to see if the segment that the effect is being applied to is a 2D Matrix, and if it is not, then it runs the static effect which displays no pattern: ```cpp if (!strip.isMatrix || !SEGMENT.is2D()) FX_FALLBACK_STATIC; // not a 2D set-up ``` The next code block contains several constant variable definitions which essentially serve to extract the dimensions of the user's 2D matrix and allow WLED to interpret the matrix as a 1D coordinate system (WLED must do this for all 2D animations): ```cpp const int cols = SEG_W; const int rows = SEG_H; const auto XY = [&](int x, int y) { return x + y * cols; }; ``` * The first line assigns the number of columns (width) in the active segment to cols. * SEG_W is a macro defined in WLED that expands to SEGMENT.width(). This value is the width of your 2D matrix segment, used to traverse the matrix correctly. * Next, we assign the number of rows (height) in the segment to rows. * SEG_H is a macro for SEGMENT.height(). Combined with cols, this allows pixel addressing in 2D (x, y) space. * The third line declares a lambda function named `XY` to map (x, y) matrix coordinates into a 1D index in the LED array. This assumes row-major order (left to right, top to bottom). * This lambda helps with mapping a local 1D array to a 2D one. The next lines of code further the setup process by defining variables that allow the effect's settings to be configurable using the UI sliders (or alternatively, through API calls): ```cpp const uint8_t refresh_hz = map(SEGMENT.speed, 0, 255, 20, 80); const unsigned refresh_ms = 1000 / refresh_hz; const int16_t diffusion = map(SEGMENT.custom1, 0, 255, 0, 100); const uint8_t spark_rate = SEGMENT.intensity; const uint8_t turbulence = SEGMENT.custom2; ``` * The first line maps the SEGMENT.speed (user-controllable parameter from 0–255) to a value between 20 and 80 Hz. * This determines how often the effect should refresh per second (Higher speed = more frames per second). * Next we convert refresh rate from Hz to milliseconds. (It’s easier to schedule animation updates in WLED using elapsed time in milliseconds.) * This value is used to time when to update the effect. * The third line utilizes the `custom1` control (0–255 range, usually exposed via sliders) to define the diffusion rate, mapped to 0–100. * This controls how much "heat" spreads to neighboring pixels — more diffusion = smoother flame spread. * Next we assign `SEGMENT.intensity` (user input 0–255) to a variable named `spark_rate`. * This controls how frequently new "spark" pixels appear at the bottom of the matrix. * A higher value means more frequent ignition of flame points. * The final line stores the user-defined `custom2` value to a variable called `turbulence`. * This is used to introduce randomness in spark generation or flow — more turbulence means more chaotic behavior. Next we will look at some lines of code that handle memory allocation and effect initialization: ```cpp unsigned dataSize = cols * rows; // SEGLEN (virtual length) is equivalent to vWidth()*vHeight() for 2D ``` * This part calculates how much memory we need to represent per-pixel state. * `cols * rows` or `(or SEGLEN)` returns the total number of pixels in the current segment. * This fire effect models heat values per pixel (not just colors), so we need persistent storage — one uint8_t per pixel — for the entire effect. > **_NOTE:_** Virtual lengths `vWidth()` and `vHeight()` will be evaluated differently based on your own custom effect, and based on what other settings are active. For example: If you have an LED strip of length = 60 and you enable grouping = 2, then the virtual length will be 30, so the FX will render 30 pixels instead of 60. This is also true for mirroring or adding gaps--it halves the size. For a 1D strip mapped to 2D, the virtual length depends on selected mode. Keep these things in mind during your custom effect's creation. ```cpp if (!SEGENV.allocateData(dataSize)) FX_FALLBACK_STATIC; // allocation failed ``` * Upon the first call, this section allocates a persistent data buffer tied to the segment environment (`SEGENV.data`). All subsequent calls simply ensure that the data is still valid. * The syntax `SEGENV.allocateData(n)` requests a buffer of size n bytes (1 byte per pixel here). * If allocation fails (e.g., out of memory), it returns false, and the effect can’t proceed. * It calls previously defined `mode_static()` fallback effect, which just fills the segment with a static color. We need to do this because WLED needs a fail-safe behavior if a custom effect can't run properly due to memory constraints. The next lines of code clear the LEDs and initialize timing: ```cpp if (SEGENV.call == 0) { SEGMENT.fill(BLACK); SEGENV.step = 0; } ``` * The first line checks whether this is the first time the effect is being run; `SEGENV.call` is a counter for how many times this effect function has been invoked since it started. * If `SEGENV.call` equals 0 (which it does on the very first call, making it useful for initialization), then it clears the LED segment by filling it with black (turns off all LEDs). * This gives a clean starting point for the fire animation. * It also initializes `SEGENV.step`, a timing marker, to 0. This value is later used as a timestamp to control when the next animation frame should occur (based on elapsed time). The next block of code is where the animation update logic starts to kick in: ```cpp if ((strip.now - SEGENV.step) >= refresh_ms) { uint8_t tmp_row[cols]; // Keep for ≤~1 KiB; otherwise consider heap or reuse SEGENV.data as scratch. SEGENV.step = strip.now; // scroll up for (unsigned y = 1; y < rows; y++) for (unsigned x = 0; x < cols; x++) { unsigned src = XY(x, y); unsigned dst = XY(x, y - 1); SEGENV.data[dst] = SEGENV.data[src]; } ``` * The first line checks if it's time to update the effect frame. `strip.now` is the current timestamp in milliseconds; `SEGENV.step` is the last update time (set during initialization or previous frame). `refresh_ms` is how long to wait between frames, computed earlier based on SEGMENT.speed. * The conditional statement in the first line of code ensures the effect updates on a fixed interval — e.g., every 20 ms for 50 Hz. * The second line of code declares a temporary row buffer for intermediate diffusion results that is one byte per column (horizontal position), so this buffer holds one row's worth of heat values. * You'll see later that it writes results here before updating `SEGENV.data`. * Note: this is allocated on the stack each frame. Keep such VLAs ≤ ~1 KiB; for larger sizes, prefer a buffer in `SEGENV.data`. > **_IMPORTANT NOTE:_** Creating variable‑length arrays (VLAs) is non‑standard C++, but this practice is used throughout WLED and works in practice. But be aware that VLAs live on the stack, which is limited. If the array scales with segment length (1D), it can overflow the stack and crash. Keep VLAs ≲ ~1 KiB; an array with 4000 LEDs is ~4 KiB and will likely crash. It’s worse with `uint16_t`. Anything larger than ~1 KiB should go into `SEGENV.data`, which has a higher limit. Now we get to the spark generation portion, where new bursts of heat appear at the bottom of the matrix: ```cpp if (hw_random8() > turbulence) { // create new sparks at bottom row for (unsigned x = 0; x < cols; x++) { uint8_t p = hw_random8(); if (p < spark_rate) { unsigned dst = XY(x, rows - 1); SEGENV.data[dst] = 255; } } } ``` * The first line randomizes whether we even attempt to spawn sparks this frame. * `hw_random8()` gives a random number between 0–255 using a fast hardware RNG. * `turbulence` is a user-controlled parameter (SEGMENT.custom2, set earlier). * Higher turbulence means this block is less likely to run (because `hw_random8()` is less likely to exceed a high threshold). * This adds randomness to when sparks appear — simulating natural flicker and chaotic fire. * The next line loops over all columns in the bottom row (row `rows - 1`). * Another random number, `p`, is used to probabilistically decide whether a spark appears at this (x, `rows-1`) position. * Next is a conditional statement. The lower spark_rate is, the fewer sparks will appear. * `spark_rate` comes from `SEGMENT.intensity` (0–255). * High intensity means more frequent ignition. * `dst` calculates the destination index in the bottom row at column x. * The final line here sets the heat at this pixel to maximum (255). * This simulates a fresh burst of flame, which will diffuse and move upward over time in subsequent frames. Next we reach the first part of the core of the fire simulation, which is diffusion (how heat spreads to neighboring pixels): ```cpp // diffuse for (unsigned y = 0; y < rows; y++) { for (unsigned x = 0; x < cols; x++) { unsigned v = SEGENV.data[XY(x, y)]; if (x > 0) { v += SEGENV.data[XY(x - 1, y)]; } if (x < (cols - 1)) { v += SEGENV.data[XY(x + 1, y)]; } tmp_row[x] = min(255, (int)(v * 100 / (300 + diffusion))); } ``` * This block of code starts by looping over each row from top to bottom. (We will do diffusion for each pixel row.) * Next we start an inner loop which iterates across each column in the current row. * Starting with the current heat value of pixel (x, y) assigned `v`: * if there’s a pixel to the left, add its heat to the total. * If there’s a pixel to the right, add its heat as well. * So essentially, what the two `if` statements accomplish is: `v = center + left + right`. * The final line of code applies diffusion smoothing: * The denominator controls how much the neighboring heat contributes. `300 + diffusion` means that with higher diffusion, you get more smoothing (since the sum is divided more). * The `v * 100` scales things before dividing (preserving some dynamic range). * `min(255, ...)` clamps the result to 8-bit range. * This entire line of code stores the smoothed heat into the temporary row buffer. After calculating tmp_row, we now handle rendering the pixels by updating the actual segment data and turning 'heat' into visible colors: ```cpp for (unsigned x = 0; x < cols; x++) { SEGENV.data[XY(x, y)] = tmp_row[x]; if (SEGMENT.check1) { uint32_t color = SEGMENT.color_from_palette(tmp_row[x], true, false, 0); SEGMENT.setPixelColorXY(x, y, color); } else { uint32_t base = SEGCOLOR(0); SEGMENT.setPixelColorXY(x, y, color_fade(base, tmp_row[x])); } } } ``` * This next loop starts iterating over each row from top to bottom. (We're now doing this for color-rendering for each pixel row.) * Next we update the main segment data with the smoothed value for this pixel. * The if statement creates a conditional rendering path — the user can toggle this. If `check1` is enabled in the effect metadata, we use a color palette to display the flame. * The next line converts the heat value (`tmp_row[x]`) into a `color` from the current palette with 255 brightness, and no wrapping in palette lookup. * This creates rich gradient flames (e.g., yellow → red → black). * Finally we set the rendered color for the pixel (x, y). * This repeats for each pixel in each row. * If palette use is disabled, we fallback to fading a base color. * `SEGCOLOR(0)` gets the first user-selected color for the segment. * The final line of code fades that base color according to the heat value (acts as brightness multiplier). * Even though the effect logic itself controls when to update based on refresh_ms, WLED will still call this function at roughly FRAMETIME intervals (the FPS limit set in config) to check whether an update is needed. If nothing needs to change, the frame still needs to be re-rendered so color or brightness transitions will be smooth. If you want to run your effect at a fixed frame rate you can use the following code to not update your effect state, be aware however that transitions for your effect will also run at this frame rate - for example if you limit your effect to say 5 FPS, brightness changes and color changes may not look smooth. Also `SEGMENT.call` is still incremented on each function call. ```cpp //limit update rate if (strip.now - SEGENV.step < FRAMETIME_FIXED) return; SEGENV.step = strip.now; ``` ### The Metadata String At the end of every effect is an important line of code called the **metadata string**. It defines how the effect is to be interacted with in the UI: ```cpp static const char _data_FX_MODE_DIFFUSIONFIRE[] PROGMEM = "Diffusion Fire@!,Spark rate,Diffusion Speed,Turbulence,,Use palette;;Color;;2;pal=35"; ``` This metadata string is passed into `strip.addEffect()` and parsed by WLED to determine how your effect appears and behaves in the UI. The string follows the syntax of `;;;;`, where Effect Parameters are specified by a comma-separated list. The values for Effect Parameters will always follow the convention in the table below: | Parameter | Default tooltip label | | :-------- | :-------------------- | | sx | Effect Speed | | ix | Effect Intensity | | c1 | Custom 1 | | c2 | Custom 2 | | c3 | Custom 3 | | o1 | Checkbox 1 | | o2 | Checkbox 2 | | o3 | Checkbox 3 | Using this info, let’s split the Metadata string above into logical sections: | Syntax Element | Description | | :---------------------------------------------- | :---------- | | "Diffusion Fire@! | Name. (The @ symbol marks the end of the Effect Name, and the beginning of the Parameter String elements.) | | !, | Use default UI entry; for the first space, this will automatically create a slider for Speed | | Spark rate, Diffusion Speed, Turbulence, | UI sliders for Spark Rate, Diffusion Speed, and Turbulence. Defining slider 2 as "Spark Rate" overwrites the default value of Intensity. | | (blank), | unused (empty field with not even a space) | | Use palette; | This occupies the spot for the 6th effect parameter, which automatically makes this a checkbox argument `o1` called Use palette in the UI. When this is enabled, the effect uses `SEGMENT.color_from_palette(...)` (RGBW-aware, respects wrap), otherwise it fades from `SEGCOLOR(0)`. The first semicolon marks the end of the Effect Parameters and the beginning of the `Colors` parameter. | | Color; | Custom color field `(SEGCOLOR(0))` | | (blank); | Empty means the effect does not allow Palettes to be selected by the user. But used in conjunction with the checkbox argument, palette use can be turned on/off by the user. | | 2; | Flag specifying that the effect requires a 2D matrix setup | | pal=35" | Default Palette ID. this is the setting that the effect starts up with. | More information on metadata strings can be found [here](https://kno.wled.ge/interfaces/json-api/#effect-metadata). ## Understanding 1D WLED Effects Next, we will look at a 1D WLED effect called `Sinelon`. This one is an especially interesting example because it shows how a single effect function can be used to create several different selectable effects in the UI. We will break this effect down step by step. (This effect was originally one of the FastLED example effects; more information on FastLED can be found [here](https://fastled.io/).) ```cpp static void sinelon_base(bool dual, bool rainbow=false) { ``` * The first line of code defines `sinelon base` as static helper function. This is how all effects are initially defined. * Notice that it has some optional flags; these parameters will allow us to easily define the effect in different ways in the UI. ```cpp if (SEGLEN <= 1) FX_FALLBACK_STATIC; ``` * If segment length ≤ 1, there’s nothing to animate. Just show static mode. The line of code helps create the "Fade Out" Trail: ```cpp SEGMENT.fade_out(SEGMENT.intensity); ``` * Gradually dims all LEDs each frame using SEGMENT.intensity as fade amount. * Creates the trailing "comet" effect by leaving a fading path behind the moving dot. Next, the effect computes some position information for the actively changing pixel, and the rest of the pixels as well: ```cpp unsigned pos = beatsin16_t(SEGMENT.speed/10, 0, SEGLEN-1); if (SEGENV.call == 0) SEGENV.aux0 = pos; ``` * Calculates a sine-based oscillation to move the dot smoothly back and forth. * `beatsin16_t` is an improved version of FastLED’s beatsin16 function, generating smooth oscillations * SEGMENT.speed / 10: affects oscillation speed. Higher = faster. * 0: minimum position. * SEGLEN-1: maximum position. * On first call `(SEGENV.call == 0)`, stores initial position in `SEGENV.aux0`. (`SEGENV.aux0` is a temporary state variable to keep track of last position.) The next lines of code help determine the colors to be used: ```cpp uint32_t color1 = SEGMENT.color_from_palette(pos, true, false, 0); uint32_t color2 = SEGCOLOR(2); ``` * `color1`: main moving dot color, chosen from palette using the current position as index. * `color2`: secondary color from user-configured color slot 2. The next part takes into account the optional argument for if a Rainbow colored palette is in use: ```cpp if (rainbow) { color1 = SEGMENT.color_wheel((pos & 0x07) * 32); } ``` * If `rainbow` is true, override color1 using a rainbow wheel, producing rainbow cycling colors. * `(pos & 0x07) * 32` ensures the color changes gradually with position. ```cpp SEGMENT.setPixelColor(pos, color1); ``` * Lights up the computed position with the selected color. The next line takes into account another one of the optional arguments for the effect to potentially handle dual mirrored dots which create the animation: ```cpp if (dual) { if (!color2) color2 = SEGMENT.color_from_palette(pos, true, false, 0); if (rainbow) color2 = color1; // share rainbow color SEGMENT.setPixelColor(SEGLEN-1-pos, color2); } ``` * If dual is true: * Uses `color2` for mirrored dot on opposite side. * If `color2` is not set (0), fallback to same palette color as `color1`. * In `rainbow` mode, force both dots to share the rainbow color. * Sets pixel at `SEGLEN-1-pos` to `color2`. This final part of the effect function will fill in the 'trailing' pixels to complete the animation: ```cpp if (SEGENV.aux0 < pos) { for (unsigned i = SEGENV.aux0; i < pos ; i++) { SEGMENT.setPixelColor(i, color1); if (dual) SEGMENT.setPixelColor(SEGLEN-1-i, color2); } } else { for (unsigned i = SEGENV.aux0; i > pos ; i--) { SEGMENT.setPixelColor(i, color1); if (dual) SEGMENT.setPixelColor(SEGLEN-1-i, color2); } } SEGENV.aux0 = pos; } ``` * The first line checks if current position has changed since last frame. (Prevents holes if the dot moves quickly and "skips" pixels.) If the position has changed, then it will implement the logic to update the rest of the pixels. * Fills in all pixels between previous position (SEGENV.aux0) and new position (pos) to ensure smooth continuous trail. * Works in both directions: Forward (if new pos > old pos), and Backward (if new pos < old pos). * Updates `SEGENV.aux0` to current position at the end. The last part of this effect has the Wrapper functions for different Sinelon modes. Notice that there are three different modes that we can define from the single effect definition by leveraging the arguments in the function: ```cpp void mode_sinelon(void) { sinelon_base(false); } // Calls sinelon_base with dual = false and rainbow = false void mode_sinelon_dual(void) { sinelon_base(true); } // Calls sinelon_base with dual = true and rainbow = false void mode_sinelon_rainbow(void) { sinelon_base(false, true); } // Calls sinelon_base with dual = false and rainbow = true ``` And then the last part defines the metadata strings for each effect to specify how it will be portrayed in the UI: ```cpp static const char _data_FX_MODE_SINELON[] PROGMEM = "Sinelon@!,Trail;!,!,!;!"; static const char _data_FX_MODE_SINELON_DUAL[] PROGMEM = "Sinelon Dual@!,Trail;!,!,!;!"; static const char _data_FX_MODE_SINELON_RAINBOW[] PROGMEM = "Sinelon Rainbow@!,Trail;,,!;!"; ``` Refer to the section above for guidance on understanding metadata strings. ### The UserFxUsermod Class The `UserFxUsermod` class registers the `mode_diffusionfire` effect with WLED. This section starts right after the effect function and metadata string, and is responsible for making the effect usable in the WLED interface: ```cpp class UserFxUsermod : public Usermod { private: public: void setup() override { strip.addEffect(255, &mode_diffusionfire, _data_FX_MODE_DIFFUSIONFIRE); //////////////////////////////////////// // add your effect function(s) here // //////////////////////////////////////// // use id=255 for all custom user FX (the final id is assigned when adding the effect) // strip.addEffect(255, &mode_your_effect, _data_FX_MODE_YOUR_EFFECT); // strip.addEffect(255, &mode_your_effect2, _data_FX_MODE_YOUR_EFFECT2); // strip.addEffect(255, &mode_your_effect3, _data_FX_MODE_YOUR_EFFECT3); } void loop() override {} // nothing to do in the loop uint16_t getId() override { return USERMOD_ID_USER_FX; } }; ``` * The first line declares a new class called UserFxUsermod. It inherits from `Usermod`, which is the base class WLED uses for any pluggable user-defined modules. * This makes UserFxUsermod a valid WLED extension that can hook into `setup()`, `loop()`, and other lifecycle events. * The `void setup()` function runs once when WLED initializes the usermod. * It's where you should register your effects, initialize hardware, or do any other setup logic. * `override` ensures that this matches the Usermod base class definition. * The `strip.addEffect` line is an important one that registers the custom effect so WLED knows about it. * 255: Temporary ID — WLED will assign a unique ID automatically. (**Create all custom effects with the 255 ID.**) * `&mode_diffusionfire`: Pointer to the effect function. * `_data_FX_MODE_DIFFUSIONFIRE`: Metadata string stored in PROGMEM, describing the effect name and UI fields (like sliders). * After this, your custom effect shows up in the WLED effects list. * The `loop()` function remains empty because this usermod doesn’t need to do anything continuously. WLED still calls this every main loop, but nothing is done here. * If your usermod had to respond to input or update state, you'd do it here. * The last part returns a unique ID constant used to identify this usermod. * USERMOD_ID_USER_FX is defined in [const.h](https://github.com/wled/WLED/blob/main/wled00/const.h). WLED uses this for tracking, debugging, or referencing usermods internally. The final part of this file handles instantiation and initialization: ```cpp static UserFxUsermod user_fx; REGISTER_USERMOD(user_fx); ``` * The first line creates a single, global instance of your usermod class. * The last line is a macro that tells WLED: “This is a valid usermod — load it during startup.” * WLED adds it to the list of active usermods, calls `setup()` and `loop()`, and lets it interact with the system. ## Combining Multiple Effects in this Usermod So now let's say that you wanted add the effects "Diffusion Fire" and "Sinelon" through this same Usermod file: * Navigate to [the code for Sinelon](https://github.com/wled/WLED/blob/7b0075d3754fa883fc1bbc9fbbe82aa23a9b97b8/wled00/FX.cpp#L3110). * Copy this code, and place it below the metadata string for Diffusion Fire. Be sure to get the metadata string as well--and to name it something different than what's already inside the core WLED code. (Refer to the metadata String section above for more information.) * Register the effect using the `addEffect` function in the Usermod class. * Compile the code! ## Compiling Compiling WLED yourself is beyond the scope of this tutorial, but [the complete guide to compiling WLED can be found here](https://kno.wled.ge/advanced/compiling-wled/), on the official WLED documentation website. ## Change Log ### Version 1.0.0 * First version of the custom effect creation guide ## Contact Us This custom effect tutorial guide is still in development. If you have suggestions on what should be added, or if you've found any parts of this guide which seem incorrect, feel free to reach out [here](mailto:aregis1992@gmail.com) and help us improve this guide for future creators. ================================================ FILE: usermods/user_fx/library.json ================================================ { "name": "user_fx", "build": { "libArchive": false } } ================================================ FILE: usermods/user_fx/user_fx.cpp ================================================ #include "wled.h" // for information how FX metadata strings work see https://kno.wled.ge/interfaces/json-api/#effect-metadata // paletteBlend: 0 - wrap when moving, 1 - always wrap, 2 - never wrap, 3 - none (undefined) #define PALETTE_SOLID_WRAP (paletteBlend == 1 || paletteBlend == 3) #define indexToVStrip(index, stripNr) ((index) | (int((stripNr)+1)<<16)) // static effect, used if an effect fails to initialize static void mode_static(void) { SEGMENT.fill(SEGCOLOR(0)); } #define FX_FALLBACK_STATIC { mode_static(); return; } // If you define configuration options in your class and need to reference them in your effect function, add them here. // If you only need to use them in your class you can define them as class members instead. // bool myConfigValue = false; ///////////////////////// // User FX functions // ///////////////////////// // Diffusion Fire: fire effect intended for 2D setups smaller than 16x16 static void mode_diffusionfire(void) { if (!strip.isMatrix || !SEGMENT.is2D()) FX_FALLBACK_STATIC; // not a 2D set-up const int cols = SEG_W; const int rows = SEG_H; const auto XY = [&](int x, int y) { return x + y * cols; }; const uint8_t refresh_hz = map(SEGMENT.speed, 0, 255, 20, 80); const unsigned refresh_ms = 1000 / refresh_hz; const int16_t diffusion = map(SEGMENT.custom1, 0, 255, 0, 100); const uint8_t spark_rate = SEGMENT.intensity; const uint8_t turbulence = SEGMENT.custom2; unsigned dataSize = cols * rows; // SEGLEN (virtual length) is equivalent to vWidth()*vHeight() for 2D if (!SEGENV.allocateData(dataSize)) FX_FALLBACK_STATIC; // allocation failed if (SEGENV.call == 0) { SEGMENT.fill(BLACK); SEGENV.step = 0; } if ((strip.now - SEGENV.step) >= refresh_ms) { // Keep for ≤~1 KiB; otherwise consider heap or reuse SEGENV.data as scratch. uint8_t tmp_row[cols]; SEGENV.step = strip.now; // scroll up for (unsigned y = 1; y < rows; y++) for (unsigned x = 0; x < cols; x++) { unsigned src = XY(x, y); unsigned dst = XY(x, y - 1); SEGENV.data[dst] = SEGENV.data[src]; } if (hw_random8() > turbulence) { // create new sparks at bottom row for (unsigned x = 0; x < cols; x++) { uint8_t p = hw_random8(); if (p < spark_rate) { unsigned dst = XY(x, rows - 1); SEGENV.data[dst] = 255; } } } // diffuse for (unsigned y = 0; y < rows; y++) { for (unsigned x = 0; x < cols; x++) { unsigned v = SEGENV.data[XY(x, y)]; if (x > 0) { v += SEGENV.data[XY(x - 1, y)]; } if (x < (cols - 1)) { v += SEGENV.data[XY(x + 1, y)]; } tmp_row[x] = min(255, (int)(v * 100 / (300 + diffusion))); } for (unsigned x = 0; x < cols; x++) { SEGENV.data[XY(x, y)] = tmp_row[x]; if (SEGMENT.check1) { uint32_t color = SEGMENT.color_from_palette(tmp_row[x], true, false, 0); SEGMENT.setPixelColorXY(x, y, color); } else { uint32_t base = SEGCOLOR(0); SEGMENT.setPixelColorXY(x, y, color_fade(base, tmp_row[x])); } } } } } static const char _data_FX_MODE_DIFFUSIONFIRE[] PROGMEM = "Diffusion Fire@!,Spark rate,Diffusion Speed,Turbulence,,Use palette;;Color;;2;pal=35"; /* * Spinning Wheel effect - LED animates around 1D strip (or each column in a 2D matrix), slows down and stops at random position * Created by Bob Loeffler and claude.ai * First slider (Spin speed) is for the speed of the moving/spinning LED (random number within a narrow speed range). * If value is 0, a random speed will be selected from the full range of values. * Second slider (Spin slowdown start time) is for how long before the slowdown phase starts (random number within a narrow time range). * If value is 0, a random time will be selected from the full range of values. * Third slider (Spinner size) is for the number of pixels that make up the spinner. * Fourth slider (Spin delay) is for how long it takes for the LED to start spinning again after the previous spin. * The first checkbox allows the spinner to spin. If it's enabled, the spinner will do its thing. If it's not enabled, it will wait for the user to enable * it either by clicking the checkbox or by pressing a physical button (e.g. using a playlist to run a couple presets that have JSON API codes). * The second checkbox sets "color per block" mode. Enabled means that each spinner block will be the same color no matter what its LED position is. * The third checkbox enables synchronized restart (all spinners restart together instead of individually). * aux0 stores the settings checksum to detect changes * aux1 stores the color scale for performance */ static void mode_spinning_wheel(void) { if (SEGLEN < 1) FX_FALLBACK_STATIC; unsigned strips = SEGMENT.nrOfVStrips(); if (strips == 0) FX_FALLBACK_STATIC; constexpr unsigned stateVarsPerStrip = 8; unsigned dataSize = sizeof(uint32_t) * stateVarsPerStrip; if (!SEGENV.allocateData(dataSize * strips)) FX_FALLBACK_STATIC; uint32_t* state = reinterpret_cast(SEGENV.data); // state[0] = current position (fixed point: upper 16 bits = position, lower 16 bits = fraction) // state[1] = velocity (fixed point: pixels per frame * 65536) // state[2] = phase (0=fast spin, 1=slowing, 2=wobble, 3=stopped) // state[3] = stop time (when phase 3 was entered) // state[4] = wobble step (0=at stop pos, 1=moved back, 2=returned to stop) // state[5] = slowdown start time (when to transition from phase 0 to phase 1) // state[6] = wobble timing (for 200ms / 400ms / 300ms delays) // state[7] = store the stop position per strip // state[] index values for easier readability constexpr unsigned CUR_POS_IDX = 0; // state[0] constexpr unsigned VELOCITY_IDX = 1; constexpr unsigned PHASE_IDX = 2; constexpr unsigned STOP_TIME_IDX = 3; constexpr unsigned WOBBLE_STEP_IDX = 4; constexpr unsigned SLOWDOWN_TIME_IDX = 5; constexpr unsigned WOBBLE_TIME_IDX = 6; constexpr unsigned STOP_POS_IDX = 7; SEGMENT.fill(SEGCOLOR(1)); // Handle random seeding globally (outside the virtual strip) if (SEGENV.call == 0) { random16_set_seed(hw_random16()); SEGENV.aux1 = (255 << 8) / SEGLEN; // Cache the color scaling } // Check if settings changed (do this once, not per virtual strip) uint32_t settingssum = SEGMENT.speed + SEGMENT.intensity + SEGMENT.custom1 + SEGMENT.custom3 + SEGMENT.check1 + SEGMENT.check3; bool settingsChanged = (SEGENV.aux0 != settingssum); if (settingsChanged) { random16_add_entropy(hw_random16()); SEGENV.aux0 = settingssum; } // Check if all spinners are stopped and ready to restart (for synchronized restart) bool allReadyToRestart = true; if (SEGMENT.check3) { uint8_t spinnerSize = map(SEGMENT.custom1, 0, 255, 1, 10); uint16_t spin_delay = map(SEGMENT.custom3, 0, 31, 2000, 15000); uint32_t now = strip.now; for (unsigned stripNr = 0; stripNr < strips; stripNr += spinnerSize) { uint32_t* stripState = &state[stripNr * stateVarsPerStrip]; // Check if this spinner is stopped AND has waited its delay if (stripState[PHASE_IDX] != 3 || stripState[STOP_TIME_IDX] == 0) { allReadyToRestart = false; break; } // Check if delay has elapsed if ((now - stripState[STOP_TIME_IDX]) < spin_delay) { allReadyToRestart = false; break; } } } struct virtualStrip { static void runStrip(uint16_t stripNr, uint32_t* state, bool settingsChanged, bool allReadyToRestart, unsigned strips) { uint8_t phase = state[PHASE_IDX]; uint32_t now = strip.now; // Check for restart conditions bool needsReset = false; if (SEGENV.call == 0) { needsReset = true; } else if (settingsChanged && SEGMENT.check1) { needsReset = true; } else if (phase == 3 && state[STOP_TIME_IDX] != 0) { // If synchronized restart is enabled, only restart when all strips are ready if (SEGMENT.check3) { if (allReadyToRestart) { needsReset = true; } } else { // Normal mode: restart after individual strip delay uint16_t spin_delay = map(SEGMENT.custom3, 0, 31, 2000, 15000); if ((now - state[STOP_TIME_IDX]) >= spin_delay) { needsReset = true; } } } // Initialize or restart if (needsReset && SEGMENT.check1) { // spin the spinner(s) only if the "Spin me!" checkbox is enabled state[CUR_POS_IDX] = 0; // Set velocity uint16_t speed = map(SEGMENT.speed, 0, 255, 300, 800); if (speed == 300) { // random speed (user selected 0 on speed slider) state[VELOCITY_IDX] = random16(200, 900) * 655; // fixed-point velocity scaling (approx. 65536/100) } else { state[VELOCITY_IDX] = random16(speed - 100, speed + 100) * 655; } // Set slowdown start time uint16_t slowdown = map(SEGMENT.intensity, 0, 255, 3000, 5000); if (slowdown == 3000) { // random slowdown start time (user selected 0 on intensity slider) state[SLOWDOWN_TIME_IDX] = now + random16(2000, 6000); } else { state[SLOWDOWN_TIME_IDX] = now + random16(slowdown - 1000, slowdown + 1000); } state[PHASE_IDX] = 0; state[STOP_TIME_IDX] = 0; state[WOBBLE_STEP_IDX] = 0; state[WOBBLE_TIME_IDX] = 0; state[STOP_POS_IDX] = 0; // Initialize stop position phase = 0; } uint32_t pos_fixed = state[CUR_POS_IDX]; uint32_t velocity = state[VELOCITY_IDX]; // Phase management if (phase == 0) { // Fast spinning phase if ((int32_t)(now - state[SLOWDOWN_TIME_IDX]) >= 0) { phase = 1; state[PHASE_IDX] = 1; } } else if (phase == 1) { // Slowing phase - apply deceleration uint32_t decel = velocity / 80; if (decel < 100) decel = 100; velocity = (velocity > decel) ? velocity - decel : 0; state[VELOCITY_IDX] = velocity; // Check if stopped if (velocity < 2000) { velocity = 0; state[VELOCITY_IDX] = 0; phase = 2; state[PHASE_IDX] = 2; state[WOBBLE_STEP_IDX] = 0; uint16_t stop_pos = (pos_fixed >> 16) % SEGLEN; state[STOP_POS_IDX] = stop_pos; state[WOBBLE_TIME_IDX] = now; } } else if (phase == 2) { // Wobble phase (moves the LED back one and then forward one) uint32_t wobble_step = state[WOBBLE_STEP_IDX]; uint16_t stop_pos = state[STOP_POS_IDX]; uint32_t elapsed = now - state[WOBBLE_TIME_IDX]; if (wobble_step == 0 && elapsed >= 200) { // Move back one LED from stop position uint16_t back_pos = (stop_pos == 0) ? SEGLEN - 1 : stop_pos - 1; pos_fixed = ((uint32_t)back_pos) << 16; state[CUR_POS_IDX] = pos_fixed; state[WOBBLE_STEP_IDX] = 1; state[WOBBLE_TIME_IDX] = now; } else if (wobble_step == 1 && elapsed >= 400) { // Move forward to the stop position pos_fixed = ((uint32_t)stop_pos) << 16; state[CUR_POS_IDX] = pos_fixed; state[WOBBLE_STEP_IDX] = 2; state[WOBBLE_TIME_IDX] = now; } else if (wobble_step == 2 && elapsed >= 300) { // Wobble complete, enter stopped phase phase = 3; state[PHASE_IDX] = 3; state[STOP_TIME_IDX] = now; } } // Update position (phases 0 and 1 only) if (phase == 0 || phase == 1) { pos_fixed += velocity; state[CUR_POS_IDX] = pos_fixed; } // Draw LED for all phases uint16_t pos = (pos_fixed >> 16) % SEGLEN; uint8_t spinnerSize = map(SEGMENT.custom1, 0, 255, 1, 10); // Calculate color once per spinner block (based on strip number, not position) uint8_t hue; if (SEGMENT.check2) { // Each spinner block gets its own color based on strip number uint16_t numSpinners = max(1U, (strips + spinnerSize - 1) / spinnerSize); hue = (uint32_t)(255) * (stripNr / spinnerSize) / numSpinners; } else { // Color changes with position hue = (SEGENV.aux1 * pos) >> 8; } uint32_t color = ColorFromPaletteWLED(SEGPALETTE, hue, 255, LINEARBLEND); // Draw the spinner with configurable size (1-10 LEDs) for (int8_t x = 0; x < spinnerSize; x++) { for (uint8_t y = 0; y < spinnerSize; y++) { uint16_t drawPos = (pos + y) % SEGLEN; int16_t drawStrip = stripNr + x; // Wrap horizontally if needed, or skip if out of bounds if (drawStrip >= 0 && drawStrip < strips) { SEGMENT.setPixelColor(indexToVStrip(drawPos, drawStrip), color); } } } } }; for (unsigned stripNr = 0; stripNr < strips; stripNr++) { // Only run on strips that are multiples of spinnerSize to avoid overlap uint8_t spinnerSize = map(SEGMENT.custom1, 0, 255, 1, 10); if (stripNr % spinnerSize == 0) { virtualStrip::runStrip(stripNr, &state[stripNr * stateVarsPerStrip], settingsChanged, allReadyToRestart, strips); } } } static const char _data_FX_MODE_SPINNINGWHEEL[] PROGMEM = "Spinning Wheel@Speed (0=random),Slowdown (0=random),Spinner size,,Spin delay,Spin me!,Color per block,Sync restart;!,!;!;;m12=1,c1=1,c3=8,o1=1,o3=1"; /* / Lava Lamp 2D effect * Uses particles to simulate rising blobs of "lava" or wax * Particles slowly rise, merge to create organic flowing shapes, and then fall to the bottom to start again * Created by Bob Loeffler using claude.ai * The first slider sets the number of active blobs * The second slider sets the size range of the blobs * The third slider sets the damping value for horizontal blob movement * The Attract checkbox sets the attraction of blobs (checked will make the blobs attract other close blobs horizontally) * The Keep Color Ratio checkbox sets whether we preserve the color ratio when displaying pixels that are in 2 or more overlapping blobs * aux0 keeps track of the blob size value * aux1 keeps track of the number of blobs */ typedef struct LavaParticle { float x, y; // Position float vx, vy; // Velocity float size; // Blob size uint8_t hue; // Color bool active; // will not be displayed if false uint16_t delayTop; // number of frames to wait at top before falling again bool idleTop; // sitting idle at the top uint16_t delayBottom; // number of frames to wait at bottom before rising again bool idleBottom; // sitting idle at the bottom } LavaParticle; static void mode_2D_lavalamp(void) { if (!strip.isMatrix || !SEGMENT.is2D()) FX_FALLBACK_STATIC; // not a 2D set-up const uint16_t cols = SEG_W; const uint16_t rows = SEG_H; constexpr float MAX_BLOB_RADIUS = 20.0f; // cap to prevent frame rate drops on large matrices constexpr size_t MAX_LAVA_PARTICLES = 34; // increasing this value could cause slowness for large matrices constexpr size_t MAX_TOP_FPS_DELAY = 900; // max delay when particles are at the top constexpr size_t MAX_BOTTOM_FPS_DELAY = 1200; // max delay when particles are at the bottom // Allocate per-segment storage if (!SEGENV.allocateData(sizeof(LavaParticle) * MAX_LAVA_PARTICLES)) FX_FALLBACK_STATIC; LavaParticle* lavaParticles = reinterpret_cast(SEGENV.data); // Initialize particles on first call if (SEGENV.call == 0) { for (int i = 0; i < MAX_LAVA_PARTICLES; i++) { lavaParticles[i].active = false; } } // Track particle size and particle count slider changes, re-initialize if either changes uint8_t currentNumParticles = (SEGMENT.intensity >> 3) + 3; uint8_t currentSize = SEGMENT.custom1; if (currentNumParticles > MAX_LAVA_PARTICLES) currentNumParticles = MAX_LAVA_PARTICLES; bool needsReinit = (currentSize != SEGENV.aux0) || (currentNumParticles != SEGENV.aux1); if (needsReinit) { for (int i = 0; i < MAX_LAVA_PARTICLES; i++) { lavaParticles[i].active = false; } SEGENV.aux0 = currentSize; SEGENV.aux1 = currentNumParticles; } uint8_t size = currentSize; uint8_t numParticles = currentNumParticles; // blob size based on matrix width const float minSize = cols * 0.15f; // Minimum 15% of width const float maxSize = cols * 0.4f; // Maximum 40% of width float sizeRange = (maxSize - minSize) * (size / 255.0f); int rangeInt = max(1, (int)(sizeRange)); // calculate the spawning area for the particles const float spawnXStart = cols * 0.20f; const float spawnXWidth = cols * 0.60f; int spawnX = max(1, (int)(spawnXWidth)); bool preserveColorRatio = SEGMENT.check3; // Spawn new particles at the bottom near the center for (int i = 0; i < MAX_LAVA_PARTICLES; i++) { if (!lavaParticles[i].active && hw_random8() < 32) { // spawn when slot available // Spawn in the middle 60% of the matrix width lavaParticles[i].x = spawnXStart + (float)hw_random16(spawnX); lavaParticles[i].y = rows - 1; lavaParticles[i].vx = (hw_random16(7) - 3) / 250.0f; lavaParticles[i].vy = -(hw_random16(20) + 10) / 100.0f * 0.3f; lavaParticles[i].size = minSize + (float)hw_random16(rangeInt); if (lavaParticles[i].size > MAX_BLOB_RADIUS) lavaParticles[i].size = MAX_BLOB_RADIUS; lavaParticles[i].hue = hw_random8(); lavaParticles[i].active = true; // Set random delays when particles are at top and bottom lavaParticles[i].delayTop = hw_random16(MAX_TOP_FPS_DELAY); lavaParticles[i].delayBottom = hw_random16(MAX_BOTTOM_FPS_DELAY); lavaParticles[i].idleBottom = true; break; } } // Fade background slightly for trailing effect SEGMENT.fadeToBlackBy(40); // Update and draw particles int activeCount = 0; unsigned long currentMillis = strip.now; for (int i = 0; i < MAX_LAVA_PARTICLES; i++) { if (!lavaParticles[i].active) continue; activeCount++; // Keep particle count on target by deactivating excess particles if (activeCount > numParticles) { lavaParticles[i].active = false; activeCount--; continue; } LavaParticle *p = &lavaParticles[i]; // Physics update p->x += p->vx; p->y += p->vy; // Optional particle/blob attraction if (SEGMENT.check2) { for (int j = 0; j < MAX_LAVA_PARTICLES; j++) { if (i == j || !lavaParticles[j].active) continue; LavaParticle *other = &lavaParticles[j]; // Skip attraction if moving in same vertical direction (both up or both down) if ((p->vy < 0 && other->vy < 0) || (p->vy > 0 && other->vy > 0)) continue; float dx = other->x - p->x; float dy = other->y - p->y; // Apply weak horizontal attraction only float attractRange = p->size + other->size; float distSq = dx*dx + dy*dy; float attractRangeSq = attractRange * attractRange; if (distSq > 0 && distSq < attractRangeSq) { float dist = sqrt(distSq); // Only compute sqrt when needed float force = (1.0f - (dist / attractRange)) * 0.0001f; p->vx += (dx / dist) * force; } } } // Horizontal oscillation (makes it more organic) float damping= map(SEGMENT.custom2, 0, 255, 97, 87) / 100.0f; p->vx += sin((currentMillis / 1000.0f + i) * 0.5f) * 0.002f; // Reduced oscillation p->vx *= damping; // damping for more or less horizontal drift // Bounce off sides (don't affect vertical velocity) if (p->x < 0) { p->x = 0; p->vx = abs(p->vx); // reverse horizontal } if (p->x >= cols) { p->x = cols - 1; p->vx = -abs(p->vx); // reverse horizontal } // Adjust rise/fall velocity depending on approx distance from heat source (at bottom) // In top 1/4th of rows... if (p->y < rows * .25f) { if (p->vy >= 0) { // if going down, delay the particles so they won't go down immediately if (p->delayTop > 0 && p->idleTop) { p->vy = 0.0f; p->delayTop--; p->idleTop = true; } else { p->vy = 0.01f; p->delayTop = hw_random16(MAX_TOP_FPS_DELAY); p->idleTop = false; } } else if (p->vy <= 0) { // if going up, slow down the rise rate p->vy = -0.03f; } } // In next 1/4th of rows... if (p->y <= rows * .50f && p->y >= rows * .25f) { if (p->vy > 0) { // if going down, speed up the fall rate p->vy = 0.03f; } else if (p->vy <= 0) { // if going up, speed up the rise rate a little more p->vy = -0.05f; } } // In next 1/4th of rows... if (p->y <= rows * .75f && p->y >= rows * .50f) { if (p->vy > 0) { // if going down, speed up the fall rate a little more p->vy = 0.04f; } else if (p->vy <= 0) { // if going up, speed up the rise rate p->vy = -0.03f; } } // In bottom 1/4th of rows... if (p->y > rows * .75f) { if (p->vy >= 0) { // if going down, slow down the fall rate p->vy = 0.02f; } else if (p->vy <= 0) { // if going up, delay the particles so they won't go up immediately if (p->delayBottom > 0 && p->idleBottom) { p->vy = 0.0f; p->delayBottom--; p->idleBottom = true; } else { p->vy = -0.01f; p->delayBottom = hw_random16(MAX_BOTTOM_FPS_DELAY); p->idleBottom = false; } } } // Boundary handling with reversal of direction // When reaching TOP (y=0 area), reverse to fall back down, but need to delay first if (p->y <= 0.5f * p->size) { p->y = 0.5f * p->size; if (p->vy < 0) { p->vy = 0.005f; // set to a tiny positive value to start falling very slowly p->idleTop = true; } } // When reaching BOTTOM (y=rows-1 area), reverse to rise back up, but need to delay first if (p->y >= rows - 0.5f * p->size) { p->y = rows - 0.5f * p->size; if (p->vy > 0) { p->vy = -0.005f; // set to a tiny negative value to start rising very slowly p->idleBottom = true; } } // Get color uint32_t color; color = SEGMENT.color_from_palette(p->hue, true, PALETTE_SOLID_WRAP, 0); // Extract RGB and apply life/opacity uint8_t w = (W(color) * 255) >> 8; uint8_t r = (R(color) * 255) >> 8; uint8_t g = (G(color) * 255) >> 8; uint8_t b = (B(color) * 255) >> 8; // Draw blob with sub-pixel accuracy using bilinear distribution float sizeSq = p->size * p->size; // Get fractional offsets of particle center float fracX = p->x - floorf(p->x); float fracY = p->y - floorf(p->y); int centerX = (int)floorf(p->x); int centerY = (int)floorf(p->y); for (int dy = -(int)p->size - 1; dy <= (int)p->size + 1; dy++) { for (int dx = -(int)p->size - 1; dx <= (int)p->size + 1; dx++) { int px = centerX + dx; int py = centerY + dy; if (px < 0 || px >= cols || py < 0 || py >= rows) continue; // Sub-pixel distance: measure from true float center to pixel center float subDx = dx - fracX; // distance from true center to this pixel's center float subDy = dy - fracY; float distSq = subDx * subDx + subDy * subDy; if (distSq < sizeSq) { float intensity = 1.0f - (distSq / sizeSq); intensity = intensity * intensity; // smooth falloff uint8_t bw = (uint8_t)(w * intensity); uint8_t br = (uint8_t)(r * intensity); uint8_t bg = (uint8_t)(g * intensity); uint8_t bb = (uint8_t)(b * intensity); uint32_t existing = SEGMENT.getPixelColorXY(px, py); uint32_t newColor = RGBW32(br, bg, bb, bw); SEGMENT.setPixelColorXY(px, py, color_add(existing, newColor, preserveColorRatio ? true : false)); } } } } } static const char _data_FX_MODE_2D_LAVALAMP[] PROGMEM = "Lava Lamp@,# of blobs,Blob size,H. Damping,,,Attract,Keep Color Ratio;;!;2;ix=64,c2=192,o2=1,o3=1,pal=47"; /* / Magma effect * 2D magma/lava animation * Adapted from FireLamp_JeeUI implementation (https://github.com/DmytroKorniienko/FireLamp_JeeUI/tree/dev) * Original idea by SottNick, remastered by kostyamat * Adapted to WLED by Bob Loeffler and claude.ai * First slider (speed) is for the speed or flow rate of the moving magma. * Second slider (intensity) is for the height of the magma. * Third slider (lava bombs) is for the number of lava bombs (particles). The max # is 1/2 the number of columns on the 2D matrix. * Fourth slider (gravity) is for how high the lava bombs will go. * The checkbox (check2) is for whether the lava bombs can be seen in the magma or behind it. */ // Draw the magma static void drawMagma(const uint16_t width, const uint16_t height, float *ff_y, float *ff_z, uint8_t *shiftHue) { // Noise parameters - adjust these for different magma characteristics // deltaValue: higher = more detailed/turbulent magma // deltaHue: higher = taller magma structures constexpr uint8_t magmaDeltaValue = 12U; constexpr uint8_t magmaDeltaHue = 10U; uint16_t ff_y_int = (uint16_t)*ff_y; uint16_t ff_z_int = (uint16_t)*ff_z; for (uint16_t i = 0; i < width; i++) { for (uint16_t j = 0; j < height; j++) { // Generate Perlin noise value (0-255) uint8_t noise = perlin8(i * magmaDeltaValue, (j + ff_y_int + hw_random8(2)) * magmaDeltaHue, ff_z_int); uint8_t paletteIndex = qsub8(noise, shiftHue[j]); // Apply the vertical fade gradient CRGB col = SEGMENT.color_from_palette(paletteIndex, false, PALETTE_SOLID_WRAP, 0); // Get color from palette SEGMENT.addPixelColorXY(i, height - 1 - j, col); // magma rises from bottom of display } } } // Move and draw lava bombs (particles) static void drawLavaBombs(const uint16_t width, const uint16_t height, float *particleData, float gravity, uint8_t particleCount) { for (uint16_t i = 0; i < particleCount; i++) { uint16_t idx = i * 4; particleData[idx + 3] -= gravity; particleData[idx + 0] += particleData[idx + 2]; particleData[idx + 1] += particleData[idx + 3]; float posX = particleData[idx + 0]; float posY = particleData[idx + 1]; if (posY > height + height / 4) { particleData[idx + 3] = -particleData[idx + 3] * 0.8f; } if (posY < (float)(height / 8) - 1.0f || posX < 0 || posX >= width) { particleData[idx + 0] = hw_random(0, width * 100) / 100.0f; particleData[idx + 1] = hw_random(0, height * 25) / 100.0f; particleData[idx + 2] = hw_random(-75, 75) / 100.0f; float baseVelocity = hw_random(60, 120) / 100.0f; if (hw_random8() < 50) { baseVelocity *= 1.6f; } particleData[idx + 3] = baseVelocity; continue; } int16_t xi = (int16_t)posX; int16_t yi = (int16_t)posY; if (xi >= 0 && xi < width && yi >= 0 && yi < height) { // Get a random color from the current palette uint8_t randomIndex = hw_random8(64, 128); CRGB pcolor = ColorFromPaletteWLED(SEGPALETTE, randomIndex, 255, LINEARBLEND); // Pre-calculate anti-aliasing weights float xf = posX - xi; float yf = posY - yi; float ix = 1.0f - xf; float iy = 1.0f - yf; uint8_t w0 = 255 * ix * iy; uint8_t w1 = 255 * xf * iy; uint8_t w2 = 255 * ix * yf; uint8_t w3 = 255 * xf * yf; int16_t yFlipped = height - 1 - yi; // Flip Y coordinate SEGMENT.addPixelColorXY(xi, yFlipped, pcolor.scale8(w0)); if (xi + 1 < width) SEGMENT.addPixelColorXY(xi + 1, yFlipped, pcolor.scale8(w1)); if (yFlipped - 1 >= 0) SEGMENT.addPixelColorXY(xi, yFlipped - 1, pcolor.scale8(w2)); if (xi + 1 < width && yFlipped - 1 >= 0) SEGMENT.addPixelColorXY(xi + 1, yFlipped - 1, pcolor.scale8(w3)); } } } static void mode_2D_magma(void) { if (!strip.isMatrix || !SEGMENT.is2D()) FX_FALLBACK_STATIC; // not a 2D set-up const uint16_t width = SEG_W; const uint16_t height = SEG_H; const uint8_t MAGMA_MAX_PARTICLES = width / 2; if (MAGMA_MAX_PARTICLES < 2) FX_FALLBACK_STATIC; // matrix too narrow for lava bombs constexpr size_t SETTINGS_SUM_BYTES = 4; // 4 bytes for settings sum // Allocate memory: particles (4 floats each) + 2 floats for noise counters + shiftHue cache + settingsSum const uint16_t dataSize = (MAGMA_MAX_PARTICLES * 4 + 2) * sizeof(float) + height * sizeof(uint8_t) + SETTINGS_SUM_BYTES; if (!SEGENV.allocateData(dataSize)) FX_FALLBACK_STATIC; // allocation failed float* particleData = reinterpret_cast(SEGENV.data); float* ff_y = &particleData[MAGMA_MAX_PARTICLES * 4]; float* ff_z = &particleData[MAGMA_MAX_PARTICLES * 4 + 1]; uint32_t* settingsSumPtr = reinterpret_cast(&particleData[MAGMA_MAX_PARTICLES * 4 + 2]); uint8_t* shiftHue = reinterpret_cast(reinterpret_cast(settingsSumPtr) + SETTINGS_SUM_BYTES); // Check if settings changed uint32_t settingsKey = (uint32_t)SEGMENT.speed | ((uint32_t)SEGMENT.intensity << 8) | ((uint32_t)SEGMENT.custom1 << 16) | ((uint32_t)SEGMENT.custom2 << 24); bool settingsChanged = (*settingsSumPtr != settingsKey); if (SEGENV.call == 0 || settingsChanged) { // Intensity slider controls magma height uint16_t intensity = SEGMENT.intensity; uint16_t fadeRange = map(intensity, 0, 255, height / 3, height); // shiftHue controls the vertical color gradient (magma fades out toward top) for (uint16_t j = 0; j < height; j++) { if (j < fadeRange) { // prevent division issues and ensure smooth gradient if (fadeRange > 1) { shiftHue[j] = (uint8_t)(j * 255 / (fadeRange - 1)); } else { shiftHue[j] = 0; // Single row magma = no fade } } else { shiftHue[j] = 255; } } // Initialize all particles for (uint16_t i = 0; i < MAGMA_MAX_PARTICLES; i++) { uint16_t idx = i * 4; particleData[idx + 0] = hw_random(0, width * 100) / 100.0f; particleData[idx + 1] = hw_random(0, height * 25) / 100.0f; particleData[idx + 2] = hw_random(-75, 75) / 100.0f; float baseVelocity = hw_random(60, 120) / 100.0f; if (hw_random8() < 50) { baseVelocity *= 1.6f; } particleData[idx + 3] = baseVelocity; } *ff_y = 0.0f; *ff_z = 0.0f; *settingsSumPtr = settingsKey; } if (!shiftHue) FX_FALLBACK_STATIC; // safety check // Speed control float speedfactor = SEGMENT.speed / 255.0f; speedfactor = speedfactor * speedfactor * 1.5f; if (speedfactor < 0.001f) speedfactor = 0.001f; // Gravity control float gravity = map(SEGMENT.custom2, 0, 255, 5, 20) / 100.0f; // Number of particles (lava bombs) uint8_t particleCount = map(SEGMENT.custom1, 0, 255, 0, MAGMA_MAX_PARTICLES); particleCount = constrain(particleCount, 0, MAGMA_MAX_PARTICLES); // Draw lava bombs in front of magma (or behind it) if (SEGMENT.check2) { drawMagma(width, height, ff_y, ff_z, shiftHue); SEGMENT.fadeToBlackBy(70); // Dim the entire display to create trailing effect if (particleCount > 0) drawLavaBombs(width, height, particleData, gravity, particleCount); } else { if (particleCount > 0) drawLavaBombs(width, height, particleData, gravity, particleCount); SEGMENT.fadeToBlackBy(70); // Dim the entire display to create trailing effect drawMagma(width, height, ff_y, ff_z, shiftHue); } // noise counters based on speed slider *ff_y += speedfactor * 2.0f; *ff_z += speedfactor; SEGENV.step++; } static const char _data_FX_MODE_2D_MAGMA[] PROGMEM = "Magma@Flow rate,Magma height,Lava bombs,Gravity,,,Bombs in front;;!;2;ix=192,c2=32,o2=1,pal=35"; /* / Ants (created by making modifications to the Rolling Balls code) - Bob Loeffler 2025 * First slider is for the ants' speed. * Second slider is for the # of ants. * Third slider is for the Ants' size. * Fourth slider (custom2) is for blurring the LEDs in the segment. * Checkbox1 is for Gathering food (enabled if you want the ants to gather food, disabled if they are just walking). * We will switch directions when they get to the beginning or end of the segment when gathering food. * When gathering food, the Pass By option will automatically be enabled so they can drop off their food easier (and look for more food). * Checkbox2 is for Smear mode (enabled is smear pixel colors, disabled is no smearing) * Checkbox3 is for whether the ants will bump into each other (disabled) or just pass by each other (enabled) */ // Ant structure representing each ant's state struct Ant { unsigned long lastBumpUpdate; // the last time the ant bumped into another ant bool hasFood; float velocity; float position; // (0.0 to 1.0 range) }; constexpr unsigned MAX_ANTS = 32; constexpr float MIN_COLLISION_TIME_MS = 2.0f; constexpr float VELOCITY_MIN = 2.0f; constexpr float VELOCITY_MAX = 10.0f; constexpr unsigned ANT_SIZE_MIN = 1; constexpr unsigned ANT_SIZE_MAX = 20; // Helper function to get food pixel color based on ant and background colors static uint32_t getFoodColor(uint32_t antColor, uint32_t backgroundColor) { if (antColor == WHITE) return (backgroundColor == YELLOW) ? GRAY : YELLOW; return (backgroundColor == WHITE) ? YELLOW : WHITE; } // Helper function to handle ant boundary wrapping or bouncing static void handleBoundary(Ant& ant, float& position, bool gatherFood, bool atStart, unsigned long currentTime) { if (gatherFood) { // Bounce mode: reverse direction and update food status position = atStart ? 0.0f : 1.0f; ant.velocity = -ant.velocity; ant.lastBumpUpdate = currentTime; ant.position = position; ant.hasFood = atStart; // Has food when leaving start, drops it at end } else { // Wrap mode: teleport to opposite end position = atStart ? 1.0f : 0.0f; ant.lastBumpUpdate = currentTime; ant.position = position; } } // Helper function to calculate ant color static uint32_t getAntColor(int antIndex, int numAnts, bool usePalette) { if (usePalette) return SEGMENT.color_from_palette(antIndex * 255 / numAnts, false, (paletteBlend == 1 || paletteBlend == 3), 255); // Alternate between two colors for default palette return (antIndex % 3 == 1) ? SEGCOLOR(0) : SEGCOLOR(2); } // Helper function to render a single ant pixel with food handling static void renderAntPixel(int pixelIndex, int pixelOffset, int antSize, const Ant& ant, uint32_t antColor, uint32_t backgroundColor, bool gatherFood) { bool isMovingBackward = (ant.velocity < 0); bool isFoodPixel = gatherFood && ant.hasFood && ((isMovingBackward && pixelOffset == 0) || (!isMovingBackward && pixelOffset == antSize - 1)); if (isFoodPixel) { SEGMENT.setPixelColor(pixelIndex, getFoodColor(antColor, backgroundColor)); } else { SEGMENT.setPixelColor(pixelIndex, antColor); } } static void mode_ants(void) { if (SEGLEN <= 1) FX_FALLBACK_STATIC; // Allocate memory for ant data uint32_t backgroundColor = SEGCOLOR(1); unsigned dataSize = sizeof(Ant) * MAX_ANTS; if (!SEGENV.allocateData(dataSize)) FX_FALLBACK_STATIC; // Allocation failed Ant* ants = reinterpret_cast(SEGENV.data); // Extract configuration from segment settings unsigned numAnts = min(1 + (SEGLEN * SEGMENT.intensity >> 12), MAX_ANTS); bool gatherFood = SEGMENT.check1; bool SmearMode = SEGMENT.check2; bool passBy = SEGMENT.check3 || gatherFood; // global no‑collision when gathering food is enabled unsigned antSize = map(SEGMENT.custom1, 0, 255, ANT_SIZE_MIN, ANT_SIZE_MAX) + (gatherFood ? 1 : 0); // Initialize ants on first call if (SEGENV.call == 0) { int confusedAntIndex = hw_random(0, numAnts); // the first random ant to go backwards for (int i = 0; i < MAX_ANTS; i++) { ants[i].lastBumpUpdate = strip.now; // Random velocity float velocity = VELOCITY_MIN + (VELOCITY_MAX - VELOCITY_MIN) * hw_random16(1000, 5000) / 5000.0f; // One random ant moves in opposite direction ants[i].velocity = (i == confusedAntIndex) ? -velocity : velocity; // Random starting position (0.0 to 1.0) ants[i].position = hw_random16(0, 10000) / 10000.0f; // Ants don't have food yet ants[i].hasFood = false; } } // Calculate time conversion factor based on speed slider float timeConversionFactor = float(scale8(8, 255 - SEGMENT.speed) + 1) * 20000.0f; // Clear background if not in Smear mode if (!SmearMode) SEGMENT.fill(backgroundColor); // Update and render each ant for (int i = 0; i < numAnts; i++) { float timeSinceLastUpdate = float(int(strip.now - ants[i].lastBumpUpdate)) / timeConversionFactor; float newPosition = ants[i].position + ants[i].velocity * timeSinceLastUpdate; // Reset ants that wandered too far off-track (e.g., after intensity change) if (newPosition < -0.5f || newPosition > 1.5f) { newPosition = ants[i].position = hw_random16(0, 10000) / 10000.0f; ants[i].lastBumpUpdate = strip.now; } // Handle boundary conditions (bounce or wrap) if (newPosition <= 0.0f && ants[i].velocity < 0.0f) { handleBoundary(ants[i], newPosition, gatherFood, true, strip.now); } else if (newPosition >= 1.0f && ants[i].velocity > 0.0f) { handleBoundary(ants[i], newPosition, gatherFood, false, strip.now); } // Handle collisions between ants (if not passing by) if (!passBy) { for (int j = i + 1; j < numAnts; j++) { if (fabsf(ants[j].velocity - ants[i].velocity) < 0.001f) continue; // Moving in same direction at same speed; avoids tiny denominators // Calculate collision time using physics - collisionTime formula adapted from rolling_balls float timeOffset = float(int(ants[j].lastBumpUpdate - ants[i].lastBumpUpdate)); float collisionTime = (timeConversionFactor * (ants[i].position - ants[j].position) + ants[i].velocity * timeOffset) / (ants[j].velocity - ants[i].velocity); // Check if collision occurred in valid time window float timeSinceJ = float(int(strip.now - ants[j].lastBumpUpdate)); if (collisionTime > MIN_COLLISION_TIME_MS && collisionTime < timeSinceJ) { // Update positions to collision point float adjustedTime = (collisionTime + float(int(ants[j].lastBumpUpdate - ants[i].lastBumpUpdate))) / timeConversionFactor; ants[i].position += ants[i].velocity * adjustedTime; ants[j].position = ants[i].position; // Update collision time unsigned long collisionMoment = static_cast(collisionTime + 0.5f) + ants[j].lastBumpUpdate; ants[i].lastBumpUpdate = collisionMoment; ants[j].lastBumpUpdate = collisionMoment; // Reverse the ant with greater speed magnitude if (fabsf(ants[i].velocity) > fabsf(ants[j].velocity)) { ants[i].velocity = -ants[i].velocity; } else { ants[j].velocity = -ants[j].velocity; } // Recalculate position after collision newPosition = ants[i].position + ants[i].velocity * float(int(strip.now - ants[i].lastBumpUpdate)) / timeConversionFactor; } } } // Clamp position to valid range newPosition = constrain(newPosition, 0.0f, 1.0f); unsigned pixelPosition = roundf(newPosition * (SEGLEN - 1)); // Determine ant color uint32_t antColor = getAntColor(i, numAnts, SEGMENT.palette != 0); // Render ant pixels for (int pixelOffset = 0; pixelOffset < antSize; pixelOffset++) { unsigned currentPixel = pixelPosition + pixelOffset; if (currentPixel >= SEGLEN) break; renderAntPixel(currentPixel, pixelOffset, antSize, ants[i], antColor, backgroundColor, gatherFood); } // Update ant state ants[i].lastBumpUpdate = strip.now; ants[i].position = newPosition; } SEGMENT.blur(SEGMENT.custom2>>1); } static const char _data_FX_MODE_ANTS[] PROGMEM = "Ants@Ant speed,# of ants,Ant size,Blur,,Gathering food,Smear,Pass by;!,!,!;!;1;sx=192,ix=255,c1=32,c2=0,o1=1,o3=1"; /* / Morse Code by Bob Loeffler * Adapted from code by automaticaddison.com and then optimized by claude.ai * aux0 is the pattern offset for scrolling * aux1 saves settings: check2 (1 bit), check3 (1 bit), text hash (4 bits) and pattern length (10 bits) * The first slider (sx) selects the scrolling speed * The second slider selects the color mode (lower half selects color wheel, upper half selects color palettes) * Checkbox1 displays all letters in a word with the same color * Checkbox2 displays punctuation or not * Checkbox3 displays the End-of-message code or not * We get the text from the SEGMENT.name and convert it to morse code * This effect uses a bit array, instead of bool array, for efficient storage - 8x memory reduction (128 bytes vs 1024 bytes) * * Morse Code rules: * - a dot is 1 pixel/LED; a dash is 3 pixels/LEDs * - there is 1 space between each dot or dash that make up a letter/number/punctuation * - there are 3 spaces between each letter/number/punctuation * - there are 7 spaces between each word */ // Bit manipulation macros #define SET_BIT8(arr, i) ((arr)[(i) >> 3] |= (1 << ((i) & 7))) #define GET_BIT8(arr, i) (((arr)[(i) >> 3] & (1 << ((i) & 7))) != 0) // Build morse code pattern into a buffer static void build_morsecode_pattern(const char *morse_code, uint8_t *pattern, uint8_t *wordIndex, uint16_t &index, uint8_t currentWord, int maxSize) { const char *c = morse_code; // Build the dots and dashes into pattern array while (*c != '\0') { // it's a dot which is 1 pixel if (*c == '.') { if (index >= maxSize - 1) return; SET_BIT8(pattern, index); wordIndex[index] = currentWord; index++; } else { // Must be a dash which is 3 pixels if (index >= maxSize - 3) return; SET_BIT8(pattern, index); wordIndex[index] = currentWord; index++; SET_BIT8(pattern, index); wordIndex[index] = currentWord; index++; SET_BIT8(pattern, index); wordIndex[index] = currentWord; index++; } c++; // 1 space between parts of a letter/number/punctuation (but not after the last one) if (*c != '\0') { if (index >= maxSize) return; wordIndex[index] = currentWord; index++; } } // 3 spaces between two letters/numbers/punctuation if (index >= maxSize - 2) return; wordIndex[index] = currentWord; index++; if (index >= maxSize - 1) return; wordIndex[index] = currentWord; index++; if (index >= maxSize) return; wordIndex[index] = currentWord; index++; } static void mode_morsecode(void) { if (SEGLEN < 1) FX_FALLBACK_STATIC; // A-Z in Morse Code static const char * letters[] = {".-", "-...", "-.-.", "-..", ".", "..-.", "--.", "....", "..", ".---", "-.-", ".-..", "--", "-.", "---", ".--.", "--.-", ".-.", "...", "-", "..-", "...-", ".--", "-..-", "-.--", "--.."}; // 0-9 in Morse Code static const char * numbers[] = {"-----", ".----", "..---", "...--", "....-", ".....", "-....", "--...", "---..", "----."}; // Punctuation in Morse Code struct PunctuationMapping { char character; const char* code; }; static const PunctuationMapping punctuation[] = { {'.', ".-.-.-"}, {',', "--..--"}, {'?', "..--.."}, {':', "---..."}, {'-', "-....-"}, {'!', "-.-.--"}, {'&', ".-..."}, {'@', ".--.-."}, {')', "-.--.-"}, {'(', "-.--."}, {'/', "-..-."}, {'\'', ".----."} }; // Get the text to display char text[WLED_MAX_SEGNAME_LEN+1] = {'\0'}; size_t len = 0; if (SEGMENT.name) len = strlen(SEGMENT.name); if (len == 0) { strcpy_P(text, PSTR("I Love WLED!")); } else { strcpy(text, SEGMENT.name); } // Convert to uppercase in place for (char *p = text; *p; p++) { *p = toupper(*p); } // Allocate per-segment storage for pattern (1023 bits = 127 bytes) + word index array (1024 bytes) + word count (1 byte) constexpr size_t MORSECODE_MAX_PATTERN_SIZE = 1023; constexpr size_t MORSECODE_PATTERN_BYTES = (MORSECODE_MAX_PATTERN_SIZE + 7) / 8; // 128 bytes constexpr size_t MORSECODE_WORD_INDEX_BYTES = MORSECODE_MAX_PATTERN_SIZE; // 1 byte per bit position constexpr size_t MORSECODE_WORD_COUNT_BYTES = 1; // 1 byte for word count if (!SEGENV.allocateData(MORSECODE_PATTERN_BYTES + MORSECODE_WORD_INDEX_BYTES + MORSECODE_WORD_COUNT_BYTES)) FX_FALLBACK_STATIC; uint8_t* morsecodePattern = reinterpret_cast(SEGENV.data); uint8_t* wordIndexArray = reinterpret_cast(SEGENV.data + MORSECODE_PATTERN_BYTES); uint8_t* wordCountPtr = reinterpret_cast(SEGENV.data + MORSECODE_PATTERN_BYTES + MORSECODE_WORD_INDEX_BYTES); // SEGENV.aux1 stores: [bit 15: check2] [bit 14: check3] [bits 10-13: text hash (4 bits)] [bits 0-9: pattern length] bool lastCheck2 = (SEGENV.aux1 & 0x8000) != 0; bool lastCheck3 = (SEGENV.aux1 & 0x4000) != 0; uint16_t lastHashBits = (SEGENV.aux1 >> 10) & 0xF; // 4 bits of hash uint16_t patternLength = SEGENV.aux1 & 0x3FF; // Lower 10 bits for length (up to 1023) // Compute text hash uint16_t textHash = 0; for (char *p = text; *p; p++) { textHash = ((textHash << 5) + textHash) + *p; } uint16_t currentHashBits = (textHash >> 12) & 0xF; // Use upper 4 bits of hash bool textChanged = (currentHashBits != lastHashBits) && (SEGENV.call > 0); // Check if we need to rebuild the pattern bool needsRebuild = (SEGENV.call == 0) || textChanged || (SEGMENT.check2 != lastCheck2) || (SEGMENT.check3 != lastCheck3); // Initialize on first call or rebuild pattern if (needsRebuild) { patternLength = 0; // Clear the bit array and word index array first memset(morsecodePattern, 0, MORSECODE_PATTERN_BYTES); memset(wordIndexArray, 0, MORSECODE_WORD_INDEX_BYTES); // Track current word index uint8_t currentWordIndex = 0; // Build complete morse code pattern for (char *c = text; *c; c++) { if (patternLength >= MORSECODE_MAX_PATTERN_SIZE - 10) break; if (*c >= 'A' && *c <= 'Z') { build_morsecode_pattern(letters[*c - 'A'], morsecodePattern, wordIndexArray, patternLength, currentWordIndex, MORSECODE_MAX_PATTERN_SIZE); } else if (*c >= '0' && *c <= '9') { build_morsecode_pattern(numbers[*c - '0'], morsecodePattern, wordIndexArray, patternLength, currentWordIndex, MORSECODE_MAX_PATTERN_SIZE); } else if (*c == ' ') { // Space between words - increment word index for next word currentWordIndex++; // Add 4 additional spaces (7 total with the 3 after each letter) for (int x = 0; x < 4; x++) { if (patternLength >= MORSECODE_MAX_PATTERN_SIZE) break; wordIndexArray[patternLength] = currentWordIndex; patternLength++; } } else if (SEGMENT.check2) { const char *punctuationCode = nullptr; for (const auto& p : punctuation) { if (*c == p.character) { punctuationCode = p.code; break; } } if (punctuationCode) { build_morsecode_pattern(punctuationCode, morsecodePattern, wordIndexArray, patternLength, currentWordIndex, MORSECODE_MAX_PATTERN_SIZE); } } } if (SEGMENT.check3) { build_morsecode_pattern(".-.-.", morsecodePattern, wordIndexArray, patternLength, currentWordIndex, MORSECODE_MAX_PATTERN_SIZE); } for (int x = 0; x < 7; x++) { if (patternLength >= MORSECODE_MAX_PATTERN_SIZE) break; wordIndexArray[patternLength] = currentWordIndex; patternLength++; } // Store the total number of words (currentWordIndex + 1 because it's 0-indexed) *wordCountPtr = currentWordIndex + 1; // Store pattern length, checkbox states, and hash bits in aux1 SEGENV.aux1 = patternLength | (currentHashBits << 10) | (SEGMENT.check2 ? 0x8000 : 0) | (SEGMENT.check3 ? 0x4000 : 0); // Reset the scroll offset SEGENV.aux0 = 0; } // if pattern is empty for some reason, display black background only if (patternLength == 0) { SEGMENT.fill(BLACK); return; } // Update offset to make the morse code scroll // Use step for scroll timing only uint32_t cycleTime = 50 + (255 - SEGMENT.speed)*3; uint32_t it = strip.now / cycleTime; if (SEGENV.step != it) { SEGENV.aux0++; SEGENV.step = it; } // Clear background SEGMENT.fill(BLACK); // Draw the scrolling pattern int offset = SEGENV.aux0 % patternLength; // Get the word count and calculate color spacing uint8_t wordCount = *wordCountPtr; if (wordCount == 0) wordCount = 1; uint8_t colorSpacing = 255 / wordCount; // Distribute colors evenly across color wheel/palette for (int i = 0; i < SEGLEN; i++) { int patternIndex = (offset + i) % patternLength; if (GET_BIT8(morsecodePattern, patternIndex)) { uint8_t wordIdx = wordIndexArray[patternIndex]; if (SEGMENT.check1) { // make each word a separate color if (SEGMENT.custom3 < 16) // use word index to select base color, add slight offset for animation SEGMENT.setPixelColor(i, SEGMENT.color_wheel((wordIdx * colorSpacing) + (SEGENV.aux0 / 4))); else SEGMENT.setPixelColor(i, SEGMENT.color_from_palette(wordIdx * colorSpacing, true, PALETTE_SOLID_WRAP, 0)); } else { if (SEGMENT.custom3 < 16) SEGMENT.setPixelColor(i, SEGMENT.color_wheel(SEGENV.aux0 + i)); else SEGMENT.setPixelColor(i, SEGMENT.color_from_palette(i, true, PALETTE_SOLID_WRAP, 0)); } } } } static const char _data_FX_MODE_MORSECODE[] PROGMEM = "Morse Code@Speed,,,,Color mode,Color by Word,Punctuation,EndOfMessage;;!;1;sx=192,c3=8,o1=1,o2=1"; ///////////////////// // UserMod Class // ///////////////////// class UserFxUsermod : public Usermod { private: public: void setup() override { strip.addEffect(255, &mode_diffusionfire, _data_FX_MODE_DIFFUSIONFIRE); strip.addEffect(255, &mode_spinning_wheel, _data_FX_MODE_SPINNINGWHEEL); strip.addEffect(255, &mode_2D_lavalamp, _data_FX_MODE_2D_LAVALAMP); strip.addEffect(255, &mode_2D_magma, _data_FX_MODE_2D_MAGMA); strip.addEffect(255, &mode_ants, _data_FX_MODE_ANTS); strip.addEffect(255, &mode_morsecode, _data_FX_MODE_MORSECODE); //////////////////////////////////////// // add your effect function(s) here // //////////////////////////////////////// // use id=255 for all custom user FX (the final id is assigned when adding the effect) // strip.addEffect(255, &mode_your_effect, _data_FX_MODE_YOUR_EFFECT); // strip.addEffect(255, &mode_your_effect2, _data_FX_MODE_YOUR_EFFECT2); // strip.addEffect(255, &mode_your_effect3, _data_FX_MODE_YOUR_EFFECT3); } /////////////////////////////////////////////////////////////////////////////////////////////// // If you want configuration options in the usermod settings page, implement these methods // /////////////////////////////////////////////////////////////////////////////////////////////// // void addToConfig(JsonObject& root) override // { // JsonObject top = root.createNestedObject(FPSTR("User FX")); // top["myConfigValue"] = myConfigValue; // } // bool readFromConfig(JsonObject& root) override // { // JsonObject top = root[FPSTR("User FX")]; // bool configComplete = !top.isNull(); // configComplete &= getJsonValue(top["myConfigValue"], myConfigValue); // return configComplete; // } void loop() override {} // nothing to do in the loop uint16_t getId() override { return USERMOD_ID_USER_FX; } }; static UserFxUsermod user_fx; REGISTER_USERMOD(user_fx); ================================================ FILE: usermods/usermod_rotary_brightness_color/README.md ================================================ # Rotary Encoder (Brightness and Color) V2 usermod that enables changing brightness and color using a rotary encoder change between modes by pressing a button (many encoders have one included) it will wait for AUTOSAVE_SETTLE_MS milliseconds. a "settle" period in case there are other changes (any change will extend the "settle" period). 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: 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 define `USERMOD_ROTARY_ENCODER_BRIGHTNESS_COLOR` e.g. `#define USERMOD_ROTARY_ENCODER_BRIGHTNESS_COLOR` in my_config.h or add `-D USERMOD_ROTARY_ENCODER_BRIGHTNESS_COLOR` to `build_flags` in platformio_override.ini ### Define Your Options Open Usermod Settings in WLED to change settings: `fadeAmount` - how many points to fade the Neopixel with each step of the rotary encoder (default 5) `pin[3]` - pins to connect to the rotary encoder: - `pin[0]` is pin A on your rotary encoder - `pin[1]` is pin B on your rotary encoder - `pin[2]` is the button on your rotary encoder (optional, set to -1 to disable the button and the rotary encoder will control brightness only) ### PlatformIO requirements No special requirements. ## Change Log - 2021-07
Upgraded to work with the latest WLED code, and make settings configurable in Usermod Settings - 2025-03
Upgraded to work with the latest WLED code ================================================ FILE: usermods/usermod_rotary_brightness_color/library.json ================================================ { "name": "usermod_rotary_brightness_color", "build": { "libArchive": false } } ================================================ FILE: usermods/usermod_rotary_brightness_color/usermod_rotary_brightness_color.cpp ================================================ #include "wled.h" //v2 usermod that allows to change brightness and color using a rotary encoder, //change between modes by pressing a button (many encoders have one included) class RotaryEncoderBrightnessColor : public Usermod { private: //Private class members. You can declare variables and functions only accessible to your usermod here unsigned long lastTime = 0; unsigned long currentTime; unsigned long loopTime; unsigned char select_state = 0; // 0 = brightness 1 = color unsigned char button_state = HIGH; unsigned char prev_button_state = HIGH; CRGB fastled_col; CHSV prim_hsv; int16_t new_val; unsigned char Enc_A; unsigned char Enc_B; unsigned char Enc_A_prev = 0; // private class members configurable by Usermod Settings (defaults set inside readFromConfig()) int8_t pins[3]; // pins[0] = DT from encoder, pins[1] = CLK from encoder, pins[2] = CLK from encoder (optional) int fadeAmount; // how many points to fade the Neopixel with each step public: //Functions called by WLED /* * 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() { //Serial.println("Hello from my usermod!"); pinMode(pins[0], INPUT_PULLUP); pinMode(pins[1], INPUT_PULLUP); if(pins[2] >= 0) pinMode(pins[2], INPUT_PULLUP); currentTime = millis(); loopTime = currentTime; } /* * 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 if (currentTime >= (loopTime + 2)) // 2ms since last check of encoder = 500Hz { if(pins[2] >= 0) { button_state = digitalRead(pins[2]); if (prev_button_state != button_state) { if (button_state == LOW) { if (select_state == 1) { select_state = 0; } else { select_state = 1; } prev_button_state = button_state; } else { prev_button_state = button_state; } } } int Enc_A = digitalRead(pins[0]); // Read encoder pins int Enc_B = digitalRead(pins[1]); if ((!Enc_A) && (Enc_A_prev)) { // A has gone from high to low if (Enc_B == HIGH) { // B is high so clockwise if (select_state == 0) { if (bri + fadeAmount <= 255) bri += fadeAmount; // increase the brightness, dont go over 255 } else { fastled_col.red = colPri[0]; fastled_col.green = colPri[1]; fastled_col.blue = colPri[2]; prim_hsv = rgb2hsv_approximate(fastled_col); new_val = (int16_t)prim_hsv.h + fadeAmount; if (new_val > 255) new_val -= 255; // roll-over if bigger than 255 if (new_val < 0) new_val += 255; // roll-over if smaller than 0 prim_hsv.h = (byte)new_val; hsv2rgb_rainbow(prim_hsv, fastled_col); colPri[0] = fastled_col.red; colPri[1] = fastled_col.green; colPri[2] = fastled_col.blue; } } else if (Enc_B == LOW) { // B is low so counter-clockwise if (select_state == 0) { if (bri - fadeAmount >= 0) bri -= fadeAmount; // decrease the brightness, dont go below 0 } else { fastled_col.red = colPri[0]; fastled_col.green = colPri[1]; fastled_col.blue = colPri[2]; prim_hsv = rgb2hsv_approximate(fastled_col); new_val = (int16_t)prim_hsv.h - fadeAmount; if (new_val > 255) new_val -= 255; // roll-over if bigger than 255 if (new_val < 0) new_val += 255; // roll-over if smaller than 0 prim_hsv.h = (byte)new_val; hsv2rgb_rainbow(prim_hsv, fastled_col); colPri[0] = fastled_col.red; colPri[1] = fastled_col.green; colPri[2] = fastled_col.blue; } } //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(CALL_MODE_BUTTON); updateInterfaces(CALL_MODE_BUTTON); } Enc_A_prev = Enc_A; // Store value of A for next time loopTime = currentTime; // Updates loopTime } } void addToConfig(JsonObject& root) { JsonObject top = root.createNestedObject("rotEncBrightness"); top["fadeAmount"] = fadeAmount; JsonArray pinArray = top.createNestedArray("pin"); pinArray.add(pins[0]); pinArray.add(pins[1]); pinArray.add(pins[2]); } /* * This example uses a more robust method of checking for missing values in the config, and setting back to defaults: * - The getJsonValue() function copies the value to the variable only if the key requested is present, returning false with no copy if the value isn't present * - configComplete is used to return false if any value is missing, not just if the main object is missing * - The defaults are loaded every time readFromConfig() is run, not just once after boot * * This ensures that missing values are added to the config, with their default values, in the rare but plausible cases of: * - a single value being missing at boot, e.g. if the Usermod was upgraded and a new setting was added * - a single value being missing after boot (e.g. if the cfg.json was manually edited and a value was removed) * * If configComplete is false, the default values are already set, and by returning false, WLED now knows it needs to save the defaults by calling addToConfig() */ bool readFromConfig(JsonObject& root) { // set defaults here, they will be set before setup() is called, and if any values parsed from ArduinoJson below are missing, the default will be used instead fadeAmount = 5; pins[0] = -1; pins[1] = -1; pins[2] = -1; JsonObject top = root["rotEncBrightness"]; bool configComplete = !top.isNull(); configComplete &= getJsonValue(top["fadeAmount"], fadeAmount); configComplete &= getJsonValue(top["pin"][0], pins[0]); configComplete &= getJsonValue(top["pin"][1], pins[1]); configComplete &= getJsonValue(top["pin"][2], pins[2]); return configComplete; } }; static RotaryEncoderBrightnessColor usermod_rotary_brightness_color; REGISTER_USERMOD(usermod_rotary_brightness_color); ================================================ FILE: usermods/usermod_v2_HttpPullLightControl/library.json ================================================ { "name": "usermod_v2_HttpPullLightControl", "build": { "libArchive": false } } ================================================ FILE: usermods/usermod_v2_HttpPullLightControl/readme.md ================================================ # usermod_v2_HttpPullLightControl The `usermod_v2_HttpPullLightControl` is a custom user module for WLED that enables remote control over the lighting state and color through HTTP requests. It periodically polls a specified URL to obtain a JSON response containing instructions for controlling individual lights. ## Features * Configure the URL endpoint (only support HTTP for now, no HTTPS) and polling interval via the WLED user interface. * All options from the JSON API are supported (since v0.0.3). See: [https://kno.wled.ge/interfaces/json-api/](https://kno.wled.ge/interfaces/json-api/) * The ability to control the brightness of all lights and the state (on/off) and color of individual lights remotely. * Start or stop an effect and when you run the same effect when its's already running, it won't restart. * The ability to control all these settings per segment. * Remotely turn on/off relays, change segments or presets. * Unique ID generation based on the device's MAC address and a configurable salt value, appended to the request URL for identification. ## Configuration * Enable the `usermod_v2_HttpPullLightControl` via the WLED user interface. * Specify the URL endpoint and polling interval. ## JSON Format and examples * The module sends a GET request to the configured URL, appending a unique identifier as a query parameter: `https://www.example.com/mycustompage.php?id=xxxxxxxx` where xxxxxxx is a 40 character long SHA1 hash of the MAC address combined with a given salt. * Response Format (since v0.0.3) it is eactly the same as the WLED JSON API, see: [https://kno.wled.ge/interfaces/json-api/](https://kno.wled.ge/interfaces/json-api/) After getting the URL (it can be a static file like static.json or a mylogic.php which gives a dynamic response), the response is read and parsed to WLED. * An example of a response to set the individual lights: 0 to RED, 12 to Green and 14 to BLUE. Remember that is will SET lights, you might want to set all the others to black. `{ "seg": { "i": [ 0, "FF0000", 12, "00FF00", 14, "0000FF" ] } }` * Another example setting the first 10 LEDs to RED, LED 40 to a PURPLE (using RGB values) and all LEDs in between OFF (black color) `{ "seg": { "i": [ 0,10, "FF0000", 10,40, "00FF00", 40, [0,100,100] ] } }` * Or first set all lights to black (off), then the LED5 to color RED: `{ "seg": { "i": [ 0,40, "000000", 5, "FF0000" ] } }` * Or use the following example to start an effect, but first we UNFREEZE (frz=false) the segment because it was frozen by individual light control in the previous examples (28=Chase effect, Speed=180m Intensity=128). The three color slots are the slots you see under the color wheel and used by the effect. RED, Black, White in this case. ```json `{ "seg": { "frz": false, "fx": 28, "sx": 200, "ix": 128, "col": [ "FF0000", "000000", "FFFFFF" ] } }` ``` ## Installation 1. Add `usermod_v2_HttpPullLightControl` to your WLED project following the instructions provided in the WLED documentation. 2. Compile by setting the build_flag: -D USERMOD_HTTP_PULL_LIGHT_CONTROL and upload to your ESP32/ESP8266! 3. There are several compile options which you can put in your platformio.ini or platformio_override.ini: * -DUSERMOD_HTTP_PULL_LIGHT_CONTROL ;To Enable the usermod * -DHTTP_PULL_LIGHT_CONTROL_URL="\"`http://mydomain.com/json-response.php`\"" ; The URL which will be requested all the time to set the lights/effects * -DHTTP_PULL_LIGHT_CONTROL_SALT="\"my_very-S3cret_C0de\"" ; A secret SALT which will help by making the ID more safe * -DHTTP_PULL_LIGHT_CONTROL_INTERVAL=30 ; The interval at which the URL is requested in seconds * -DHTTP_PULL_LIGHT_CONTROL_HIDE_SALT ; Do you want to Hide the SALT in the User Interface? If yes, Set this flag. Note that the salt can now only be set via the above -DHTTP_PULL_LIGHT_CONTROL_SALT= setting * -DWLED_AP_SSID="\"Christmas Card\"" ; These flags are not just for my Usermod but you probably want to set them * -DWLED_AP_PASS="\"christmas\"" * -DWLED_OTA_PASS="\"otapw-secret\"" * -DMDNS_NAME="\"christmascard\"" * -DSERVERNAME="\"CHRISTMASCARD\"" * -D ABL_MILLIAMPS_DEFAULT=450 * -D DEFAULT_LED_COUNT=60 ; For a LED Ring of 60 LEDs * -D BTNPIN=41 ; The M5Stack Atom S3 Lite has a button on GPIO41 * -D DATA_PINS=2 ; The M5Stack Atom S3 Lite has a Grove connector on the front, we use this GPIO2 * -D STATUSLED=35 ; The M5Stack Atom S3 Lite has a Multi-Color LED on GPIO35, although I didnt managed to control it * -D IRPIN=4 ; The M5Stack Atom S3 Lite has a IR LED on GPIO4 * -D DEBUG=1 ; Set these DEBUG flags ONLY if you want to debug and read out Serial (using Visual Studio Code - Serial Monitor) * -DDEBUG_LEVEL=5 * -DWLED_DEBUG ## Use Case: Interactive Christmas Cards Imagine distributing interactive Christmas cards embedded with a tiny ESP32 and a string of 20 LEDs to 20 friends. When a friend powers on their card, it connects to their Wi-Fi network and starts polling your server via the `usermod_v2_HttpPullLightControl`. (Tip: Let them scan a QR code to connect to the WLED WiFi, from there they configure their own WiFi). Your server keeps track of how many cards are active at any given time. If all 20 cards are active, your server instructs each card to light up all of its LEDs. However, if only 4 cards are active, your server instructs each card to light up only 4 LEDs. This creates a real-time interactive experience, symbolizing the collective spirit of the holiday season. Each lit LED represents a friend who's thinking about the others, and the visual feedback creates a sense of connection among the group, despite the physical distance. This setup demonstrates a unique way to blend traditional holiday sentiments with modern technology, offering an engaging and memorable experience. ================================================ FILE: usermods/usermod_v2_HttpPullLightControl/usermod_v2_HttpPullLightControl.cpp ================================================ #include "usermod_v2_HttpPullLightControl.h" // add more strings here to reduce flash memory usage const char HttpPullLightControl::_name[] PROGMEM = "HttpPullLightControl"; const char HttpPullLightControl::_enabled[] PROGMEM = "Enable"; static HttpPullLightControl http_pull_usermod; REGISTER_USERMOD(http_pull_usermod); void HttpPullLightControl::setup() { //Serial.begin(115200); // Print version number DEBUG_PRINT(F("HttpPullLightControl version: ")); DEBUG_PRINTLN(HTTP_PULL_LIGHT_CONTROL_VERSION); // Start a nice chase so we know its booting and searching for its first http pull. DEBUG_PRINTLN(F("Starting a nice chase so we now it is booting.")); Segment& seg = strip.getMainSegment(); seg.setMode(28); // Set to chase seg.speed = 200; seg.intensity = 255; seg.setPalette(128); seg.setColor(0, 5263440); seg.setColor(1, 0); seg.setColor(2, 4605510); // Go on with generating a unique ID and splitting the URL into parts uniqueId = generateUniqueId(); // Cache the unique ID DEBUG_PRINT(F("UniqueId calculated: ")); DEBUG_PRINTLN(uniqueId); parseUrl(); DEBUG_PRINTLN(F("HttpPullLightControl successfully setup")); } // This is the main loop function, from here we check the URL and handle the response. // Effects or individual lights are set as a result from this. void HttpPullLightControl::loop() { if (!enabled || offMode) return; // Do nothing when not enabled or powered off if (millis() - lastCheck >= checkInterval * 1000) { DEBUG_PRINTLN(F("Calling checkUrl function")); checkUrl(); lastCheck = millis(); } } // Generate a unique ID based on the MAC address and a SALT String HttpPullLightControl::generateUniqueId() { uint8_t mac[6]; WiFi.macAddress(mac); char macStr[18]; sprintf(macStr, "%02x:%02x:%02x:%02x:%02x:%02x", mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]); // Set the MAC Address to a string and make it UPPERcase String macString = String(macStr); macString.toUpperCase(); DEBUG_PRINT(F("WiFi MAC address is: ")); DEBUG_PRINTLN(macString); DEBUG_PRINT(F("Salt is: ")); DEBUG_PRINTLN(salt); String input = macString + salt; #ifdef ESP8266 // For ESP8266 we use the Hash.h library which is built into the ESP8266 Core return sha1(input); #endif #ifdef ESP32 // For ESP32 we use the mbedtls library which is built into the ESP32 core int status = 0; unsigned char shaResult[20]; // SHA1 produces a hash of 20 bytes (which is 40 HEX characters) mbedtls_sha1_context ctx; mbedtls_sha1_init(&ctx); status = mbedtls_sha1_starts_ret(&ctx); if (status != 0) { DEBUG_PRINTLN(F("Error starting SHA1 checksum calculation")); } status = mbedtls_sha1_update_ret(&ctx, reinterpret_cast(input.c_str()), input.length()); if (status != 0) { DEBUG_PRINTLN(F("Error feeding update buffer into ongoing SHA1 checksum calculation")); } status = mbedtls_sha1_finish_ret(&ctx, shaResult); if (status != 0) { DEBUG_PRINTLN(F("Error finishing SHA1 checksum calculation")); } mbedtls_sha1_free(&ctx); // Convert the Hash to a hexadecimal string char buf[41]; for (int i = 0; i < 20; i++) { sprintf(&buf[i*2], "%02x", shaResult[i]); } return String(buf); #endif } // This function is called when the user updates the Sald and so we need to re-calculate the unique ID void HttpPullLightControl::updateSalt(String newSalt) { DEBUG_PRINTLN(F("Salt updated")); this->salt = newSalt; uniqueId = generateUniqueId(); DEBUG_PRINT(F("New UniqueId is: ")); DEBUG_PRINTLN(uniqueId); } // The function is used to separate the URL in a host part and a path part void HttpPullLightControl::parseUrl() { int firstSlash = url.indexOf('/', 7); // Skip http(s):// host = url.substring(7, firstSlash); path = url.substring(firstSlash); } // This function is called by WLED when the USERMOD config is read bool HttpPullLightControl::readFromConfig(JsonObject& root) { // Attempt to retrieve the nested object for this usermod JsonObject top = root[FPSTR(_name)]; bool configComplete = !top.isNull(); // check if the object exists // Retrieve the values using the getJsonValue function for better error handling configComplete &= getJsonValue(top[FPSTR(_enabled)], enabled, enabled); // default value=enabled configComplete &= getJsonValue(top["checkInterval"], checkInterval, checkInterval); // default value=60 #ifndef HTTP_PULL_LIGHT_CONTROL_HIDE_URL configComplete &= getJsonValue(top["url"], url, url); // default value="http://example.com" #endif #ifndef HTTP_PULL_LIGHT_CONTROL_HIDE_SALT configComplete &= getJsonValue(top["salt"], salt, salt); // default value=your_salt_here #endif return configComplete; } // This function is called by WLED when the USERMOD config is saved in the frontend void HttpPullLightControl::addToConfig(JsonObject& root) { // Create a nested object for this usermod JsonObject top = root.createNestedObject(FPSTR(_name)); // Write the configuration parameters to the nested object top[FPSTR(_enabled)] = enabled; if (enabled==false) // To make it a bit more user-friendly, we unfreeze the main segment after disabling the module. Because individual light control (like for a christmas card) might have been done. strip.getMainSegment().freeze=false; top["checkInterval"] = checkInterval; #ifndef HTTP_PULL_LIGHT_CONTROL_HIDE_URL top["url"] = url; #endif #ifndef HTTP_PULL_LIGHT_CONTROL_HIDE_SALT top["salt"] = salt; updateSalt(salt); // Update the UniqueID #endif parseUrl(); // Re-parse the URL, maybe path and host is changed } // Do the http request here. Note that we can not do https requests with the AsyncTCP library // We do everything Asynchronous, so all callbacks are defined here void HttpPullLightControl::checkUrl() { // Extra Inactivity check to see if AsyncCLient hangs if (client != nullptr && ( millis() - lastActivityTime > inactivityTimeout ) ) { DEBUG_PRINTLN(F("Inactivity detected, deleting client.")); delete client; client = nullptr; } if (client != nullptr && client->connected()) { DEBUG_PRINTLN(F("We are still connected, do nothing")); // Do nothing, Client is still connected return; } if (client != nullptr) { // Delete previous client instance if exists, just to prevent any memory leaks DEBUG_PRINTLN(F("Delete previous instances")); delete client; client = nullptr; } DEBUG_PRINTLN(F("Creating new AsyncClient instance.")); client = new AsyncClient(); if(client) { client->onData([](void *arg, AsyncClient *c, void *data, size_t len) { DEBUG_PRINTLN(F("Data received.")); // Cast arg back to the usermod class instance HttpPullLightControl *instance = (HttpPullLightControl *)arg; instance->lastActivityTime = millis(); // Update lastactivity time when data is received // Convertert to Safe-String char *strData = new char[len + 1]; strncpy(strData, (char*)data, len); strData[len] = '\0'; String responseData = String(strData); //String responseData = String((char *)data); // Make sure its zero-terminated String //responseData[len] = '\0'; delete[] strData; // Do not forget to remove this one instance->handleResponse(responseData); }, this); client->onDisconnect([](void *arg, AsyncClient *c) { DEBUG_PRINTLN(F("Disconnected.")); //Set the class-own client pointer to nullptr if its the current client HttpPullLightControl *instance = static_cast(arg); if (instance->client == c) { delete instance->client; // Delete the client instance instance->client = nullptr; } }, this); client->onTimeout([](void *arg, AsyncClient *c, uint32_t time) { DEBUG_PRINTLN(F("Timeout")); //Set the class-own client pointer to nullptr if its the current client HttpPullLightControl *instance = static_cast(arg); if (instance->client == c) { delete instance->client; // Delete the client instance instance->client = nullptr; } }, this); client->onError([](void *arg, AsyncClient *c, int8_t error) { DEBUG_PRINTLN("Connection error occurred!"); DEBUG_PRINT("Error code: "); DEBUG_PRINTLN(error); //Set the class-own client pointer to nullptr if its the current client HttpPullLightControl *instance = static_cast(arg); if (instance->client == c) { delete instance->client; instance->client = nullptr; } // Do not remove client here, it is maintained by AsyncClient }, this); client->onConnect([](void *arg, AsyncClient *c) { // Cast arg back to the usermod class instance HttpPullLightControl *instance = (HttpPullLightControl *)arg; instance->onClientConnect(c); // Call a method on the instance when the client connects }, this); client->setAckTimeout(ackTimeout); // Just some safety measures because we do not want any memory fillup client->setRxTimeout(rxTimeout); DEBUG_PRINT(F("Connecting to: ")); DEBUG_PRINT(host); DEBUG_PRINT(F(" via port ")); DEBUG_PRINTLN((url.startsWith("https")) ? 443 : 80); // Update lastActivityTime just before sending the request lastActivityTime = millis(); //Try to connect if (!client->connect(host.c_str(), (url.startsWith("https")) ? 443 : 80)) { DEBUG_PRINTLN(F("Failed to initiate connection.")); // Connection failed, so cleanup delete client; client = nullptr; } else { // Connection successfull, wait for callbacks to go on. DEBUG_PRINTLN(F("Connection initiated, awaiting response...")); } } else { DEBUG_PRINTLN(F("Failed to create AsyncClient instance.")); } } // This function is called from the checkUrl function when the connection is establised // We request the data here void HttpPullLightControl::onClientConnect(AsyncClient *c) { DEBUG_PRINT(F("Client connected: ")); DEBUG_PRINTLN(c->connected() ? F("Yes") : F("No")); if (c->connected()) { String request = "GET " + path + (path.indexOf('?') > 0 ? "&id=" : "?id=") + uniqueId + " HTTP/1.1\r\n" "Host: " + host + "\r\n" "Connection: close\r\n" "Accept: application/json\r\n" "Accept-Encoding: identity\r\n" // No compression "User-Agent: ESP32 HTTP Client\r\n\r\n"; // Optional: User-Agent and end with a double rnrn ! DEBUG_PRINT(request.c_str()); auto bytesSent = c->write(request.c_str()); if (bytesSent == 0) { // Connection could not be made DEBUG_PRINT(F("Failed to send HTTP request.")); } else { DEBUG_PRINT(F("Request sent successfully, bytes sent: ")); DEBUG_PRINTLN(bytesSent ); } } } // This function is called when we receive data after connecting and doing our request // It parses the JSON data to WLED void HttpPullLightControl::handleResponse(String& responseStr) { DEBUG_PRINTLN(F("Received response for handleResponse.")); // Get a Bufferlock, we can not use doc if (!requestJSONBufferLock(myLockId)) { DEBUG_PRINT(F("ERROR: Can not request JSON Buffer Lock, number: ")); DEBUG_PRINTLN(myLockId); return; } // Search for two linebreaks between headers and content int bodyPos = responseStr.indexOf("\r\n\r\n"); if (bodyPos > 0) { String jsonStr = responseStr.substring(bodyPos + 4); // +4 Skip the two CRLFs jsonStr.trim(); DEBUG_PRINTLN("Response: "); DEBUG_PRINTLN(jsonStr); // Check for valid JSON, otherwise we brick the program runtime if (jsonStr[0] == '{' || jsonStr[0] == '[') { // Attempt to deserialize the JSON response DeserializationError error = deserializeJson(*pDoc, jsonStr); if (error == DeserializationError::Ok) { // Get JSON object from th doc JsonObject obj = pDoc->as(); // Parse the object throuhg deserializeState (use CALL_MODE_NO_NOTIFY or OR CALL_MODE_DIRECT_CHANGE) deserializeState(obj, CALL_MODE_NO_NOTIFY); } else { // If there is an error in deserialization, exit the function DEBUG_PRINT(F("DeserializationError: ")); DEBUG_PRINTLN(error.c_str()); } } else { DEBUG_PRINTLN(F("Invalid JSON response")); } } else { DEBUG_PRINTLN(F("No body found in the response")); } // Release the BufferLock again releaseJSONBufferLock(); } ================================================ FILE: usermods/usermod_v2_HttpPullLightControl/usermod_v2_HttpPullLightControl.h ================================================ #pragma once /* * Usermod: HttpPullLightControl * Versie: 0.0.4 * Repository: https://github.com/roelbroersma/WLED-usermodv2_HttpPullLightControl * Author: Roel Broersma * Website: https://www.roelbroersma.nl * Github author: github.com/roelbroersma * Description: This usermod for WLED will request a given URL to know which effects * or individual lights it should turn on/off. So you can remote control a WLED * installation without having access to it (if no port forward, vpn or public IP is available). * Use Case: Create a WLED 'Ring of Thought' christmas card. Sent a LED ring with 60 LEDs to 60 friends. * When they turn it on and put it at their WiFi, it will contact your server. Now you can reply with a given * number of lights that should turn on. Each light is a friend who did contact your server in the past 5 minutes. * So on each of your friends LED rings, the number of lights will be the number of friends who have it turned on. * Features: It sends a unique ID (has of MAC and salt) to the URL, so you can define each client without a need to map their IP address. * Tested: Tested on WLED v0.14 with ESP32-S3 (M5Stack Atom S3 Lite), but should also workd for other ESPs and ESP8266. */ #include "wled.h" // Use the following for SHA1 computation of our HASH, unfortunatelly PlatformIO doesnt recognize Hash.h while its already in the Core. // We use Hash.h for ESP8266 (in the core) and mbedtls/sha256.h for ESP32 (in the core). #ifdef ESP8266 #include #endif #ifdef ESP32 #include "mbedtls/sha1.h" #endif #define HTTP_PULL_LIGHT_CONTROL_VERSION "0.0.4" class HttpPullLightControl : public Usermod { private: static const char _name[]; static const char _enabled[]; static const char _salt[]; static const char _url[]; bool enabled = true; #ifdef HTTP_PULL_LIGHT_CONTROL_INTERVAL uint16_t checkInterval = HTTP_PULL_LIGHT_CONTROL_INTERVAL; #else uint16_t checkInterval = 60; // Default interval of 1 minute #endif #ifdef HTTP_PULL_LIGHT_CONTROL_URL String url = HTTP_PULL_LIGHT_CONTROL_URL; #else String url = "http://example.org/example.php"; // Default-URL (http only!), can also be url with IP address in it. HttpS urls are not supported (yet) because of AsyncTCP library #endif #ifdef HTTP_PULL_LIGHT_CONTROL_SALT String salt = HTTP_PULL_LIGHT_CONTROL_SALT; #else String salt = "1just_a_very-secret_salt2"; // Salt for generating a unique ID when requesting the URL (in this way you can give different answers based on the WLED device who does the request) #endif // NOTE THAT THERE IS ALSO A #ifdef HTTP_PULL_LIGHT_CONTROL_HIDE_URL and a HTTP_PULL_LIGHT_CONTROL_HIDE_SALT IF YOU DO NOT WANT TO SHOW THE OPTIONS IN THE USERMOD SETTINGS // Define constants static const uint8_t myLockId = USERMOD_ID_HTTP_PULL_LIGHT_CONTROL ; // Used for the requestJSONBufferLock(id) function static const int16_t ackTimeout = 9000; // ACK timeout in milliseconds when doing the URL request static const uint16_t rxTimeout = 9000; // RX timeout in milliseconds when doing the URL request static const unsigned long FNV_offset_basis = 2166136261; static const unsigned long FNV_prime = 16777619; static const unsigned long inactivityTimeout = 30000; // When the AsyncClient is inactive (hanging) for this many milliseconds, we kill it unsigned long lastCheck = 0; // Timestamp of last check unsigned long lastActivityTime = 0; // Time of last activity of AsyncClient String host; // Host extracted from the URL String path; // Path extracted from the URL String uniqueId; // Cached unique ID AsyncClient *client = nullptr; // Used very often, beware of closing and freeing String generateUniqueId(); void parseUrl(); void updateSalt(String newSalt); // Update the salt value and recalculate the unique ID void checkUrl(); // Check the specified URL for light control instructions void handleResponse(String& response); void onClientConnect(AsyncClient *c); public: void setup(); void loop(); bool readFromConfig(JsonObject& root); void addToConfig(JsonObject& root); uint16_t getId() { return USERMOD_ID_HTTP_PULL_LIGHT_CONTROL; } inline void enable(bool enable) { enabled = enable; } // Enable or Disable the usermod inline bool isEnabled() { return enabled; } // Get usermod enabled or disabled state virtual ~HttpPullLightControl() { // Remove the cached client if needed if (client) { client->onDisconnect(nullptr); client->onError(nullptr); client->onTimeout(nullptr); client->onData(nullptr); client->onConnect(nullptr); // Now it is safe to delete the client. delete client; // This is safe even if client is nullptr. client = nullptr; } } }; ================================================ FILE: usermods/usermod_v2_RF433/library.json ================================================ { "name": "usermod_v2_RF433", "build": { "libArchive": false }, "dependencies": { "sui77/rc-switch":"2.6.4" } } ================================================ FILE: usermods/usermod_v2_RF433/readme.md ================================================ # RF433 remote usermod Usermod for controlling WLED using a generic 433 / 315MHz remote and simple 3-pin receiver See for compatibility details ## Build - Create a `platformio_override.ini` file at the root of the wled source directory if not already present - Copy the `433MHz RF remote example for esp32dev` section from `platformio_override.sample.ini` into it - Duplicate/adjust for other boards ## Usage - Connect receiver to a free pin - Set pin in Config->Usermods - Info pane will show the last received button code - Upload the remote433.json sample file in this folder to the ESP with the file editor at [http://\[wled-ip\]/edit](http://ip/edit) - Edit as necessary, the key is the button number retrieved from the info pane, and the "cmd" can be either an [HTTP API](https://kno.wled.ge/interfaces/http-api/) or a [JSON API](https://kno.wled.ge/interfaces/json-api/) command. ================================================ FILE: usermods/usermod_v2_RF433/remote433.json ================================================ { "13985576": { "cmnt": "Toggle Power using HTTP API", "cmd": "T=2" }, "3670817": { "cmnt": "Force Power ON using HTTP API", "cmd": "T=1" }, "13985572": { "cmnt": "Set brightness to 200 using JSON API", "cmd": {"bri":200} }, "3670818": { "cmnt": "Run Preset 1 using JSON API", "cmd": {"ps":1} }, "13985570": { "cmnt": "Increase brightness by 40 using HTTP API", "cmd": "A=~40" }, "13985569": { "cmnt": "Decrease brightness by 40 using HTTP API", "cmd": "A=~-40" }, "7608836": { "cmnt": "Start 1min timer using JSON API", "cmd": {"nl":{"on":true,"dur":1,"mode":0}} }, "7608840": { "cmnt": "Select random effect on all segments using JSON API", "cmd": {"seg":{"fx":"r"}} } } ================================================ FILE: usermods/usermod_v2_RF433/usermod_v2_RF433.cpp ================================================ #include "wled.h" #include "Arduino.h" #include #define RF433_BUSWAIT_TIMEOUT 24 class RF433Usermod : public Usermod { private: RCSwitch mySwitch = RCSwitch(); unsigned long lastCommand = 0; unsigned long lastTime = 0; bool modEnabled = true; int8_t receivePin = -1; static const char _modName[]; static const char _modEnabled[]; static const char _receivePin[]; bool initDone = false; public: void setup() { mySwitch.disableReceive(); if (modEnabled) { mySwitch.enableReceive(receivePin); } initDone = true; } /* * connected() is called every time the WiFi is (re)connected * Use it to initialize network interfaces */ void connected() { } void loop() { if (!modEnabled || strip.isUpdating()) return; if (mySwitch.available()) { unsigned long receivedCommand = mySwitch.getReceivedValue(); mySwitch.resetAvailable(); // Discard duplicates, limit long press repeat if (lastCommand == receivedCommand && millis() - lastTime < 800) return; lastCommand = receivedCommand; lastTime = millis(); DEBUG_PRINT(F("RF433 Receive: ")); DEBUG_PRINTLN(receivedCommand); if(!remoteJson433(receivedCommand)) DEBUG_PRINTLN(F("RF433: unknown button")); } } // Add last received button to info pane void addToJsonInfo(JsonObject &root) { if (!initDone) return; // prevent crash on boot applyPreset() JsonObject user = root["u"]; if (user.isNull()) user = root.createNestedObject("u"); JsonArray switchArr = user.createNestedArray("RF433 Last Received"); // name switchArr.add(lastCommand); } void addToConfig(JsonObject &root) { JsonObject top = root.createNestedObject(FPSTR(_modName)); // usermodname top[FPSTR(_modEnabled)] = modEnabled; JsonArray pinArray = top.createNestedArray("pin"); pinArray.add(receivePin); DEBUG_PRINTLN(F(" config saved.")); } bool readFromConfig(JsonObject &root) { JsonObject top = root[FPSTR(_modName)]; if (top.isNull()) { DEBUG_PRINT(FPSTR(_modName)); DEBUG_PRINTLN(F(": No config found. (Using defaults.)")); return false; } getJsonValue(top[FPSTR(_modEnabled)], modEnabled); getJsonValue(top["pin"][0], receivePin); DEBUG_PRINTLN(F("config (re)loaded.")); // Redo init on update if(initDone) setup(); return true; } /* * 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_RF433; } // this function follows the same principle as decodeIRJson() / remoteJson() bool remoteJson433(int button) { char objKey[14]; bool parsed = false; if (!requestJSONBufferLock(JSON_LOCK_REMOTE)) return false; sprintf_P(objKey, PSTR("\"%d\":"), button); unsigned long start = millis(); while (strip.isUpdating() && millis()-start < RF433_BUSWAIT_TIMEOUT) yield(); // wait for strip to finish updating, accessing FS during sendout causes glitches // attempt to read command from remote.json readObjectFromFile(PSTR("/remote433.json"), objKey, pDoc); JsonObject fdo = pDoc->as(); if (fdo.isNull()) { // the received button does not exist releaseJSONBufferLock(); return parsed; } String cmdStr = fdo["cmd"].as(); JsonObject jsonCmdObj = fdo["cmd"]; //object if (jsonCmdObj.isNull()) // we could also use: fdo["cmd"].is() { // HTTP API command String apireq = "win"; apireq += '&'; // reduce flash string usage if (!cmdStr.startsWith(apireq)) cmdStr = apireq + cmdStr; // if no "win&" prefix if (!irApplyToAllSelected && cmdStr.indexOf(F("SS="))<0) { char tmp[10]; sprintf_P(tmp, PSTR("&SS=%d"), strip.getMainSegmentId()); cmdStr += tmp; } fdo.clear(); // clear JSON buffer (it is no longer needed) handleSet(nullptr, cmdStr, false); // no stateUpdated() call here stateUpdated(CALL_MODE_BUTTON); parsed = true; } else { // command is JSON object if (jsonCmdObj[F("psave")].isNull()) deserializeState(jsonCmdObj, CALL_MODE_BUTTON_PRESET); else { uint8_t psave = jsonCmdObj[F("psave")].as(); char pname[33]; sprintf_P(pname, PSTR("IR Preset %d"), psave); fdo.clear(); if (psave > 0 && psave < 251) savePreset(psave, pname, fdo); } parsed = true; } releaseJSONBufferLock(); return parsed; } }; const char RF433Usermod::_modName[] PROGMEM = "RF433 Remote"; const char RF433Usermod::_modEnabled[] PROGMEM = "Enabled"; const char RF433Usermod::_receivePin[] PROGMEM = "RX Pin"; static RF433Usermod usermod_v2_RF433; REGISTER_USERMOD(usermod_v2_RF433); ================================================ FILE: usermods/usermod_v2_animartrix/library.json ================================================ { "name": "animartrix", "build": { "libArchive": false }, "dependencies": { "Animartrix": "https://github.com/netmindz/animartrix.git#b172586" } } ================================================ FILE: usermods/usermod_v2_animartrix/readme.md ================================================ # ANIMartRIX Addes the effects from ANIMartRIX to WLED CC BY-NC 3.0 licensed effects by Stefan Petrick, include this usermod only if you accept the terms! ## Installation Add 'animartrix' to 'custom_usermods' in your platformio_override.ini. ================================================ FILE: usermods/usermod_v2_animartrix/usermod_v2_animartrix.cpp ================================================ #include "wled.h" #include #warning WLED usermod: CC BY-NC 3.0 licensed effects by Stefan Petrick, include this usermod only if you accept the terms! //======================================================================================================================== static const char _data_FX_mode_Module_Experiment10[] PROGMEM = "Z💡Module_Experiment10@Speed;;1;2"; static const char _data_FX_mode_Module_Experiment9[] PROGMEM = "Z💡Module_Experiment9@Speed;;1;2"; static const char _data_FX_mode_Module_Experiment8[] PROGMEM = "Z💡Module_Experiment8@Speed;;1;2"; static const char _data_FX_mode_Module_Experiment7[] PROGMEM = "Z💡Module_Experiment7@Speed;;1;2"; static const char _data_FX_mode_Module_Experiment6[] PROGMEM = "Z💡Module_Experiment6@Speed;;1;2"; static const char _data_FX_mode_Module_Experiment5[] PROGMEM = "Z💡Module_Experiment5@Speed;;1;2"; static const char _data_FX_mode_Module_Experiment4[] PROGMEM = "Z💡Module_Experiment4@Speed;;1;2"; static const char _data_FX_mode_Zoom2[] PROGMEM = "Z💡Zoom2@Speed;;1;2"; static const char _data_FX_mode_Module_Experiment3[] PROGMEM = "Z💡Module_Experiment3@Speed;;1;2"; static const char _data_FX_mode_Module_Experiment2[] PROGMEM = "Z💡Module_Experiment2@Speed;;1;2"; static const char _data_FX_mode_Module_Experiment1[] PROGMEM = "Z💡Module_Experiment1@Speed;;1;2"; static const char _data_FX_mode_Parametric_Water[] PROGMEM = "Z💡Parametric_Water@Speed;;1;2"; static const char _data_FX_mode_Water[] PROGMEM = "Z💡Water@Speed;;1;2"; static const char _data_FX_mode_Complex_Kaleido_6[] PROGMEM = "Z💡Complex_Kaleido_6@Speed;;1;2"; static const char _data_FX_mode_Complex_Kaleido_5[] PROGMEM = "Z💡Complex_Kaleido_5@Speed;;1;2"; static const char _data_FX_mode_Complex_Kaleido_4[] PROGMEM = "Z💡Complex_Kaleido_4@Speed;;1;2"; static const char _data_FX_mode_Complex_Kaleido_3[] PROGMEM = "Z💡Complex_Kaleido_3@Speed;;1;2"; static const char _data_FX_mode_Complex_Kaleido_2[] PROGMEM = "Z💡Complex_Kaleido_2@Speed;;1;2"; static const char _data_FX_mode_Complex_Kaleido[] PROGMEM = "Z💡Complex_Kaleido@Speed;;1;2"; static const char _data_FX_mode_SM10[] PROGMEM = "Z💡SM10@Speed;;1;2"; static const char _data_FX_mode_SM9[] PROGMEM = "Z💡SM9@Speed;;1;2"; static const char _data_FX_mode_SM8[] PROGMEM = "Z💡SM8@Speed;;1;2"; static const char _data_FX_mode_SM7[] PROGMEM = "Z💡SM7@Speed;;1;2"; static const char _data_FX_mode_SM6[] PROGMEM = "Z💡SM6@Speed;;1;2"; static const char _data_FX_mode_SM5[] PROGMEM = "Z💡SM5@Speed;;1;2"; static const char _data_FX_mode_SM4[] PROGMEM = "Z💡SM4@Speed;;1;2"; static const char _data_FX_mode_SM3[] PROGMEM = "Z💡SM3@Speed;;1;2"; static const char _data_FX_mode_SM2[] PROGMEM = "Z💡SM2@Speed;;1;2"; static const char _data_FX_mode_SM1[] PROGMEM = "Z💡SM1@Speed;;1;2"; static const char _data_FX_mode_Big_Caleido[] PROGMEM = "Z💡Big_Caleido@Speed;;1;2"; static const char _data_FX_mode_RGB_Blobs5[] PROGMEM = "Z💡RGB_Blobs5@Speed;;1;2"; static const char _data_FX_mode_RGB_Blobs4[] PROGMEM = "Z💡RGB_Blobs4@Speed;;1;2"; static const char _data_FX_mode_RGB_Blobs3[] PROGMEM = "Z💡RGB_Blobs3@Speed;;1;2"; static const char _data_FX_mode_RGB_Blobs2[] PROGMEM = "Z💡RGB_Blobs2@Speed;;1;2"; static const char _data_FX_mode_RGB_Blobs[] PROGMEM = "Z💡RGB_Blobs@Speed;;1;2"; static const char _data_FX_mode_Polar_Waves[] PROGMEM = "Z💡Polar_Waves@Speed;;1;2"; static const char _data_FX_mode_Slow_Fade[] PROGMEM = "Z💡Slow_Fade@Speed;;1;2"; static const char _data_FX_mode_Zoom[] PROGMEM = "Z💡Zoom@Speed;;1;2"; static const char _data_FX_mode_Hot_Blob[] PROGMEM = "Z💡Hot_Blob@Speed;;1;2"; static const char _data_FX_mode_Spiralus2[] PROGMEM = "Z💡Spiralus2@Speed;;1;2"; static const char _data_FX_mode_Spiralus[] PROGMEM = "Z💡Spiralus@Speed;;1;2"; static const char _data_FX_mode_Yves[] PROGMEM = "Z💡Yves@Speed;;1;2"; static const char _data_FX_mode_Scaledemo1[] PROGMEM = "Z💡Scaledemo1@Speed;;1;2"; static const char _data_FX_mode_Lava1[] PROGMEM = "Z💡Lava1@Speed;;1;2"; static const char _data_FX_mode_Caleido3[] PROGMEM = "Z💡Caleido3@Speed;;1;2"; static const char _data_FX_mode_Caleido2[] PROGMEM = "Z💡Caleido2@Speed;;1;2"; static const char _data_FX_mode_Caleido1[] PROGMEM = "Z💡Caleido1@Speed;;1;2"; static const char _data_FX_mode_Distance_Experiment[] PROGMEM = "Z💡Distance_Experiment@Speed;;1;2"; static const char _data_FX_mode_Center_Field[] PROGMEM = "Z💡Center_Field@Speed;;1;2"; static const char _data_FX_mode_Waves[] PROGMEM = "Z💡Waves@Speed;;1;2"; static const char _data_FX_mode_Chasing_Spirals[] PROGMEM = "Z💡Chasing_Spirals@Speed;;1;2"; static const char _data_FX_mode_Rotating_Blob[] PROGMEM = "Z💡Rotating_Blob@Speed;;1;2"; class ANIMartRIXMod:public ANIMartRIX { public: void initEffect() { if (SEGENV.call == 0) { init(SEGMENT.virtualWidth(), SEGMENT.virtualHeight(), false); } float speedFactor = 1.0; if (SEGMENT.speed < 128) { speedFactor = (float) map(SEGMENT.speed, 0, 127, 1, 10) / 10.0f; } else{ speedFactor = map(SEGMENT.speed, 128, 255, 10, 100) / 10; } setSpeedFactor(speedFactor); } void setPixelColor(int x, int y, rgb pixel) { SEGMENT.setPixelColorXY(x, y, CRGB(pixel.red, pixel.green, pixel.blue)); } void setPixelColor(int index, rgb pixel) { SEGMENT.setPixelColor(index, CRGB(pixel.red, pixel.green, pixel.blue)); } // Add any extra custom effects not part of the ANIMartRIX libary here }; ANIMartRIXMod anim; void mode_Module_Experiment10() { anim.initEffect(); anim.Module_Experiment10(); } void mode_Module_Experiment9() { anim.initEffect(); anim.Module_Experiment9(); } void mode_Module_Experiment8() { anim.initEffect(); anim.Module_Experiment8(); } void mode_Module_Experiment7() { anim.initEffect(); anim.Module_Experiment7(); } void mode_Module_Experiment6() { anim.initEffect(); anim.Module_Experiment6(); } void mode_Module_Experiment5() { anim.initEffect(); anim.Module_Experiment5(); } void mode_Module_Experiment4() { anim.initEffect(); anim.Module_Experiment4(); } void mode_Zoom2() { anim.initEffect(); anim.Zoom2(); } void mode_Module_Experiment3() { anim.initEffect(); anim.Module_Experiment3(); } void mode_Module_Experiment2() { anim.initEffect(); anim.Module_Experiment2(); } void mode_Module_Experiment1() { anim.initEffect(); anim.Module_Experiment1(); } void mode_Parametric_Water() { anim.initEffect(); anim.Parametric_Water(); } void mode_Water() { anim.initEffect(); anim.Water(); } void mode_Complex_Kaleido_6() { anim.initEffect(); anim.Complex_Kaleido_6(); } void mode_Complex_Kaleido_5() { anim.initEffect(); anim.Complex_Kaleido_5(); } void mode_Complex_Kaleido_4() { anim.initEffect(); anim.Complex_Kaleido_4(); } void mode_Complex_Kaleido_3() { anim.initEffect(); anim.Complex_Kaleido_3(); } void mode_Complex_Kaleido_2() { anim.initEffect(); anim.Complex_Kaleido_2(); } void mode_Complex_Kaleido() { anim.initEffect(); anim.Complex_Kaleido(); } void mode_SM10() { anim.initEffect(); anim.SM10(); } void mode_SM9() { anim.initEffect(); anim.SM9(); } void mode_SM8() { anim.initEffect(); anim.SM8(); } // void mode_SM7() { // anim.initEffect(); // anim.SM7(); // // } void mode_SM6() { anim.initEffect(); anim.SM6(); } void mode_SM5() { anim.initEffect(); anim.SM5(); } void mode_SM4() { anim.initEffect(); anim.SM4(); } void mode_SM3() { anim.initEffect(); anim.SM3(); } void mode_SM2() { anim.initEffect(); anim.SM2(); } void mode_SM1() { anim.initEffect(); anim.SM1(); } void mode_Big_Caleido() { anim.initEffect(); anim.Big_Caleido(); } void mode_RGB_Blobs5() { anim.initEffect(); anim.RGB_Blobs5(); } void mode_RGB_Blobs4() { anim.initEffect(); anim.RGB_Blobs4(); } void mode_RGB_Blobs3() { anim.initEffect(); anim.RGB_Blobs3(); } void mode_RGB_Blobs2() { anim.initEffect(); anim.RGB_Blobs2(); } void mode_RGB_Blobs() { anim.initEffect(); anim.RGB_Blobs(); } void mode_Polar_Waves() { anim.initEffect(); anim.Polar_Waves(); } void mode_Slow_Fade() { anim.initEffect(); anim.Slow_Fade(); } void mode_Zoom() { anim.initEffect(); anim.Zoom(); } void mode_Hot_Blob() { anim.initEffect(); anim.Hot_Blob(); } void mode_Spiralus2() { anim.initEffect(); anim.Spiralus2(); } void mode_Spiralus() { anim.initEffect(); anim.Spiralus(); } void mode_Yves() { anim.initEffect(); anim.Yves(); } void mode_Scaledemo1() { anim.initEffect(); anim.Scaledemo1(); } void mode_Lava1() { anim.initEffect(); anim.Lava1(); } void mode_Caleido3() { anim.initEffect(); anim.Caleido3(); } void mode_Caleido2() { anim.initEffect(); anim.Caleido2(); } void mode_Caleido1() { anim.initEffect(); anim.Caleido1(); } void mode_Distance_Experiment() { anim.initEffect(); anim.Distance_Experiment(); } void mode_Center_Field() { anim.initEffect(); anim.Center_Field(); } void mode_Waves() { anim.initEffect(); anim.Waves(); } void mode_Chasing_Spirals() { anim.initEffect(); anim.Chasing_Spirals(); } void mode_Rotating_Blob() { anim.initEffect(); anim.Rotating_Blob(); } class AnimartrixUsermod : public Usermod { protected: bool enabled = false; //WLEDMM const char *_name; //WLEDMM bool initDone = false; //WLEDMM unsigned long lastTime = 0; //WLEDMM public: AnimartrixUsermod(const char *name, bool enabled) { this->_name = name; this->enabled = enabled; } //WLEDMM void setup() { strip.addEffect(255, &mode_Module_Experiment10, _data_FX_mode_Module_Experiment10); strip.addEffect(255, &mode_Module_Experiment9, _data_FX_mode_Module_Experiment9); strip.addEffect(255, &mode_Module_Experiment8, _data_FX_mode_Module_Experiment8); strip.addEffect(255, &mode_Module_Experiment7, _data_FX_mode_Module_Experiment7); strip.addEffect(255, &mode_Module_Experiment6, _data_FX_mode_Module_Experiment6); strip.addEffect(255, &mode_Module_Experiment5, _data_FX_mode_Module_Experiment5); strip.addEffect(255, &mode_Module_Experiment4, _data_FX_mode_Module_Experiment4); strip.addEffect(255, &mode_Zoom2, _data_FX_mode_Zoom2); strip.addEffect(255, &mode_Module_Experiment3, _data_FX_mode_Module_Experiment3); strip.addEffect(255, &mode_Module_Experiment2, _data_FX_mode_Module_Experiment2); strip.addEffect(255, &mode_Module_Experiment1, _data_FX_mode_Module_Experiment1); strip.addEffect(255, &mode_Parametric_Water, _data_FX_mode_Parametric_Water); strip.addEffect(255, &mode_Water, _data_FX_mode_Water); strip.addEffect(255, &mode_Complex_Kaleido_6, _data_FX_mode_Complex_Kaleido_6); strip.addEffect(255, &mode_Complex_Kaleido_5, _data_FX_mode_Complex_Kaleido_5); strip.addEffect(255, &mode_Complex_Kaleido_4, _data_FX_mode_Complex_Kaleido_4); strip.addEffect(255, &mode_Complex_Kaleido_3, _data_FX_mode_Complex_Kaleido_3); strip.addEffect(255, &mode_Complex_Kaleido_2, _data_FX_mode_Complex_Kaleido_2); strip.addEffect(255, &mode_Complex_Kaleido, _data_FX_mode_Complex_Kaleido); strip.addEffect(255, &mode_SM10, _data_FX_mode_SM10); strip.addEffect(255, &mode_SM9, _data_FX_mode_SM9); strip.addEffect(255, &mode_SM8, _data_FX_mode_SM8); // strip.addEffect(255, &mode_SM7, _data_FX_mode_SM7); strip.addEffect(255, &mode_SM6, _data_FX_mode_SM6); strip.addEffect(255, &mode_SM5, _data_FX_mode_SM5); strip.addEffect(255, &mode_SM4, _data_FX_mode_SM4); strip.addEffect(255, &mode_SM3, _data_FX_mode_SM3); strip.addEffect(255, &mode_SM2, _data_FX_mode_SM2); strip.addEffect(255, &mode_SM1, _data_FX_mode_SM1); strip.addEffect(255, &mode_Big_Caleido, _data_FX_mode_Big_Caleido); strip.addEffect(255, &mode_RGB_Blobs5, _data_FX_mode_RGB_Blobs5); strip.addEffect(255, &mode_RGB_Blobs4, _data_FX_mode_RGB_Blobs4); strip.addEffect(255, &mode_RGB_Blobs3, _data_FX_mode_RGB_Blobs3); strip.addEffect(255, &mode_RGB_Blobs2, _data_FX_mode_RGB_Blobs2); strip.addEffect(255, &mode_RGB_Blobs, _data_FX_mode_RGB_Blobs); strip.addEffect(255, &mode_Polar_Waves, _data_FX_mode_Polar_Waves); strip.addEffect(255, &mode_Slow_Fade, _data_FX_mode_Slow_Fade); strip.addEffect(255, &mode_Zoom, _data_FX_mode_Zoom); strip.addEffect(255, &mode_Hot_Blob, _data_FX_mode_Hot_Blob); strip.addEffect(255, &mode_Spiralus2, _data_FX_mode_Spiralus2); strip.addEffect(255, &mode_Spiralus, _data_FX_mode_Spiralus); strip.addEffect(255, &mode_Yves, _data_FX_mode_Yves); strip.addEffect(255, &mode_Scaledemo1, _data_FX_mode_Scaledemo1); strip.addEffect(255, &mode_Lava1, _data_FX_mode_Lava1); strip.addEffect(255, &mode_Caleido3, _data_FX_mode_Caleido3); strip.addEffect(255, &mode_Caleido2, _data_FX_mode_Caleido2); strip.addEffect(255, &mode_Caleido1, _data_FX_mode_Caleido1); strip.addEffect(255, &mode_Distance_Experiment, _data_FX_mode_Distance_Experiment); strip.addEffect(255, &mode_Center_Field, _data_FX_mode_Center_Field); strip.addEffect(255, &mode_Waves, _data_FX_mode_Waves); strip.addEffect(255, &mode_Chasing_Spirals, _data_FX_mode_Chasing_Spirals); strip.addEffect(255, &mode_Rotating_Blob, _data_FX_mode_Rotating_Blob); initDone = true; } void loop() { if (!enabled || strip.isUpdating()) return; // do your magic here if (millis() - lastTime > 1000) { //USER_PRINTLN("I'm alive!"); lastTime = millis(); } } void addToJsonInfo(JsonObject& root) { char myStringBuffer[16]; // buffer for snprintf() JsonObject user = root["u"]; if (user.isNull()) user = root.createNestedObject("u"); JsonArray infoArr = user.createNestedArray(FPSTR(_name)); String uiDomString = F("Animartrix requires the Creative Commons Attribution License CC BY-NC 3.0"); infoArr.add(uiDomString); } uint16_t getId() { return USERMOD_ID_ANIMARTRIX; } }; static AnimartrixUsermod animartrix_module("Animartrix", false); REGISTER_USERMOD(animartrix_module); ================================================ FILE: usermods/usermod_v2_auto_save/library.json ================================================ { "name": "auto_save", "build": { "libArchive": false } } ================================================ FILE: usermods/usermod_v2_auto_save/readme.md ================================================ # 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_AFTER_SEC seconds, a "settle" period in case there are other changes (any change will extend the "settle" period). It will additionally load preset AUTOSAVE_PRESET_NUM at startup during the first `loop()`. AutoSaveUsermod is standalone, but if FourLineDisplayUsermod is installed, it will notify the user of the saved changes. Note: 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 usermod included wled00\usermods_list.cpp * `AUTOSAVE_AFTER_SEC` - define the delay time after the settings auto-saving routine should be executed * `AUTOSAVE_PRESET_NUM` - define the preset number used by autosave usermod * `USERMOD_AUTO_SAVE_ON_BOOT` - define if autosave should be enabled on boot * `USERMOD_FOUR_LINE_DISPLAY` - 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) Example to add in platformio_override: -D USERMOD_AUTO_SAVE -D AUTOSAVE_AFTER_SEC=10 -D AUTOSAVE_PRESET_NUM=100 -D USERMOD_AUTO_SAVE_ON_BOOT=true You can also configure auto-save parameters using Usermods settings page. ### PlatformIO requirements No special requirements. Note: the Four Line Display usermod requires the libraries `U8g2` and `Wire`. ## Change Log 2021-02 * First public release 2021-04 * Adaptation for runtime configuration. ================================================ FILE: usermods/usermod_v2_auto_save/usermod_v2_auto_save.cpp ================================================ #include "wled.h" // v2 Usermod to automatically save settings // to configurable preset after a change to any of // // * brightness // * effect speed // * effect intensity // * mode (effect) // * palette // // but it will wait for configurable number of seconds, a "settle" // period in case there are other changes (any change will // extend the "settle" window). // // It can be configured to load auto saved preset at startup, // during the first `loop()`. // // By default it will not save the state if an unmodified preset // is selected (to not duplicate it). You can change this behaviour // by setting autoSaveIgnorePresets=false // // AutoSaveUsermod is standalone, but if FourLineDisplayUsermod // is installed, it will notify the user of the saved changes. // format: "~ MM-DD HH:MM:SS ~" #define PRESET_NAME_BUFFER_SIZE 25 class AutoSaveUsermod : public Usermod { private: bool firstLoop = true; bool initDone = false; bool enabled = true; // configurable parameters #ifdef AUTOSAVE_AFTER_SEC uint16_t autoSaveAfterSec = AUTOSAVE_AFTER_SEC; #else uint16_t autoSaveAfterSec = 15; // 15s by default #endif #ifdef AUTOSAVE_PRESET_NUM uint8_t autoSavePreset = AUTOSAVE_PRESET_NUM; #else uint8_t autoSavePreset = 250; // last possible preset #endif #ifdef USERMOD_AUTO_SAVE_ON_BOOT bool applyAutoSaveOnBoot = USERMOD_AUTO_SAVE_ON_BOOT; #else bool applyAutoSaveOnBoot = false; // do we load auto-saved preset on boot? #endif bool autoSaveIgnorePresets = true; // ignore by default to not duplicate presets // If we've detected the need to auto save, this will be non zero. unsigned long autoSaveAfter = 0; uint8_t knownBrightness = 0; uint8_t knownEffectSpeed = 0; uint8_t knownEffectIntensity = 0; uint8_t knownMode = 0; uint8_t knownPalette = 0; #ifdef USERMOD_FOUR_LINE_DISPLAY FourLineDisplayUsermod* display; #endif // strings to reduce flash memory usage (used more than twice) static const char _name[]; static const char _autoSaveEnabled[]; static const char _autoSaveAfterSec[]; static const char _autoSavePreset[]; static const char _autoSaveApplyOnBoot[]; static const char _autoSaveIgnorePresets[]; void inline saveSettings() { char presetNameBuffer[PRESET_NAME_BUFFER_SIZE]; updateLocalTime(); sprintf_P(presetNameBuffer, PSTR("~ %02d-%02d %02d:%02d:%02d ~"), month(localTime), day(localTime), hour(localTime), minute(localTime), second(localTime)); cacheInvalidate++; // force reload of presets savePreset(autoSavePreset, presetNameBuffer); } void inline displayOverlay() { #ifdef USERMOD_FOUR_LINE_DISPLAY if (display != nullptr) { display->wakeDisplay(); display->overlay("Settings", "Auto Saved", 1500); } #endif } void enable(bool enable) { enabled = enable; } public: // gets called once at boot. Do all initialization that doesn't depend on // network here void setup() { #ifdef USERMOD_FOUR_LINE_DISPLAY // This Usermod has enhanced functionality if // FourLineDisplayUsermod is available. display = (FourLineDisplayUsermod*) UsermodManager::lookup(USERMOD_ID_FOUR_LINE_DISP); #endif initDone = true; if (enabled && applyAutoSaveOnBoot) applyPreset(autoSavePreset); knownBrightness = bri; knownEffectSpeed = effectSpeed; knownEffectIntensity = effectIntensity; knownMode = strip.getMainSegment().mode; knownPalette = strip.getMainSegment().palette; } // gets called every time WiFi is (re-)connected. Initialize own network // interfaces here void connected() {} /* * Da loop. */ void loop() { static unsigned long lastRun = 0; unsigned long now = millis(); if (!autoSaveAfterSec || !enabled || (autoSaveIgnorePresets && currentPreset>0) || (strip.isUpdating() && now - lastRun < 240)) return; // setting 0 as autosave seconds disables autosave lastRun = now; uint8_t currentMode = strip.getMainSegment().mode; uint8_t currentPalette = strip.getMainSegment().palette; unsigned long wouldAutoSaveAfter = now + autoSaveAfterSec*1000; 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 flickery? saveSettings(); displayOverlay(); } } /* * 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) { JsonObject user = root["u"]; if (user.isNull()) { user = root.createNestedObject("u"); } JsonArray infoArr = user.createNestedArray(FPSTR(_name)); // name String uiDomString = F(""); infoArr.add(uiDomString); } /* * 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) { if (!initDone) return; // prevent crash on boot applyPreset() bool en = enabled; JsonObject um = root[FPSTR(_name)]; if (!um.isNull()) { if (um[FPSTR(_autoSaveEnabled)].is()) { en = um[FPSTR(_autoSaveEnabled)].as(); } else { String str = um[FPSTR(_autoSaveEnabled)]; // checkbox -> off or on en = (bool)(str!="off"); // off is guaranteed to be present } if (en != enabled) enable(en); } } /* * 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) { // we add JSON object: {"Autosave": {"autoSaveAfterSec": 10, "autoSavePreset": 99}} JsonObject top = root.createNestedObject(FPSTR(_name)); // usermodname top[FPSTR(_autoSaveEnabled)] = enabled; top[FPSTR(_autoSaveAfterSec)] = autoSaveAfterSec; // usermodparam top[FPSTR(_autoSavePreset)] = autoSavePreset; // usermodparam top[FPSTR(_autoSaveApplyOnBoot)] = applyAutoSaveOnBoot; top[FPSTR(_autoSaveIgnorePresets)] = autoSaveIgnorePresets; DEBUG_PRINTLN(F("Autosave config saved.")); } /* * 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 :) * * The function should return true if configuration was successfully loaded or false if there was no configuration. */ bool readFromConfig(JsonObject& root) { // we look for JSON object: {"Autosave": {"enabled": true, "autoSaveAfterSec": 10, "autoSavePreset": 250, ...}} JsonObject top = root[FPSTR(_name)]; if (top.isNull()) { DEBUG_PRINT(FPSTR(_name)); DEBUG_PRINTLN(F(": No config found. (Using defaults.)")); return false; } enabled = top[FPSTR(_autoSaveEnabled)] | enabled; autoSaveAfterSec = top[FPSTR(_autoSaveAfterSec)] | autoSaveAfterSec; autoSaveAfterSec = (uint16_t) min(3600,max(10,(int)autoSaveAfterSec)); // bounds checking autoSavePreset = top[FPSTR(_autoSavePreset)] | autoSavePreset; autoSavePreset = (uint8_t) min(250,max(100,(int)autoSavePreset)); // bounds checking applyAutoSaveOnBoot = top[FPSTR(_autoSaveApplyOnBoot)] | applyAutoSaveOnBoot; autoSaveIgnorePresets = top[FPSTR(_autoSaveIgnorePresets)] | autoSaveIgnorePresets; DEBUG_PRINT(FPSTR(_name)); DEBUG_PRINTLN(F(" config (re)loaded.")); // use "return !top["newestParameter"].isNull();" when updating Usermod with new features return true; } /* * 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; } }; // strings to reduce flash memory usage (used more than twice) const char AutoSaveUsermod::_name[] PROGMEM = "Autosave"; const char AutoSaveUsermod::_autoSaveEnabled[] PROGMEM = "enabled"; const char AutoSaveUsermod::_autoSaveAfterSec[] PROGMEM = "autoSaveAfterSec"; const char AutoSaveUsermod::_autoSavePreset[] PROGMEM = "autoSavePreset"; const char AutoSaveUsermod::_autoSaveApplyOnBoot[] PROGMEM = "autoSaveApplyOnBoot"; const char AutoSaveUsermod::_autoSaveIgnorePresets[] PROGMEM = "autoSaveIgnorePresets"; static AutoSaveUsermod autosave; REGISTER_USERMOD(autosave); ================================================ FILE: usermods/usermod_v2_brightness_follow_sun/README.md ================================================ # Update Brightness Follow Sun This UserMod can set brightness by mapping [minimum-maximum-minimum] from [sunrise-suntop-sunset], I use this UserMod to adjust the brightness of my plant growth light (pwm led), and I think it will make my plants happy. This UserMod will adjust brightness from sunrise to sunset, reaching maximum brightness at the zenith of the sun. It can also maintain the lowest brightness within 0-6 hours before sunrise and after sunset according to the settings. ## Installation define `USERMOD_BRIGHTNESS_FOLLOW_SUN` e.g. `#define USERMOD_BRIGHTNESS_FOLLOW_SUN` in my_config.h or add `-D USERMOD_BRIGHTNESS_FOLLOW_SUN` to `build_flags` in platformio_override.ini ### Options Open Usermod Settings in WLED to change settings: `Enable` - When checked `Enable`, turn on the `Brightness Follow Sun` Usermod, which will automatically turn on the lights, adjust the brightness, and turn off the lights. If you need to completely turn off the lights, please unchecked `Enable`. `Update Interval Sec` - The unit is seconds, and the brightness will be automatically refreshed according to the set parameters. `Min Brightness` - set brightness by map of min-max-min : sunrise-suntop-sunset `Max Brightness` - It needs to be set to a value greater than `Min Brightness`, otherwise it will always remain at `Min Brightness`. `Relax Hour` - The unit is in hours, with an effective range of 0-6. According to the settings, maintain the lowest brightness for 0-6 hours before sunrise and after sunset. ### PlatformIO requirements No special requirements. ### Change Log 2025-01-02 * init ================================================ FILE: usermods/usermod_v2_brightness_follow_sun/library.json ================================================ { "name": "brightness_follow_sun", "build": { "libArchive": false } } ================================================ FILE: usermods/usermod_v2_brightness_follow_sun/usermod_v2_brightness_follow_sun.cpp ================================================ #include "wled.h" //v2 usermod that allows to change brightness and color using a rotary encoder, //change between modes by pressing a button (many encoders have one included) class UsermodBrightnessFollowSun : public Usermod { private: static const char _name[]; static const char _enabled[]; static const char _update_interval[]; static const char _min_bri[]; static const char _max_bri[]; static const char _relax_hour[]; private: bool enabled = false; //WLEDMM unsigned long update_interval = 60; unsigned long update_interval_ms = 60000; int min_bri = 1; int max_bri = 255; float relax_hour = 0; int relaxSec = 0; unsigned long lastUMRun = 0; public: void setup() {}; float mapFloat(float inputValue, float inMin, float inMax, float outMin, float outMax) { if (inMax == inMin) return outMin; inputValue = constrain(inputValue, inMin, inMax); return ((inputValue - inMin) * (outMax - outMin) / (inMax - inMin)) + outMin; } uint16_t getId() override { return USERMOD_ID_BRIGHTNESS_FOLLOW_SUN; } void update() { if (sunrise == 0 || sunset == 0 || localTime == 0) return; int curSec = elapsedSecsToday(localTime); int sunriseSec = elapsedSecsToday(sunrise); int sunsetSec = elapsedSecsToday(sunset); int sunMiddleSec = sunriseSec + (sunsetSec-sunriseSec)/2; int relaxSecH = sunriseSec-relaxSec; int relaxSecE = sunsetSec+relaxSec; int briSet = 0; if (curSec >= relaxSecH && curSec <= relaxSecE) { float timeMapToAngle = curSec < sunMiddleSec ? mapFloat(curSec, sunriseSec, sunMiddleSec, 0, M_PI/2.0) : mapFloat(curSec, sunMiddleSec, sunsetSec, M_PI/2.0, M_PI); float sinValue = sin_t(timeMapToAngle); briSet = min_bri + (max_bri-min_bri)*sinValue; } bri = briSet; stateUpdated(CALL_MODE_DIRECT_CHANGE); } void loop() override { if (!enabled || strip.isUpdating()) return; if (millis() - lastUMRun < update_interval_ms) return; lastUMRun = millis(); update(); } void addToConfig(JsonObject& root) { JsonObject top = root.createNestedObject(FPSTR(_name)); // usermodname top[FPSTR(_enabled)] = enabled; top[FPSTR(_update_interval)] = update_interval; top[FPSTR(_min_bri)] = min_bri; top[FPSTR(_max_bri)] = max_bri; top[FPSTR(_relax_hour)] = relax_hour; } bool readFromConfig(JsonObject& root) { JsonObject top = root[FPSTR(_name)]; if (top.isNull()) { DEBUG_PRINTF("[%s] No config found. (Using defaults.)\n", _name); return false; } bool configComplete = true; configComplete &= getJsonValue(top[FPSTR(_enabled)], enabled, false); configComplete &= getJsonValue(top[FPSTR(_update_interval)], update_interval, 60); configComplete &= getJsonValue(top[FPSTR(_min_bri)], min_bri, 1); configComplete &= getJsonValue(top[FPSTR(_max_bri)], max_bri, 255); configComplete &= getJsonValue(top[FPSTR(_relax_hour)], relax_hour, 0); update_interval = constrain(update_interval, 1, SECS_PER_HOUR); min_bri = constrain(min_bri, 1, 255); max_bri = constrain(max_bri, 1, 255); relax_hour = constrain(relax_hour, 0, 6); update_interval_ms = update_interval*1000; relaxSec = SECS_PER_HOUR*relax_hour; lastUMRun = 0; update(); return configComplete; } }; const char UsermodBrightnessFollowSun::_name[] PROGMEM = "Brightness Follow Sun"; const char UsermodBrightnessFollowSun::_enabled[] PROGMEM = "Enabled"; const char UsermodBrightnessFollowSun::_update_interval[] PROGMEM = "Update Interval Sec"; const char UsermodBrightnessFollowSun::_min_bri[] PROGMEM = "Min Brightness"; const char UsermodBrightnessFollowSun::_max_bri[] PROGMEM = "Max Brightness"; const char UsermodBrightnessFollowSun::_relax_hour[] PROGMEM = "Relax Hour"; static UsermodBrightnessFollowSun usermod_brightness_follow_sun; REGISTER_USERMOD(usermod_brightness_follow_sun); ================================================ FILE: usermods/usermod_v2_four_line_display_ALT/4LD_wled_fonts.h ================================================ //WLED custom fonts, curtesy of @Benji (https://github.com/Proto-molecule) #pragma once /* Fontname: wled_logo_akemi_4x4 Copyright: Benji (https://github.com/proto-molecule) Glyphs: 3/3 BBX Build Mode: 3 * this logo ...WLED/images/wled_logo_akemi.png * encode map = 1, 2, 3 */ const uint8_t u8x8_wled_logo_akemi_4x4[388] U8X8_FONT_SECTION("u8x8_wled_logo_akemi_4x4") = "\1\3\4\4\0\0\0\0\0\0\0\0\0\340\360\10\350\10\350\210\270\210\350\210\270\350\10\360\340\0\0\0" "\0\0\200\200\0\0@\340\300\340@\0\0\377\377\377\377\377\377\37\37\207\207\371\371\371\377\377\377\0\0\374" "\374\7\7\371\0\0\6\4\15\34x\340\200\177\177\377\351yy\376\356\357\217\177\177\177o\377\377\0\70\77" "\277\376~\71\0\0\0\0\0\0\0\1\3\3\3\1\0\0\37\77\353\365\77\37\0\0\0\0\5\7\2\3" "\7\4\0\0\300\300\300\300\200\200\200\0\0\0\0\0\0\0\200\200\300\300\300\300\200\200\0\0\0\0\0\0" "\0\200\200\300\371\37\37\371\371\7\7\377\374\0\0\0\374\377\377\37\37\341\341\377\377\377\377\374\0\0\0\374" "\377\7\7\231\371\376>\371\371>~\377\277\70\0\270\377\177\77\376\376\71\371\371\71\177\377\277\70\0\70\377" "\177>\376\371\377\377\0\77\77\0\0\4\7\2\7\5\0\0\0\377\377\0\77\77\0\0\0\5\7\2\7\5" "\0\0\377\377\300\300\300\200\200\0\0\0\0\0\0\0\200\200\300\300\300\300\300\200\200\0\0\0\0\0\0\0" "\0\0\0\0\231\231\231\371\377\377\374\0\0\0\374\377\347\347\371\1\1\371\371\7\7\377\374\0\0\0@\340" "\300\340@\0\71\371\371\71\177\377\277\70\0\70\277\377\177\71\371\370\70\371\371~\376\377\77\70\200\340x\34" "\15\4\6\0\0\77\77\0\0\0\5\7\2\7\5\0\0\0\377\377\0\77\77\0\0\1\3\3\1\1\0\0" "\0\0\0"; /* Fontname: wled_logo_akemi_5x5 Copyright: Benji (https://github.com/proto-molecule) Glyphs: 3/3 BBX Build Mode: 3 * this logo ...WLED/images/wled_logo_akemi.png * encoded = 1, 2, 3 */ /* const uint8_t u8x8_wled_logo_akemi_5x5[604] U8X8_FONT_SECTION("u8x8_wled_logo_akemi_5x5") = "\1\3\5\5\0\0\0\0\0\0\0\0\0\0\0\0\340\340\374\14\354\14\354\14|\14\354\14||\14\354" "\14\374\340\340\0\0\0\0\0\0\0\200\0\0\0\200\200\0\200\200\0\0\0\0\377\377\377\376\377\376\377\377" "\377\377\77\77\307\307\307\307\306\377\377\377\0\0\0\360\374>\77\307\0\0\61cg\357\347\303\301\200\0\0" "\377\377\377\317\317\317\317\360\360\360\374\374\377\377\377\377\377\377\377\377\0\0\200\377\377\340\340\37\0\0\0\0" "\0\0\1\3\17\77\374\360\357\357\177\36\14\17\357\377\376\376>\376\360\357\17\17\14>\177o\340\300\343c" "{\77\17\3\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\17\37\37\362\375\37\37\17\0\0" "\0\0\1\1\1\0\1\1\1\0\0\0\200\300\300\300\300\200\200\0\0\0\0\0\0\0\0\0\0\0\200\200" "\300\300\300\300\200\200\0\0\0\0\0\0\0\0\0\0\0\0\200\200\307\307\377\377\307\307\307\77>\374\360\0" "\0\0\360\374\376\377\377\377\7\7\7\377\377\377\377\376\374\360\0\0\0\0\360\374\36\37\37\343\37\37\340\340" "\37\37\37\340\340\377\377\200\0\200\377\377\377\340\340\340\37\37\37\37\37\37\37\377\377\377\200\0\0\200\377\377" "\340\340\340\34\377\377\3\3\377\377\3\17\77{\343\303\300\303\343s\77\37\3\377\377\3\3\377\377\3\17\77" "{\343\303\300\300\343{\37\17\3\377\377\377\377\0\0\37\37\0\0\1\1\1\1\0\1\1\1\1\0\0\377" "\377\0\0\37\37\0\0\1\1\1\1\0\0\1\1\1\0\0\377\377\300\300\300\200\200\0\0\0\0\0\0\0" "\0\0\0\0\200\200\300\300\300\300\200\200\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\343\343\343\343" "\343\377\376\374\360\0\0\0\360\374\376\77\77\307\307\7\7\307\307\307\77>\374\360\0\0\0\0\0\200\200\0" "\200\200\0\0\34\34\34\37\37\377\377\377\377\200\0\200\377\377\377\377\37\37\37\0\0\37\37\37\340\340\377\377" "\200\0\0\0\1\303\347\357gc\61\0\3\3\377\377\3\7\37\177s\343\300\303s{\37\17\7\3\377\377" "\3\3\377\377\3\37\77scp<\36\17\3\1\0\0\0\0\0\0\0\37\37\0\0\0\1\1\1\0\1" "\1\1\0\0\0\0\377\377\0\0\37\37\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0"; */ /* Fontname: wled_logo_2x2 Copyright: Benji (https://github.com/proto-molecule) Glyphs: 4/4 BBX Build Mode: 3 * this logo https://cdn.discordapp.com/attachments/706623245935444088/927361780613799956/wled_scaled.png * encode map = 1, 2, 3, 4 */ const uint8_t u8x8_wled_logo_2x2[133] U8X8_FONT_SECTION("u8x8_wled_logo_2x2") = "\1\4\2\2\0\0\0\0\0\200\200\360\360\16\16\16\16\0\0\0\340\340\340\340\340\37\37\1\1\0\0\0" "\0\0\0\0\360\360\16\16\16\200\200\16\16\16\360\360\0\0\0\200\37\37\340\340\340\37\37\340\340\340\37\37" "\0\0\0\37\200~~\0\0\0\0\0\0\0\360\360\216\216\216\216\37\340\340\340\340\340\340\340\0\0\37\37" "\343\343\343\343\16\16\0\0ppp\16\16\376\376\16\16\16\360\360\340\340\0\0\0\0\0\340\340\377\377\340" "\340\340\37\37"; /* Fontname: wled_logo_4x4 Copyright: Created with Fony 1.4.7 Glyphs: 4/4 BBX Build Mode: 3 * this logo https://cdn.discordapp.com/attachments/706623245935444088/927361780613799956/wled_scaled.png * encode map = 1, 2, 3, 4 */ /* const uint8_t u8x8_wled_logo_4x4[517] U8X8_FONT_SECTION("u8x8_wled_logo_4x4") = "\1\4\4\4\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\374\374\374\374\374\374\374\374\374" "\0\0\0\0\0\0\0\0\0\0\0\0\0\300\300\300\300\300\377\377\377\377\377\0\0\0\0\0\0\0\0\0" "\0\0\0\0\0\0\0\0\0\0\0\0\0\377\377\377\377\377\17\17\17\17\17\0\0\0\0\0\0\0\0\0" "\0\0\0\0\370\370\370\370\370\370\370\370\370\7\7\7\7\7\0\0\0\0\0\0\0\0\0\0\0\0\0\0" "\0\0\0\0\0\0\0\0\0\0\374\374\374\374\374\0\0\0\0\0\374\374\374\374\374\0\0\0\0\0\0\0" "\0\0\0\0\0\377\377\377\377\377\0\0\0\0\0\300\300\300\300\300\0\0\0\0\0\377\377\377\377\377\0\0" "\0\0\300\300\0\377\377\377\377\377\0\0\0\0\0\377\377\377\377\377\0\0\0\0\0\377\377\377\377\377\0\0" "\0\0\377\377\0\7\7\7\7\7\370\370\370\370\370\7\7\7\7\7\370\370\370\370\370\7\7\7\7\7\0\0" "\0\0\7\7\0\0\0\374\374\374\374\374\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\374\374\374" "\374\374\374\374\300\300\300\77\77\77\77\77\0\0\0\0\0\0\0\0\0\0\0\0\377\377\377\377\377\300\300\300" "\300\300\300\300\377\377\377\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\377\377\377\377\377\37\37\37" "\37\37\37\37\7\7\7\370\370\370\370\370\370\370\370\370\370\370\370\370\0\0\0\0\7\7\7\7\7\370\370\370" "\370\370\370\370\374\374\374\374\374\374\0\0\0\0\0\0\0\0\374\374\374\374\374\374\374\374\374\374\374\374\374\374" "\0\0\0\0\300\300\0\0\0\0\0\0\0\77\77\77\77\77\0\0\0\0\377\377\377\377\377\0\0\0\0\377" "\377\377\377\377\37\37\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\377\377\377\377\377\0\0\0\0\377" "\377\377\377\377\370\370\370\370\370\370\0\0\0\0\0\0\0\0\370\370\370\370\377\377\377\377\377\370\370\370\370\377" "\7\7\7\7"; */ /* Fontname: 4LineDisplay_WLED_icons_1x Copyright: Benji (https://github.com/proto-molecule) Glyphs: 13/13 BBX Build Mode: 3 * 1 = sun * 2 = skip forward * 3 = fire * 4 = custom palette * 5 = puzzle piece * 6 = moon * 7 = brush * 8 = contrast * 9 = power-standby * 10 = star * 11 = heart * 12 = Akemi *----------- * 20 = wifi * 21 = media-play */ const uint8_t u8x8_4LineDisplay_WLED_icons_1x1[172] U8X8_FONT_SECTION("u8x8_4LineDisplay_WLED_icons_1x1") = "\1\25\1\1\0B\30<<\30B\0~<\30\0~<\30\0p\374\77\216\340\370\360\0||>\36\14\64 \336\67" ";\336 \64\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\2\1\11\311" "\311\1\2\0\0~<<\30\30\0"; /* Fontname: 4LineDisplay_WLED_icons_2x1 Copyright: Benji (https://github.com/proto-molecule) Glyphs: 11/11 BBX Build Mode: 3 * 1 = sun * 2 = skip forward * 3 = fire * 4 = custom palette * 5 = puzzle piece * 6 = moon * 7 = brush * 8 = contrast * 9 = power-standby * 10 = star * 11 = heart * 12 = Akemi */ const uint8_t u8x8_4LineDisplay_WLED_icons_2x1[196] U8X8_FONT_SECTION("u8x8_4LineDisplay_WLED_icons_2x1") = "\1\14\2\1\20\20BB\30\30<\275\275<\30\30BB\20\20\377~<<\70\30\20\0\377~<<" "\70\30\20\0\60p\370\374\77>\236\214\300\340\370\360\360\340\0\0\34" "\66\66<\34\374\374\374\374~\77\77~\374\374\374\374 pp \30<~~\377\370\360\360\340\340\340\340" "@@ \0\200\300\340\360\360p`\10\34\34\16\6\6\3\0\0\70|~\376\376\377\377\377\201\201\203\202" "\302Fl\70\70xL\204\200\200\217\217\200\200\204Lx\70\0\0\10\10\30\330x|\77\77|x\330\30" "\10\10\0\0\14\36\37\77\77\177~\374\374~\177\77\77\37\36\14\24\64 \60>\26\367\33\375\36>\60" " \64\24"; /* Fontname: 4LineDisplay_WLED_icons_2x Copyright: Glyphs: 11/11 BBX Build Mode: 3 * 1 = sun * 2 = skip forward * 3 = fire * 4 = custom palette * 5 = puzzle piece * 6 = moon * 7 = brush * 8 = contrast * 9 = power-standby * 10 = star * 11 = heart * 12 = Akemi */ const uint8_t u8x8_4LineDisplay_WLED_icons_2x2[389] U8X8_FONT_SECTION("u8x8_4LineDisplay_WLED_icons_2x2") = "\1\14\2\2\200\200\14\14\300\340\360\363\363\360\340\300\14\14\200\200\1\1\60\60\3\7\17\317\317\17\7\3" "\60\60\1\1\374\370\360\340\340\300\200\0\374\370\360\340\340\300\200\0\77\37\17\7\7\3\1\0\77\37\17\7" "\7\3\1\0\0\200\340\360\377\376\374\360\0\0\300\200\0\0\0\0\17\77\177\377\17\7\301\340\370\374\377\377" "\377|\0\0\360\370\234\236\376\363\363\377\377\363\363\376><\370\360\3\17\77yy\377\377\377\377\317\17\17" "\17\17\7\3\360\360\360\360\366\377\377\366\360\360\360\360\0\0\0\0\377\377\377\377\237\17\17\237\377\377\377\377" "\6\17\17\6\340\370\374\376\377\340\200\0\0\0\0\0\0\0\0\0\3\17\37\77\177\177\177\377\376|||" "\70\30\14\0\0\0\0\0\0\0\0``\360\370|<\36\7\2\0\300\360\376\377\177\77\36\0\1\1\0" "\0\0\0\0\340\370\374\376\376\377\377\377\3\3\7\6\16<\370\340\7\37\77\177\177\377\377\377\300\300\340`" "p<\37\7\300\340p\30\0\0\377\377\0\0\30p\340\300\0\0\17\37\70`\340\300\300\300\300\340`\70" "\37\17\0\0\0@\300\300\300\300\340\374\374\340\300\300\300\300@\0\0\0\0\1s\77\37\17\17\37\77s" "\1\0\0\0\360\370\374\374\374\374\370\360\360\370\374\374\374\374\370\360\0\1\3\7\17\37\77\177\177\77\37\17" "\7\3\1\0\200\200\0\0\0\360\370\374<\334\330\360\0\0\200\200\2\2\14\30\24\37\6~\7\177\7\37" "\24\30\16\2"; /* Fontname: 4LineDisplay_WLED_icons_3x Copyright: Benji (https://github.com/proto-molecule) Glyphs: 11/11 BBX Build Mode: 3 * 1 = sun * 2 = skip forward * 3 = fire * 4 = custom palette * 5 = puzzle piece * 6 = moon * 7 = brush * 8 = contrast * 9 = power-standby * 10 = star * 11 = heart * 12 = Akemi */ const uint8_t u8x8_4LineDisplay_WLED_icons_3x3[868] U8X8_FONT_SECTION("u8x8_4LineDisplay_WLED_icons_3x3") = "\1\14\3\3\0\0\34\34\34\0\200\300\300\340\347\347\347\340\300\300\200\0\34\34\34\0\0\0\34\34\34\0" "\0>\377\377\377\377\377\377\377\377\377\377\377>\0\0\34\34\34\0\0\0\16\16\16\0\0\1\1\3ss" "s\3\1\1\0\0\34\34\34\0\0\0\370\360\340\300\300\200\0\0\0\0\0\0\370\360\340\300\300\200\0\0" "\0\0\0\0\377\377\377\377\377\377\377\376~<\70\20\377\377\377\377\377\377\377\376~<\70\20\37\17\17\7" "\3\1\1\0\0\0\0\0\37\17\17\7\3\1\1\0\0\0\0\0\0\0\0\0\0\300\361\376\374\370\360\300" "\0\0\0\0\0\0\0\0\0\0\0\0\300\370\374\376\377\377\377\377\377\177\77\17\6\0\200\342\374\370\360\340" "\200\0\0\0\1\17\37\77\177\377\7\3\0\200\360\370\374\376\377\377\377\377\377\377\77\0\0\0\0\200\340\360" "\370\370\374\316\206\206\317\377\377\377\317\206\206\316\374\374\370\360\340\200<\377\377\371\360py\377\377\377\377\377" "\377\377\377\377\377\377\363\341\341\363\377\177\0\1\7\17\34\70x|\377\377\377\377\367\363c\3\3\3\3\1" "\1\1\0\0\300\300\300\300\300\300\300\316\377\377\377\316\300\300\300\300\300\300\0\0\0\0\0\0\377\377\377\377" "\377\377\377\377\377\377\377\377\377\377\377\377\377\377\300\300\340\340\340\300\377\377\377\377\377\377\377\307\3\3\3\307" "\377\377\377\377\377\377\1\1\3\3\3\1\0\300\340\370\374\374\376\377\0\0\0\0\0\0\0\0\0\0\0\0" "\0\0\0\0>\377\377\377\377\377\377\377\377\374\360\340\300\300\200\200\0\0\0\0\0\0\200\200\0\1\7\17" "\37\37\77\177\177\177\177\377\377\377\177\177\177\77\77\37\17\7\3\0\0\0\0\0\0\0\0\0\0\0\0\0" "\200\200\300\340\340\360\370\374|>\17\6\0\0\0\0\0\340\340\360\360\360\342\303\7\17\37\77\37\7\3\1" "\0\0\0\0\0\200\340\360\377\377\377\377\177\77\37\17\0\0\0\0\0\0\0\0\0\0\0\0\0\200\340\360" "\370\374\374\376\376\376\377\377\7\7\7\6\16\16\34\70\360\340\300\0|\377\377\377\377\377\377\377\377\377\377\377" "\0\0\0\0\0\0\0\0\0\377\377\377\0\3\7\17\37\77\177\177\377\377\377\377\340\340\340\340pp\70<" "\37\17\3\0\0\0\200\300\340\340\300\0\0\377\377\377\0\0\300\340\340\300\200\0\0\0\0\0\370\376\377\17" "\3\0\0\0\0\17\17\17\0\0\0\0\0\3\17\377\376\370\0\0\0\7\17\37~\376\376\377\377\377\377\377\376\376~>\36\16\6\6\2\0\0\0\0" "\0\300x<\37\17\17\7\3\7\17\17\37>\177\177\377\377\377\377\377\377\371p\60\0\0\0\0\0\0\0\0" "\0\0\0\0\0\0\0\0\0\0\0\0<\376\377\377\377\377\376<\0\0\0\0\0\0\0\0\0\0\0\0" "\0\0\0\0\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\0\0\0\0" "\0\0\0\0\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377~~\377\377" "\377\377~<\377\377\377\377\377\377\377\377\303\1\0\0\0\0\1\303\377\377\377\377\377\377\377\377\0\0\0\0" "\0\0\0\0\0\0\200\340\360\370\374\374\376\376\1\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" "\0\0\0\0\370\377\377\377\377\377\377\377\377\377\376\360\300\200\0\0\0\0\0\0\0\0\0\0\0\0\0\0" "\0\0\0\0\7\77\377\377\377\377\377\377\377\377\377\377\377\377\377\376\374\370\370\360\360\360\340\340\340\340\340\340" "\340\340\60\0\0\0\0\1\3\7\17\37\37\77\77\77\177\177\177\177\177\177\177\177\77\77\77\37\37\17\7\3" "\1\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\200\200\300\340\340\360\370\374\374" "~\77\16\4\0\0\0\0\0\0\0\0\0\0\0\0\0\0\30\34>~\377\377\377\377\177\77\37\7\3\0" "\0\0\0\0\0\0\0\0\0\360\374\376\377\377\377\377\377\376\374\370\0\0\0\3\3\1\0\0\0\0\0\0" "\0\0\0\0@@\340\370\374\377\377\377\177\177\177\77\37\17\7\1\0\0\0\0\0\0\0\0\0\0\0\0" "\0\0\0\0\0\0\200\300\340\360\370\374\374\376\376\376\377\377\377\377\17\17\17\37\36\36>|\374\370\360\340" "\300\200\0\0\360\376\377\377\377\377\377\377\377\377\377\377\377\377\377\377\0\0\0\0\0\0\0\0\0\1\3\37" "\377\377\376\360\17\177\377\377\377\377\377\377\377\377\377\377\377\377\377\377\0\0\0\0\0\0\0\0\0\200\300\370" "\377\377\177\17\0\0\1\3\7\17\37\77\77\177\177\177\377\377\377\377\360\360\360\370xx|>\77\37\17\7" "\3\1\0\0\0\0\0\0\0\200\300\200\0\0\0\0\377\377\377\377\0\0\0\0\200\300\200\0\0\0\0\0" "\0\0\0\0\300\360\374\376\177\37\7\3\3\0\0\0\377\377\377\377\0\0\0\3\3\7\37\177\376\374\360\300" "\0\0\0\0\77\377\377\377\340\200\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\200\340\377\377\377\77" "\0\0\0\0\0\0\3\7\17\37><|x\370\360\360\360\360\360\360\370x|<>\37\17\7\3\0\0" "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\340\374\374\340\0\0\0\0\0\0\0\0\0\0" "\0\0\0\0\0\20\60p\360\360\360\360\360\360\360\360\370\377\377\377\377\377\377\370\360\360\360\360\360\360\360\360" "p\60\20\0\0\0\0\0\0\0\1\3\7\317\377\377\377\377\377\377\377\377\377\377\377\377\317\7\3\1\0\0" "\0\0\0\0\0\0\0\0\0\0\0p>\37\17\17\7\3\1\0\0\1\3\7\17\17\37>p\0\0\0" "\0\0\0\0\0\200\300\340\340\360\360\360\360\360\360\340\340\300\200\0\0\200\300\340\340\360\360\360\360\360\360\340" "\340\300\200\0~\377\377\377\377\377\377\377\377\377\377\377\377\377\377\376\376\377\377\377\377\377\377\377\377\377\377\377" "\377\377\377~\0\1\3\7\17\37\77\177\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\177\77\37\17" "\7\3\1\0\0\0\0\0\0\0\0\0\0\1\3\7\17\37\77\177\177\77\37\17\7\3\1\0\0\0\0\0" "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\200\300\340\340\360\360\360\360\340\340\300\200\0\0\0\0\0\0" "\0\0\0\0\0@\340\300\340@\0\0\0\376\377\377\177\177\177\237\207\347\371\371\371\377\376\0\0\0\0@" "\340\300\340@\2\4\4\35x\340\200\0\30\237\377\177\36\376\376\37\37\377\377\37\177\377\237\30\0\200\340x" "\34\5\4\2\0\0\0\0\0\1\3\3\3\1\0\0\0\17\17\0\0\17\17\0\0\0\1\3\3\3\1\0" "\0\0\0"; */ /* Fontname: 4LineDisplay_WLED_icons_6x Copyright: Benji (https://github.com/proto-molecule) Glyphs: 11/11 BBX Build Mode: 3 * 1 = sun * 2 = skip forward * 3 = fire * 4 = custom palette * 5 = puzzle piece * 6 = moon * 7 = brush * 8 = contrast * 9 = power-standby * 10 = star * 11 = heart * 12 = Akemi */ // you can replace this (wasteful) font by using 3x3 variant with draw2x2Glyph() const uint8_t u8x8_4LineDisplay_WLED_icons_6x6[3460] U8X8_FONT_SECTION("u8x8_4LineDisplay_WLED_icons_6x6") = "\1\14\6\6\0\0\0\0\0\0\200\300\300\300\300\200\0\0\0\0\0\0\0\0\0\36\77\77\77\77\36\0" "\0\0\0\0\0\0\0\0\200\300\300\300\300\200\0\0\0\0\0\0\0\0\0\0\0\0\7\17\17\17\17\7" "\0\0\0\0\200\300\340\340\340\360\360\360\360\360\360\340\340\340\300\200\0\0\0\0\7\17\17\17\17\7\0\0" "\0\0\0\0\300\340\340\340\340\300\0\0\0\0\0\0\340\374\376\377\377\377\377\377\377\377\377\377\377\377\377\377" "\377\377\377\377\377\376\374\340\0\0\0\0\0\0\300\340\340\340\340\300\3\7\7\7\7\3\0\0\0\0\0\0" "\7\77\177\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\177\77\7\0\0\0\0\0\0\3\7" "\7\7\7\3\0\0\0\0\0\0\340\360\360\360\360\340\0\0\0\0\1\3\7\7\7\17\17\17\17\17\17\7" "\7\7\3\1\0\0\0\0\340\360\360\360\360\340\0\0\0\0\0\0\0\0\0\0\0\0\1\3\3\3\3\1" "\0\0\0\0\0\0\0\0\0x\374\374\374\374x\0\0\0\0\0\0\0\0\0\1\3\3\3\3\1\0\0" "\0\0\0\0\300\200\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\300\200\0\0" "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\377\377\377\376\376\374\370\360\360\340\300\200" "\200\0\0\0\0\0\0\0\0\0\0\0\377\377\377\376\376\374\370\360\360\340\300\200\200\0\0\0\0\0\0\0" "\0\0\0\0\377\377\377\377\377\377\377\377\377\377\377\377\377\377\376\374\374\370\360\340\340\300\200\0\377\377\377\377" "\377\377\377\377\377\377\377\377\377\377\376\374\374\370\360\340\340\300\200\0\377\377\377\377\377\377\377\377\377\377\377\377" "\377\377\177\77\77\37\17\7\7\3\1\0\377\377\377\377\377\377\377\377\377\377\377\377\377\377\177\77\77\37\17\7" "\7\3\1\0\377\377\377\177\177\77\37\17\17\7\3\1\1\0\0\0\0\0\0\0\0\0\0\0\377\377\377\177" "\177\77\37\17\17\7\3\1\1\0\0\0\0\0\0\0\0\0\0\0\3\1\0\0\0\0\0\0\0\0\0\0" "\0\0\0\0\0\0\0\0\0\0\0\0\3\1\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\1\376\374\374\370\360\340\300\200\0\0\0\0\0\0\0" "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\200\340\360\374" "\377\377\377\377\377\377\377\377\377\376\370\300\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" "\0\0\0\0\0\0\300\340\360\374\376\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\37\0\0\0\0" "\0\0\4\370\360\360\340\300\200\0\0\0\0\0\0\0\0\0\0\0\370\377\377\377\377\377\377\377\377\377\377\377" "\377\377\377\377\377\177\77\37\7\3\0\0\0\0\0\200\300\360\374\377\377\377\377\377\377\377\376\370\340\0\0\0" "\0\0\0\0\3\37\177\377\377\377\377\377\377\377\377\377\77\17\7\1\0\0\0\0\0\200\300\360\370\374\376\377" "\377\377\377\377\377\377\377\377\377\377\377\377\377\377\0\0\0\0\0\0\0\0\0\1\3\7\17\37\77\77\177\200" "\0\0\0\0\0\0\340\374\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\177\77\17\1\0\0" "\0\0\0\0\0\0\0\0\0\0\0\200\300\340\340\360\360\370|<>>>~\377\377\377\377\377\377\377\177" "\77\36\36\36\36<|\370\370\360\360\340\340\200\0\0\0\0\0\0\0\0\300\360\374\376\377\377\377\377\377\377" "\377\360\340\300\300\300\300\340\360\377\377\377\377\377\377\370\360\340\340\340\340\360\370\377\377\377\377\377\377\377\377\377" "\374\360\340\200\360\377\377\377\377\377\207\3\1\1\1\1\3\207\377\377\377\377\377\377\377\377\377\377\377\377\377\377" "\377\377\377\377\377\377\377\207\3\1\1\1\1\3\207\377\377\377\377\377\17\377\377\377\377\377\377\377\376~>>" "\77\77\177\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\376\376\376\376\377\377\377" "\177\77\37\7\0\0\3\17\77\177\377\377\360\340\300\300\300\300\340\360\377\377\377\377\377\377\377\377\377\377\77\17" "\17\7\7\7\7\7\7\7\7\7\3\3\3\3\1\0\0\0\0\0\0\0\0\0\0\0\0\1\3\7\17\37" "\37\77\77\177\177\177\377\377\377\377\377\377\377\377\377~\30\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\370\374\376\377\377\377\377\377\377\376\374\360\0\0\0\0" "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\360\360\360\360\360\360\360\360\360\360\360\360" "\360\363\377\377\377\377\377\377\377\377\363\360\360\360\360\360\360\360\360\360\360\360\360\360\0\0\0\0\0\0\0\0" "\0\0\0\0\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377" "\377\377\377\377\377\377\377\377\0\0\0\0\0\0\0\0\0\0\0\0\377\377\377\377\377\377\377\377\377\377\377\377" "\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\374\374\376\376\377\377\377\377" "\377\376\374\360\377\377\377\377\377\377\377\377\377\377\377\377\177\77\37\17\17\17\17\17\17\37\77\177\377\377\377\377" "\377\377\377\377\377\377\377\377\3\3\7\7\17\17\17\17\7\7\3\0\377\377\377\377\377\377\377\377\377\377\377\377" "\360\300\0\0\0\0\0\0\0\0\300\360\377\377\377\377\377\377\377\377\377\377\377\377\0\0\0\0\0\0\0\0" "\0\0\0\0\0\0\0\0\0\200\300\340\360\360\370\374\374\376\376\7\0\0\0\0\0\0\0\0\0\0\0\0" "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\300\360\374\376\377\377\377\377\377\377\377" "\377\377\377\340\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" "\0\0\0\0\374\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\374\360\300\200\0\0\0\0\0\0\0" "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\17\177\377\377\377\377\377\377\377\377\377\377" "\377\377\377\377\377\377\377\377\377\377\376\374\370\360\360\340\340\300\300\300\200\200\200\200\0\0\0\0\0\0\200\200" "\200\200\0\0\0\0\1\7\37\77\177\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377" "\377\377\377\377\377\377\377\377\377\377\377\377\377\177\77\37\7\1\0\0\0\0\0\0\0\0\0\0\1\3\3\7" "\17\17\37\37\37\77\77\77\77\177\177\177\177\177\177\77\77\77\77\37\37\37\17\17\7\3\3\1\0\0\0\0" "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" "\0\0\0\0\0\0\200\200\300\340\360\360\370\374\374\376\377~\34\10\0\0\0\0\0\0\0\0\0\0\0\0" "\0\0\0\0\0\0\0\0\0\0\0\200\300\300\340\360\360\370\374\376\376\377\377\377\377\377\377\177\77\17\7\3" "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\4\6\17\17\37\77\177\377" "\377\377\377\377\377\377\77\37\7\3\1\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\300\370\374\376" "\376\377\377\377\377\377\377\376\376\374\370\340\0\0\0\0\3\17\7\3\1\0\0\0\0\0\0\0\0\0\0\0" "\0\0\0\0\0\0\0\0\0\0\200\360\377\377\377\377\377\377\377\377\377\377\377\377\377\377\177\17\0\0\0\0" "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0`px\374\376\377\377\377\377\377\377" "\177\177\177\77\77\37\17\7\3\1\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" "\0\0\0\0\0\0\0\0\0\0\0\200\300\340\360\360\370\374\374\374\376\376\376\377\377\377\377\377\77\77\77\77" "\177~~\376\374\374\374\370\360\360\340\300\200\0\0\0\0\0\0\0\0\0\340\360\374\376\377\377\377\377\377\377" "\377\377\377\377\377\377\377\377\377\377\377\377\0\0\0\0\0\0\0\0\0\1\1\3\7\17\37\177\377\377\376\374" "\360\340\0\0\370\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\0\0\0\0" "\0\0\0\0\0\0\0\0\0\0\0\0\1\17\377\377\377\377\377\370\37\377\377\377\377\377\377\377\377\377\377\377" "\377\377\377\377\377\377\377\377\377\377\377\377\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\200\360\377\377" "\377\377\377\37\0\0\7\17\77\177\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\0\0\0\0" "\0\0\0\0\0\200\200\300\340\360\370\376\377\377\177\77\17\7\0\0\0\0\0\0\0\0\0\1\3\7\17\17" "\37\77\77\77\177\177\177\377\377\377\377\377\374\374\374\374\376~~\177\77\77\77\37\17\17\7\3\1\0\0\0" "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\377\377\377\377\377\377\0\0\0\0" "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\200\300\340\360\370\374\376\376|" "x \0\0\0\0\377\377\377\377\377\377\0\0\0\0 x|\376\376\374\370\360\340\300\200\0\0\0\0\0" "\0\0\0\0\300\370\376\377\377\377\177\17\7\1\0\0\0\0\0\0\0\0\377\377\377\377\377\377\0\0\0\0" "\0\0\0\0\1\7\37\177\377\377\377\376\370\200\0\0\0\0\0\0\177\377\377\377\377\377\200\0\0\0\0\0" "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\200\377\377\377\377\377\177\0\0" "\0\0\0\0\0\7\37\177\377\377\377\374\370\340\300\200\200\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" "\0\200\200\300\340\370\374\377\377\377\177\37\7\0\0\0\0\0\0\0\0\0\0\0\0\1\3\7\17\37\37\77" "\77\177~~~\374\374\374\374\374\374\374\374~~~\177\77\77\37\37\17\7\3\1\0\0\0\0\0\0\0" "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\340\374\374\340\0\0" "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" "\0\0\0\0\0\0\0\300\370\377\377\377\377\377\377\370\300\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" "\0\0\0\0\0\4\14\34<<|\374\374\374\374\374\374\374\374\374\374\374\376\377\377\377\377\377\377\377\377\377" "\377\376\374\374\374\374\374\374\374\374\374\374\374|<<\34\14\4\0\0\0\0\0\0\0\0\0\1\3\3\7" "\17\37\77\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\77\37\17\7\3\3\1\0\0\0\0" "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\300\370\377\377\377\377\377\377\177\77\37\17\17\37\77\177" "\377\377\377\377\377\377\370\300\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0p>" "\37\17\17\7\3\1\0\0\0\0\0\0\0\0\0\0\0\0\1\3\7\17\17\37>p\0\0\0\0\0\0" "\0\0\0\0\0\0\0\0\0\0\200\200\200\300\300\300\300\300\300\200\200\200\0\0\0\0\0\0\0\0\0\0" "\0\0\200\200\200\300\300\300\300\300\300\200\200\200\0\0\0\0\0\0\200\360\370\374\376\377\377\377\377\377\377\377" "\377\377\377\377\377\377\377\376\374\370\360\200\200\360\370\374\376\377\377\377\377\377\377\377\377\377\377\377\377\377\377\376" "\374\370\360\200\37\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377" "\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\37\0\0\1\3\7\17\37\77\177\377\377\377" "\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\177\77\37\17\7" "\3\1\0\0\0\0\0\0\0\0\0\0\0\0\1\3\7\17\37\77\177\377\377\377\377\377\377\377\377\377\377\377" "\377\377\377\177\77\37\17\7\3\1\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" "\0\0\0\0\0\0\1\3\7\17\37\77\77\37\17\7\3\1\0\0\0\0\0\0\0\0\0\0\0\0\0\0" "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\300\300\300\300\300\300\300" "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" "\0\0\340\370\370\376\376\377\377\377\377\377\377\377\377\77\77\77>\376\370\370\340\0\0\0\0\0\0\0\0\0" "\0\0\0\0\0 p\360\340\360p \0\0\0\0\0\0\377\377\377\377\177\177\177\177\177\207\207\340\340\377" "\377\377\377\377\377\377\377\0\0\0\0\0 p\360\340\360p \0\6\4\14\14\15|x\360\200\200\0\0" "pp\177\177\377\377\374|\374\374\374\177\177\177\377\377\377\177\377\377\377\377\177pp\0\0\200\200\360x}" "\14\14\4\6\0\0\0\0\0\0\0\3\37\37|ppp\34\34\37\3\3\0\377\377\377\0\0\0\377\377" "\377\0\3\3\37\37\34ppp~\37\37\3\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" "\0\0\0\0\0\0\0\0\7\7\7\0\0\0\7\7\7\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" "\0\0\0"; /* Fontname: akemi_8x8 Copyright: Benji (https://github.com/proto-molecule) Glyphs: 1/1 BBX Build Mode: 3 * 12 = Akemi */ /* const uint8_t u8x8_akemi_8x8[516] U8X8_FONT_SECTION("u8x8_akemi_8x8") = "\14\14\10\10\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" "\200\200\200\200\200\200\200\200\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\340\340\370\370\376\376\376\376" "\377\377\377\377\377\377\377\377\376\376\376\376\370\370\340\340\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\376\376\377\377\377\377\377\377\377\377" "\377\377\377\377\37\37\37\343\343\343\343\343\343\377\377\377\376\376\0\0\0\0\0\0\0\0\0\0\0\0\0\0" "\0\0\0\0\0\30\30~~\370\370~~\30\30\0\0\0\0\0\0\0\377\377\377\377\377\77\77\77\77\77" "\77\300\300\300\370\370\370\377\377\377\377\377\377\377\377\377\377\377\0\0\0\0\0\0\0\30\0f\0\200\0\0" "\0\0\0\0\6\6\30\30\30\31\371\370\370\340\340\0\0\0\0\0\340\340\377\377\377\377\377\376\376\376\376\376" "\376\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\371\346\346\6\6\6\6\6\0\340\340\340\341\0\0" "\0\0\0\0\0\0\0\0\0\0\1\1\37\37\377\376\376\340\340\200\201\201\341\341\177\177\37\37\1\1\377\377" "\377\377\1\1\1\1\377\377\377\377\1\1\37\37\177\177\341\341\201\201\200\200\370\370\376\376\37\37\1\1\0\0" "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\1\1\7\7\7\7\7\7\1\1\0\0\0\0\0\0\377\377" "\377\377\0\0\0\0\377\377\377\377\0\0\0\0\0\0\1\1\7\7\7\7\7\7\1\1\0\0\0\0\0\0" "\0\0\0"; */ ================================================ FILE: usermods/usermod_v2_four_line_display_ALT/library.json ================================================ { "name": "four_line_display_ALT", "build": { "libArchive": false }, "dependencies": { "U8g2": "~2.34.4", "Wire": "" } } ================================================ FILE: usermods/usermod_v2_four_line_display_ALT/platformio_override.sample.ini ================================================ [platformio] default_envs = esp32dev_fld [env:esp32dev_fld] extends = env:esp32dev_V4 custom_usermods = ${env:esp32dev_V4.custom_usermods} four_line_display_ALT build_flags = ${env:esp32dev_V4.build_flags} -D FLD_TYPE=SH1106 -D I2CSCLPIN=27 -D I2CSDAPIN=26 ================================================ FILE: usermods/usermod_v2_four_line_display_ALT/readme.md ================================================ # I2C/SPI 4 Line Display Usermod ALT This usermod could be used in compination with `usermod_v2_rotary_encoder_ui_ALT`. ## Functionalities Press the encoder to cycle through the options: * Brightness * Speed * Intensity * Palette * Effect * Main Color * Saturation Press and hold the encoder to display Network Info. If AP is active, it will display the AP, SSID and Password Also shows if the timer is enabled. [See the pair of usermods in action](https://www.youtube.com/watch?v=ulZnBt9z3TI) ## Installation Copy the example `platformio_override.sample.ini` to the root directory of your particular build. ## Configuration These options are configurable in Config > Usermods ### Usermod Setup * Global I2C GPIOs (HW) - Set the SDA and SCL pins ### 4LineDisplay * `enabled` - enable/disable usermod * `type` - display type in numeric format * 1 = I2C SSD1306 128x32 * 2 = I2C SH1106 128x32 * 3 = I2C SSD1306 128x64 (4 double-height lines) * 4 = I2C SSD1305 128x32 * 5 = I2C SSD1305 128x64 (4 double-height lines) * 6 = SPI SSD1306 128x32 * 7 = SPI SSD1306 128x64 (4 double-height lines) * 8 = SPI SSD1309 128x64 (4 double-height lines) * 9 = I2C SSD1309 128x64 (4 double-height lines) * `pin` - GPIO pins used for display; SPI displays can use SCK, MOSI, CS, DC & RST * `flip` - flip/rotate display 180° * `contrast` - set display contrast (higher contrast may reduce display lifetime) * `screenTimeOutSec` - screen saver time-out in seconds * `sleepMode` - enable/disable screen saver * `clockMode` - enable/disable clock display in screen saver mode * `showSeconds` - Show seconds on the clock display * `i2c-freq-kHz` - I2C clock frequency in kHz (may help reduce dropped frames, range: 400-3400) ### PlatformIO requirements Note: the Four Line Display usermod requires the libraries `U8g2` and `Wire`. ## Change Log 2021-10 * First public release ================================================ FILE: usermods/usermod_v2_four_line_display_ALT/usermod_v2_four_line_display.h ================================================ #include "wled.h" #undef U8X8_NO_HW_I2C // borrowed from WLEDMM: we do want I2C hardware drivers - if possible #include // from https://github.com/olikraus/u8g2/ #pragma once #ifndef FLD_ESP32_NO_THREADS #define FLD_ESP32_USE_THREADS // comment out to use 0.13.x behaviour without parallel update task - slower, but more robust. May delay other tasks like LEDs or audioreactive!! #endif #ifndef FLD_PIN_CS #define FLD_PIN_CS 15 #endif #ifdef ARDUINO_ARCH_ESP32 #ifndef FLD_PIN_DC #define FLD_PIN_DC 19 #endif #ifndef FLD_PIN_RESET #define FLD_PIN_RESET 26 #endif #else #ifndef FLD_PIN_DC #define FLD_PIN_DC 12 #endif #ifndef FLD_PIN_RESET #define FLD_PIN_RESET 16 #endif #endif #ifndef FLD_TYPE #ifndef FLD_SPI_DEFAULT #define FLD_TYPE SSD1306 #else #define FLD_TYPE SSD1306_SPI #endif #endif // When to time out to the clock or blank the screen // if SLEEP_MODE_ENABLED. #define SCREEN_TIMEOUT_MS 60*1000 // 1 min // Minimum time between redrawing screen in ms #define REFRESH_RATE_MS 1000 // Extra char (+1) for null #define LINE_BUFFER_SIZE 16+1 #define MAX_JSON_CHARS 19+1 #define MAX_MODE_LINE_SPACE 13+1 typedef enum { NONE = 0, SSD1306, // U8X8_SSD1306_128X32_UNIVISION_HW_I2C SH1106, // U8X8_SH1106_128X64_WINSTAR_HW_I2C SSD1306_64, // U8X8_SSD1306_128X64_NONAME_HW_I2C SSD1305, // U8X8_SSD1305_128X32_ADAFRUIT_HW_I2C SSD1305_64, // U8X8_SSD1305_128X64_ADAFRUIT_HW_I2C SSD1306_SPI, // U8X8_SSD1306_128X32_NONAME_HW_SPI SSD1306_SPI64, // U8X8_SSD1306_128X64_NONAME_HW_SPI SSD1309_SPI64, // U8X8_SSD1309_128X64_NONAME0_4W_HW_SPI SSD1309_64 // U8X8_SSD1309_128X64_NONAME0_HW_I2C } DisplayType; class FourLineDisplayUsermod : public Usermod { #if defined(ARDUINO_ARCH_ESP32) && defined(FLD_ESP32_USE_THREADS) public: FourLineDisplayUsermod() { if (!instance) instance = this; } static FourLineDisplayUsermod* getInstance(void) { return instance; } #endif private: static FourLineDisplayUsermod *instance; bool initDone = false; volatile bool drawing = false; volatile bool lockRedraw = false; // HW interface & configuration U8X8 *u8x8 = nullptr; // pointer to U8X8 display object #ifndef FLD_SPI_DEFAULT int8_t ioPin[3] = {-1, -1, -1}; // I2C pins: SCL, SDA uint32_t ioFrequency = 400000; // in Hz (minimum is 100000, baseline is 400000 and maximum should be 3400000) #else int8_t ioPin[3] = {FLD_PIN_CS, FLD_PIN_DC, FLD_PIN_RESET}; // custom SPI pins: CS, DC, RST uint32_t ioFrequency = 1000000; // in Hz (minimum is 500kHz, baseline is 1MHz and maximum should be 20MHz) #endif DisplayType type = FLD_TYPE; // display type bool flip = false; // flip display 180° uint8_t contrast = 10; // screen contrast uint8_t lineHeight = 1; // 1 row or 2 rows uint16_t refreshRate = REFRESH_RATE_MS; // in ms uint32_t screenTimeout = SCREEN_TIMEOUT_MS; // in ms bool sleepMode = true; // allow screen sleep? bool clockMode = false; // display clock bool showSeconds = true; // display clock with seconds bool enabled = true; bool contrastFix = false; // Next variables hold the previous known values to determine if redraw is // required. String knownSsid = apSSID; IPAddress knownIp = IPAddress(4, 3, 2, 1); 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; byte brightness100; byte fxspeed100; byte fxintensity100; bool knownnightlight = nightlightActive; bool wificonnected = interfacesInited; bool powerON = true; bool displayTurnedOff = false; unsigned long nextUpdate = 0; unsigned long lastRedraw = 0; unsigned long overlayUntil = 0; // Set to 2 or 3 to mark lines 2 or 3. Other values ignored. byte markLineNum = 255; byte markColNum = 255; // strings to reduce flash memory usage (used more than twice) static const char _name[]; static const char _enabled[]; static const char _contrast[]; static const char _refreshRate[]; static const char _screenTimeOut[]; static const char _flip[]; static const char _sleepMode[]; static const char _clockMode[]; static const char _showSeconds[]; static const char _busClkFrequency[]; static const char _contrastFix[]; // 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 // some displays need this to properly apply contrast void setVcomh(bool highContrast); void startDisplay(); /** * Wrappers for screen drawing */ void setFlipMode(uint8_t mode); void setContrast(uint8_t contrast); void drawString(uint8_t col, uint8_t row, const char *string, bool ignoreLH=false); void draw2x2String(uint8_t col, uint8_t row, const char *string); void drawGlyph(uint8_t col, uint8_t row, char glyph, const uint8_t *font, bool ignoreLH=false); void draw2x2Glyph(uint8_t col, uint8_t row, char glyph, const uint8_t *font); void draw2x2GlyphIcons(); uint8_t getCols(); void clear(); void setPowerSave(uint8_t save); void center(String &line, uint8_t width); /** * 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(); /** * Enable sleep (turn the display off) or clock mode. */ void sleepOrClock(bool enabled); public: // gets called once at boot. Do all initialization that doesn't depend on // network here void setup() override; // gets called every time WiFi is (re-)connected. Initialize own network // interfaces here void connected() override; /** * Da loop. */ void loop() override; //function to update lastredraw inline void updateRedrawTime() { lastRedraw = millis(); } /** * Redraw the screen (but only if things have changed * or if forceRedraw). */ void redraw(bool forceRedraw); void updateBrightness(); void updateSpeed(); void updateIntensity(); void drawStatusIcons(); /** * marks the position of the arrow showing * the current setting being changed * pass line and colum info */ void setMarkLine(byte newMarkLineNum, byte newMarkColNum); //Draw the arrow for the current setting being changed void drawArrow(); //Display the current effect or palette (desiredEntry) // on the appropriate line (row). void showCurrentEffectOrPalette(int inputEffPal, const char *qstring, uint8_t row); /** * 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(); /** * Allows you to show one line and a glyph as overlay for a period of time. * Clears the screen and prints. * Used in Rotary Encoder usermod. */ void overlay(const char* line1, long showHowLong, byte glyphType); /** * Allows you to show Akemi WLED logo overlay for a period of time. * Clears the screen and prints. */ void overlayLogo(long showHowLong); /** * Allows you to show two lines as overlay for a period of time. * Clears the screen and prints. * Used in Auto Save usermod */ void overlay(const char* line1, const char* line2, long showHowLong); void networkOverlay(const char* line1, long showHowLong); /** * handleButton() can be used to override default button behaviour. Returning true * will prevent button working in a default way. * Replicating button.cpp */ bool handleButton(uint8_t b); void onUpdateBegin(bool init) override; /* * 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) override; /* * 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) override; /* * 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) override; void appendConfigData() override; /* * 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) override; /* * 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 :) */ bool readFromConfig(JsonObject& root) override; /* * 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() override { return USERMOD_ID_FOUR_LINE_DISP; } }; ================================================ FILE: usermods/usermod_v2_four_line_display_ALT/usermod_v2_four_line_display_ALT.cpp ================================================ #include "usermod_v2_four_line_display.h" #include "4LD_wled_fonts.h" // // Inspired by the usermod_v2_four_line_display // // v2 usermod for using 128x32 or 128x64 i2c // OLED displays to provide a four line display // for WLED. // // Dependencies // * This Usermod works best, by far, when coupled // with RotaryEncoderUI ALT Usermod. // // Make sure to enable NTP and set your time zone in WLED Config | Time. // // 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 #ifdef ARDUINO_ARCH_ESP32 static TaskHandle_t Display_Task = nullptr; void DisplayTaskCode(void * parameter); #endif // strings to reduce flash memory usage (used more than twice) const char FourLineDisplayUsermod::_name[] PROGMEM = "4LineDisplay"; const char FourLineDisplayUsermod::_enabled[] PROGMEM = "enabled"; const char FourLineDisplayUsermod::_contrast[] PROGMEM = "contrast"; const char FourLineDisplayUsermod::_refreshRate[] PROGMEM = "refreshRate-ms"; const char FourLineDisplayUsermod::_screenTimeOut[] PROGMEM = "screenTimeOutSec"; const char FourLineDisplayUsermod::_flip[] PROGMEM = "flip"; const char FourLineDisplayUsermod::_sleepMode[] PROGMEM = "sleepMode"; const char FourLineDisplayUsermod::_clockMode[] PROGMEM = "clockMode"; const char FourLineDisplayUsermod::_showSeconds[] PROGMEM = "showSeconds"; const char FourLineDisplayUsermod::_busClkFrequency[] PROGMEM = "i2c-freq-kHz"; const char FourLineDisplayUsermod::_contrastFix[] PROGMEM = "contrastFix"; #if defined(ARDUINO_ARCH_ESP32) && defined(FLD_ESP32_USE_THREADS) FourLineDisplayUsermod *FourLineDisplayUsermod::instance = nullptr; #endif // some displays need this to properly apply contrast void FourLineDisplayUsermod::setVcomh(bool highContrast) { if (type == NONE || !enabled) return; u8x8_t *u8x8_struct = u8x8->getU8x8(); u8x8_cad_StartTransfer(u8x8_struct); u8x8_cad_SendCmd(u8x8_struct, 0x0db); //address of value u8x8_cad_SendArg(u8x8_struct, highContrast ? 0x000 : 0x040); //value 0 for fix, reboot resets default back to 64 u8x8_cad_EndTransfer(u8x8_struct); } void FourLineDisplayUsermod::startDisplay() { if (type == NONE || !enabled) return; lineHeight = u8x8->getRows() > 4 ? 2 : 1; DEBUG_PRINTLN(F("Starting display.")); u8x8->setBusClock(ioFrequency); // can be used for SPI too u8x8->begin(); setFlipMode(flip); setVcomh(contrastFix); setContrast(contrast); //Contrast setup will help to preserve OLED lifetime. In case OLED need to be brighter increase number up to 255 setPowerSave(0); //drawString(0, 0, "Loading..."); overlayLogo(3500); } /** * Wrappers for screen drawing */ void FourLineDisplayUsermod::setFlipMode(uint8_t mode) { if (type == NONE || !enabled) return; u8x8->setFlipMode(mode); } void FourLineDisplayUsermod::setContrast(uint8_t contrast) { if (type == NONE || !enabled) return; u8x8->setContrast(contrast); } void FourLineDisplayUsermod::drawString(uint8_t col, uint8_t row, const char *string, bool ignoreLH) { if (type == NONE || !enabled) return; drawing = true; u8x8->setFont(u8x8_font_chroma48medium8_r); if (!ignoreLH && lineHeight==2) u8x8->draw1x2String(col, row, string); else u8x8->drawString(col, row, string); drawing = false; } void FourLineDisplayUsermod::draw2x2String(uint8_t col, uint8_t row, const char *string) { if (type == NONE || !enabled) return; drawing = true; u8x8->setFont(u8x8_font_chroma48medium8_r); u8x8->draw2x2String(col, row, string); drawing = false; } void FourLineDisplayUsermod::drawGlyph(uint8_t col, uint8_t row, char glyph, const uint8_t *font, bool ignoreLH) { if (type == NONE || !enabled) return; drawing = true; u8x8->setFont(font); if (!ignoreLH && lineHeight==2) u8x8->draw1x2Glyph(col, row, glyph); else u8x8->drawGlyph(col, row, glyph); drawing = false; } void FourLineDisplayUsermod::draw2x2Glyph(uint8_t col, uint8_t row, char glyph, const uint8_t *font) { if (type == NONE || !enabled) return; drawing = true; u8x8->setFont(font); u8x8->draw2x2Glyph(col, row, glyph); drawing = false; } uint8_t FourLineDisplayUsermod::getCols() { if (type==NONE || !enabled) return 0; return u8x8->getCols(); } void FourLineDisplayUsermod::clear() { if (type == NONE || !enabled) return; drawing = true; u8x8->clear(); drawing = false; } void FourLineDisplayUsermod::setPowerSave(uint8_t save) { if (type == NONE || !enabled) return; u8x8->setPowerSave(save); } void FourLineDisplayUsermod::center(String &line, uint8_t width) { int len = line.length(); if (len0; i--) line = ' ' + line; for (unsigned i=line.length(); i 11) { AmPmHour -= 12; isitAM = false; } if (AmPmHour == 0) { AmPmHour = 12; } } if (knownHour != hourCurrent) { // only update date when hour changes sprintf_P(lineBuffer, PSTR("%s %2d "), monthShortStr(month(localTime)), day(localTime)); draw2x2String(2, lineHeight==1 ? 0 : lineHeight, lineBuffer); // adjust for 8 line displays, draw month and day } sprintf_P(lineBuffer,PSTR("%2d:%02d"), (useAMPM ? AmPmHour : hourCurrent), minuteCurrent); draw2x2String(2, lineHeight*2, lineBuffer); //draw hour, min. blink ":" depending on odd/even seconds if (useAMPM) drawString(12, lineHeight*2, (isitAM ? "AM" : "PM"), true); //draw am/pm if using 12 time drawStatusIcons(); //icons power, wifi, timer, etc knownMinute = minuteCurrent; knownHour = hourCurrent; } if (showSeconds && secondCurrent != lastSecond) { lastSecond = secondCurrent; draw2x2String(6, lineHeight*2, secondCurrent%2 ? " " : ":"); sprintf_P(lineBuffer, PSTR("%02d"), secondCurrent); drawString(12, lineHeight*2+1, lineBuffer, true); // even with double sized rows print seconds in 1 line } } /** * Enable sleep (turn the display off) or clock mode. */ void FourLineDisplayUsermod::sleepOrClock(bool enabled) { if (enabled) { displayTurnedOff = true; if (clockMode && ntpEnabled) { knownMinute = knownHour = 99; showTime(); } else setPowerSave(1); } else { displayTurnedOff = false; setPowerSave(0); } } // gets called once at boot. Do all initialization that doesn't depend on // network here void FourLineDisplayUsermod::setup() { bool isSPI = (type == SSD1306_SPI || type == SSD1306_SPI64 || type == SSD1309_SPI64); // check if pins are -1 and disable usermod as PinManager::allocateMultiplePins() will accept -1 as a valid pin if (isSPI) { if (spi_sclk<0 || spi_mosi<0 || ioPin[0]<0 || ioPin[1]<0 || ioPin[1]<0) { type = NONE; } else { PinManagerPinType cspins[3] = { { ioPin[0], true }, { ioPin[1], true }, { ioPin[2], true } }; if (!PinManager::allocateMultiplePins(cspins, 3, PinOwner::UM_FourLineDisplay)) { type = NONE; } } } else { if (i2c_scl<0 || i2c_sda<0) { type=NONE; } } DEBUG_PRINTLN(F("Allocating display.")); switch (type) { // U8X8 uses Wire (or Wire1 with 2ND constructor) and will use existing Wire properties (calls Wire.begin() though) case SSD1306: u8x8 = (U8X8 *) new U8X8_SSD1306_128X32_UNIVISION_HW_I2C(); break; case SH1106: u8x8 = (U8X8 *) new U8X8_SH1106_128X64_WINSTAR_HW_I2C(); break; case SSD1306_64: u8x8 = (U8X8 *) new U8X8_SSD1306_128X64_NONAME_HW_I2C(); break; case SSD1305: u8x8 = (U8X8 *) new U8X8_SSD1305_128X32_ADAFRUIT_HW_I2C(); break; case SSD1305_64: u8x8 = (U8X8 *) new U8X8_SSD1305_128X64_ADAFRUIT_HW_I2C(); break; case SSD1309_64: u8x8 = (U8X8 *) new U8X8_SSD1309_128X64_NONAME0_HW_I2C(); break; // U8X8 uses global SPI variable that is attached to VSPI bus on ESP32 case SSD1306_SPI: u8x8 = (U8X8 *) new U8X8_SSD1306_128X32_UNIVISION_4W_HW_SPI(ioPin[0], ioPin[1], ioPin[2]); break; // Pins are cs, dc, reset case SSD1306_SPI64: u8x8 = (U8X8 *) new U8X8_SSD1306_128X64_NONAME_4W_HW_SPI(ioPin[0], ioPin[1], ioPin[2]); break; // Pins are cs, dc, reset case SSD1309_SPI64: u8x8 = (U8X8 *) new U8X8_SSD1309_128X64_NONAME0_4W_HW_SPI(ioPin[0], ioPin[1], ioPin[2]); break; // Pins are cs, dc, reset // catchall default: u8x8 = (U8X8 *) new U8X8_NULL(); enabled = false; break; // catchall to create U8x8 instance } if (nullptr == u8x8) { DEBUG_PRINTLN(F("Display init failed.")); if (isSPI) { PinManager::deallocateMultiplePins((const uint8_t*)ioPin, 3, PinOwner::UM_FourLineDisplay); } type = NONE; return; } startDisplay(); onUpdateBegin(false); // create Display task initDone = true; } // gets called every time WiFi is (re-)connected. Initialize own network // interfaces here void FourLineDisplayUsermod::connected() { knownSsid = WiFi.SSID(); //apActive ? apSSID : WiFi.SSID(); //apActive ? WiFi.softAPSSID() : knownIp = Network.localIP(); //apActive ? IPAddress(4, 3, 2, 1) : Network.localIP(); networkOverlay(PSTR("NETWORK INFO"),7000); } /** * Da loop. */ void FourLineDisplayUsermod::loop() { #if !(defined(ARDUINO_ARCH_ESP32) && defined(FLD_ESP32_USE_THREADS)) if (!enabled || strip.isUpdating()) return; unsigned long now = millis(); if (now < nextUpdate) return; nextUpdate = now + ((displayTurnedOff && clockMode && showSeconds) ? 1000 : refreshRate); redraw(false); #endif } /** * Redraw the screen (but only if things have changed * or if forceRedraw). */ void FourLineDisplayUsermod::redraw(bool forceRedraw) { bool needRedraw = false; unsigned long now = millis(); if (type == NONE || !enabled) return; if (overlayUntil > 0) { if (now >= overlayUntil) { // Time to display the overlay has elapsed. overlayUntil = 0; forceRedraw = true; } else { // We are still displaying the overlay // Don't redraw. return; } } while (drawing && millis()-now < 25) delay(1); // wait if someone else is drawing if (drawing || lockRedraw) return; if (apActive && WLED_WIFI_CONFIGURED && now<15000) { knownSsid = apSSID; networkOverlay(PSTR("NETWORK INFO"),30000); return; } // Check if values which are shown on display changed from the last time. if (forceRedraw) { needRedraw = true; clear(); } else if ((bri == 0 && powerON) || (bri > 0 && !powerON)) { //trigger power icon powerON = !powerON; drawStatusIcons(); return; } else if (knownnightlight != nightlightActive) { //trigger moon icon knownnightlight = nightlightActive; drawStatusIcons(); if (knownnightlight) { String timer = PSTR("Timer On"); center(timer,LINE_BUFFER_SIZE-1); overlay(timer.c_str(), 2500, 6); } return; } else if (wificonnected != interfacesInited) { //trigger wifi icon wificonnected = interfacesInited; drawStatusIcons(); return; } else if (knownMode != effectCurrent || knownPalette != effectPalette) { if (displayTurnedOff) needRedraw = true; else { if (knownPalette != effectPalette) { showCurrentEffectOrPalette(effectPalette, JSON_palette_names, 2); knownPalette = effectPalette; } if (knownMode != effectCurrent) { showCurrentEffectOrPalette(effectCurrent, JSON_mode_names, 3); knownMode = effectCurrent; } lastRedraw = now; return; } } else if (knownBrightness != bri) { if (displayTurnedOff && nightlightActive) { knownBrightness = bri; } else if (!displayTurnedOff) { updateBrightness(); lastRedraw = now; return; } } else if (knownEffectSpeed != effectSpeed) { if (displayTurnedOff) needRedraw = true; else { updateSpeed(); lastRedraw = now; return; } } else if (knownEffectIntensity != effectIntensity) { if (displayTurnedOff) needRedraw = true; else { updateIntensity(); lastRedraw = now; return; } } if (!needRedraw) { // Nothing to change. // Turn off display after 1 minutes with no change. if (sleepMode && !displayTurnedOff && (millis() - lastRedraw > screenTimeout)) { // We will still check if there is a change in redraw() // and turn it back on if it changed. clear(); sleepOrClock(true); } else if (displayTurnedOff && ntpEnabled) { showTime(); } return; } lastRedraw = now; // Turn the display back on wakeDisplay(); // Update last known values. knownBrightness = bri; knownMode = effectCurrent; knownPalette = effectPalette; knownEffectSpeed = effectSpeed; knownEffectIntensity = effectIntensity; knownnightlight = nightlightActive; wificonnected = interfacesInited; // Do the actual drawing // First row: Icons draw2x2GlyphIcons(); drawArrow(); drawStatusIcons(); // Second row updateBrightness(); updateSpeed(); updateIntensity(); // Third row showCurrentEffectOrPalette(knownPalette, JSON_palette_names, 2); //Palette info // Fourth row showCurrentEffectOrPalette(knownMode, JSON_mode_names, 3); //Effect Mode info } void FourLineDisplayUsermod::updateBrightness() { #if defined(ARDUINO_ARCH_ESP32) && defined(FLD_ESP32_USE_THREADS) unsigned long now = millis(); while (drawing && millis()-now < 125) delay(1); // wait if someone else is drawing if (drawing || lockRedraw) return; #endif knownBrightness = bri; if (overlayUntil == 0) { lockRedraw = true; brightness100 = ((uint16_t)bri*100)/255; char lineBuffer[4]; sprintf_P(lineBuffer, PSTR("%-3d"), brightness100); drawString(1, lineHeight, lineBuffer); lockRedraw = false; } } void FourLineDisplayUsermod::updateSpeed() { #if defined(ARDUINO_ARCH_ESP32) && defined(FLD_ESP32_USE_THREADS) unsigned long now = millis(); while (drawing && millis()-now < 125) delay(1); // wait if someone else is drawing if (drawing || lockRedraw) return; #endif knownEffectSpeed = effectSpeed; if (overlayUntil == 0) { lockRedraw = true; fxspeed100 = ((uint16_t)effectSpeed*100)/255; char lineBuffer[4]; sprintf_P(lineBuffer, PSTR("%-3d"), fxspeed100); drawString(5, lineHeight, lineBuffer); lockRedraw = false; } } void FourLineDisplayUsermod::updateIntensity() { #if defined(ARDUINO_ARCH_ESP32) && defined(FLD_ESP32_USE_THREADS) unsigned long now = millis(); while (drawing && millis()-now < 125) delay(1); // wait if someone else is drawing if (drawing || lockRedraw) return; #endif knownEffectIntensity = effectIntensity; if (overlayUntil == 0) { lockRedraw = true; fxintensity100 = ((uint16_t)effectIntensity*100)/255; char lineBuffer[4]; sprintf_P(lineBuffer, PSTR("%-3d"), fxintensity100); drawString(9, lineHeight, lineBuffer); lockRedraw = false; } } void FourLineDisplayUsermod::drawStatusIcons() { #if defined(ARDUINO_ARCH_ESP32) && defined(FLD_ESP32_USE_THREADS) unsigned long now = millis(); while (drawing && millis()-now < 125) delay(1); // wait if someone else is drawing if (drawing || lockRedraw) return; #endif uint8_t col = 15; uint8_t row = 0; lockRedraw = true; drawGlyph(col, row, (wificonnected ? 20 : 0), u8x8_4LineDisplay_WLED_icons_1x1, true); // wifi icon if (lineHeight==2) { col--; } else { row++; } drawGlyph(col, row, (bri > 0 ? 9 : 0), u8x8_4LineDisplay_WLED_icons_1x1, true); // power icon if (lineHeight==2) { col--; } else { col = row = 0; } drawGlyph(col, row, (nightlightActive ? 6 : 0), u8x8_4LineDisplay_WLED_icons_1x1, true); // moon icon for nighlight mode lockRedraw = false; } /** * marks the position of the arrow showing * the current setting being changed * pass line and colum info */ void FourLineDisplayUsermod::setMarkLine(byte newMarkLineNum, byte newMarkColNum) { markLineNum = newMarkLineNum; markColNum = newMarkColNum; } //Draw the arrow for the current setting being changed void FourLineDisplayUsermod::drawArrow() { #if defined(ARDUINO_ARCH_ESP32) && defined(FLD_ESP32_USE_THREADS) unsigned long now = millis(); while (drawing && millis()-now < 125) delay(1); // wait if someone else is drawing if (drawing || lockRedraw) return; #endif lockRedraw = true; if (markColNum != 255 && markLineNum !=255) drawGlyph(markColNum, markLineNum*lineHeight, 21, u8x8_4LineDisplay_WLED_icons_1x1); lockRedraw = false; } //Display the current effect or palette (desiredEntry) // on the appropriate line (row). void FourLineDisplayUsermod::showCurrentEffectOrPalette(int inputEffPal, const char *qstring, uint8_t row) { #if defined(ARDUINO_ARCH_ESP32) && defined(FLD_ESP32_USE_THREADS) unsigned long now = millis(); while (drawing && millis()-now < 125) delay(1); // wait if someone else is drawing if (drawing || lockRedraw) return; #endif char lineBuffer[MAX_JSON_CHARS]; if (overlayUntil == 0) { lockRedraw = true; // Find the mode name in JSON unsigned printedChars = extractModeName(inputEffPal, qstring, lineBuffer, MAX_JSON_CHARS-1); if (lineBuffer[0]=='*' && lineBuffer[1]==' ') { // remove "* " from dynamic palettes for (unsigned i=2; i<=printedChars; i++) lineBuffer[i-2] = lineBuffer[i]; //include '\0' printedChars -= 2; } else if ((lineBuffer[0]==' ' && lineBuffer[1]>127)) { // remove note symbol from effect names for (unsigned i=5; i<=printedChars; i++) lineBuffer[i-5] = lineBuffer[i]; //include '\0' printedChars -= 5; } if (lineHeight == 2) { // use this code for 8 line display char smallBuffer1[MAX_MODE_LINE_SPACE]; char smallBuffer2[MAX_MODE_LINE_SPACE]; unsigned smallChars1 = 0; unsigned smallChars2 = 0; if (printedChars < MAX_MODE_LINE_SPACE) { // use big font if the text fits while (printedChars < (MAX_MODE_LINE_SPACE-1)) lineBuffer[printedChars++]=' '; lineBuffer[printedChars] = 0; drawString(1, row*lineHeight, lineBuffer); } else { // for long names divide the text into 2 lines and print them small bool spaceHit = false; for (unsigned i = 0; i < printedChars; i++) { switch (lineBuffer[i]) { case ' ': if (i > 4 && !spaceHit) { spaceHit = true; break; } if (spaceHit) smallBuffer2[smallChars2++] = lineBuffer[i]; else smallBuffer1[smallChars1++] = lineBuffer[i]; break; default: if (spaceHit) smallBuffer2[smallChars2++] = lineBuffer[i]; else smallBuffer1[smallChars1++] = lineBuffer[i]; break; } } while (smallChars1 < (MAX_MODE_LINE_SPACE-1)) smallBuffer1[smallChars1++]=' '; smallBuffer1[smallChars1] = 0; drawString(1, row*lineHeight, smallBuffer1, true); while (smallChars2 < (MAX_MODE_LINE_SPACE-1)) smallBuffer2[smallChars2++]=' '; smallBuffer2[smallChars2] = 0; drawString(1, row*lineHeight+1, smallBuffer2, true); } } else { // use this code for 4 ling displays char smallBuffer3[MAX_MODE_LINE_SPACE+1]; // uses 1x1 icon for mode/palette unsigned smallChars3 = 0; for (unsigned i = 0; i < MAX_MODE_LINE_SPACE; i++) smallBuffer3[smallChars3++] = (i >= printedChars) ? ' ' : lineBuffer[i]; smallBuffer3[smallChars3] = 0; drawString(1, row*lineHeight, smallBuffer3, true); } lockRedraw = false; } } /** * 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 FourLineDisplayUsermod::wakeDisplay() { if (type == NONE || !enabled) return false; if (displayTurnedOff) { #if defined(ARDUINO_ARCH_ESP32) && defined(FLD_ESP32_USE_THREADS) unsigned long now = millis(); while (drawing && millis()-now < 125) delay(1); // wait if someone else is drawing if (drawing || lockRedraw) return false; #endif lockRedraw = true; clear(); // Turn the display back on sleepOrClock(false); lockRedraw = false; return true; } return false; } /** * Allows you to show one line and a glyph as overlay for a period of time. * Clears the screen and prints. * Used in Rotary Encoder usermod. */ void FourLineDisplayUsermod::overlay(const char* line1, long showHowLong, byte glyphType) { #if defined(ARDUINO_ARCH_ESP32) && defined(FLD_ESP32_USE_THREADS) unsigned long now = millis(); while (drawing && millis()-now < 125) delay(1); // wait if someone else is drawing if (drawing || lockRedraw) return; #endif lockRedraw = true; // Turn the display back on if (!wakeDisplay()) clear(); // Print the overlay if (glyphType>0 && glyphType<255) { if (lineHeight == 2) drawGlyph(5, 0, glyphType, u8x8_4LineDisplay_WLED_icons_6x6, true); // use 3x3 font with draw2x2Glyph() if flash runs short and comment out 6x6 font else drawGlyph(6, 0, glyphType, u8x8_4LineDisplay_WLED_icons_3x3, true); } if (line1) { String buf = line1; center(buf, getCols()); drawString(0, (glyphType<255?3:0)*lineHeight, buf.c_str()); } overlayUntil = millis() + showHowLong; lockRedraw = false; } /** * Allows you to show Akemi WLED logo overlay for a period of time. * Clears the screen and prints. */ void FourLineDisplayUsermod::overlayLogo(long showHowLong) { #if defined(ARDUINO_ARCH_ESP32) && defined(FLD_ESP32_USE_THREADS) unsigned long now = millis(); while (drawing && millis()-now < 125) delay(1); // wait if someone else is drawing if (drawing || lockRedraw) return; #endif lockRedraw = true; // Turn the display back on if (!wakeDisplay()) clear(); // Print the overlay if (lineHeight == 2) { //add a bit of randomness switch (millis()%3) { case 0: //WLED draw2x2Glyph( 0, 2, 1, u8x8_wled_logo_2x2); draw2x2Glyph( 4, 2, 2, u8x8_wled_logo_2x2); draw2x2Glyph( 8, 2, 3, u8x8_wled_logo_2x2); draw2x2Glyph(12, 2, 4, u8x8_wled_logo_2x2); break; case 1: //WLED Akemi drawGlyph( 2, 2, 1, u8x8_wled_logo_akemi_4x4, true); drawGlyph( 6, 2, 2, u8x8_wled_logo_akemi_4x4, true); drawGlyph(10, 2, 3, u8x8_wled_logo_akemi_4x4, true); break; case 2: //Akemi //draw2x2Glyph( 5, 0, 12, u8x8_4LineDisplay_WLED_icons_3x3); // use this if flash runs short and comment out 6x6 font drawGlyph( 5, 0, 12, u8x8_4LineDisplay_WLED_icons_6x6, true); drawString(6, 6, "WLED"); break; } } else { switch (millis()%3) { case 0: //WLED draw2x2Glyph( 0, 0, 1, u8x8_wled_logo_2x2); draw2x2Glyph( 4, 0, 2, u8x8_wled_logo_2x2); draw2x2Glyph( 8, 0, 3, u8x8_wled_logo_2x2); draw2x2Glyph(12, 0, 4, u8x8_wled_logo_2x2); break; case 1: //WLED Akemi drawGlyph( 2, 0, 1, u8x8_wled_logo_akemi_4x4); drawGlyph( 6, 0, 2, u8x8_wled_logo_akemi_4x4); drawGlyph(10, 0, 3, u8x8_wled_logo_akemi_4x4); break; case 2: //Akemi //drawGlyph( 6, 0, 12, u8x8_4LineDisplay_WLED_icons_4x4); // a bit nicer, but uses extra 1.5k flash draw2x2Glyph( 6, 0, 12, u8x8_4LineDisplay_WLED_icons_2x2); break; } } overlayUntil = millis() + showHowLong; lockRedraw = false; } /** * Allows you to show two lines as overlay for a period of time. * Clears the screen and prints. * Used in Auto Save usermod */ void FourLineDisplayUsermod::overlay(const char* line1, const char* line2, long showHowLong) { #if defined(ARDUINO_ARCH_ESP32) && defined(FLD_ESP32_USE_THREADS) unsigned long now = millis(); while (drawing && millis()-now < 125) delay(1); // wait if someone else is drawing if (drawing || lockRedraw) return; #endif lockRedraw = true; // Turn the display back on if (!wakeDisplay()) clear(); // Print the overlay if (line1) { String buf = line1; center(buf, getCols()); drawString(0, 1*lineHeight, buf.c_str()); } if (line2) { String buf = line2; center(buf, getCols()); drawString(0, 2*lineHeight, buf.c_str()); } overlayUntil = millis() + showHowLong; lockRedraw = false; } void FourLineDisplayUsermod::networkOverlay(const char* line1, long showHowLong) { #if defined(ARDUINO_ARCH_ESP32) && defined(FLD_ESP32_USE_THREADS) unsigned long now = millis(); while (drawing && millis()-now < 125) delay(1); // wait if someone else is drawing if (drawing || lockRedraw) return; #endif lockRedraw = true; String line; // Turn the display back on if (!wakeDisplay()) clear(); // Print the overlay if (line1) { line = line1; center(line, getCols()); drawString(0, 0, line.c_str()); } // Second row with Wifi name line = knownSsid.substring(0, getCols() > 1 ? getCols() - 2 : 0); if (line.length() < getCols()) center(line, getCols()); drawString(0, lineHeight, line.c_str()); // Print `~` char to indicate that SSID is longer, than our display if (knownSsid.length() > getCols()) { drawString(getCols() - 1, 0, "~"); } // Third row with IP and Password in AP Mode line = knownIp.toString(); center(line, getCols()); drawString(0, lineHeight*2, line.c_str()); line = ""; if (apActive) { line = apPass; } else if (strcmp(serverDescription, "WLED") != 0) { line = serverDescription; } center(line, getCols()); drawString(0, lineHeight*3, line.c_str()); overlayUntil = millis() + showHowLong; lockRedraw = false; } /** * handleButton() can be used to override default button behaviour. Returning true * will prevent button working in a default way. * Replicating button.cpp */ bool FourLineDisplayUsermod::handleButton(uint8_t b) { yield(); if (!enabled || b // button 0 only || buttons[b].type == BTN_TYPE_SWITCH || buttons[b].type == BTN_TYPE_NONE || buttons[b].type == BTN_TYPE_RESERVED || buttons[b].type == BTN_TYPE_PIR_SENSOR || buttons[b].type == BTN_TYPE_ANALOG || buttons[b].type == BTN_TYPE_ANALOG_INVERTED) { return false; } unsigned long now = millis(); static bool buttonPressedBefore = false; static bool buttonLongPressed = false; static unsigned long buttonPressedTime = 0; static unsigned long buttonWaitTime = 0; bool handled = false; //momentary button logic if (isButtonPressed(b)) { //pressed if (!buttonPressedBefore) buttonPressedTime = now; buttonPressedBefore = true; if (now - buttonPressedTime > 600) { //long press //TODO: handleButton() handles button 0 without preset in a different way for double click //so we need to override with same behaviour //DEBUG_PRINTLN(F("4LD action.")); //if (!buttonLongPressed) longPressAction(0); buttonLongPressed = true; return false; } } else if (!isButtonPressed(b) && buttonPressedBefore) { //released long dur = now - buttonPressedTime; if (dur < 50) { buttonPressedBefore = false; return true; } //too short "press", debounce bool doublePress = buttonWaitTime; //did we have short press before? buttonWaitTime = 0; if (!buttonLongPressed) { //short press // if this is second release within 350ms it is a double press (buttonWaitTime!=0) //TODO: handleButton() handles button 0 without preset in a different way for double click if (doublePress) { networkOverlay(PSTR("NETWORK INFO"),7000); handled = true; } else { buttonWaitTime = now; } } buttonPressedBefore = false; buttonLongPressed = false; } // if 350ms elapsed since last press/release it is a short press if (buttonWaitTime && now - buttonWaitTime > 350 && !buttonPressedBefore) { buttonWaitTime = 0; //TODO: handleButton() handles button 0 without preset in a different way for double click //so we need to override with same behaviour //shortPressAction(0); //handled = false; } return handled; } #ifndef ARDUINO_RUNNING_CORE #if CONFIG_FREERTOS_UNICORE #define ARDUINO_RUNNING_CORE 0 #else #define ARDUINO_RUNNING_CORE 1 #endif #endif void FourLineDisplayUsermod::onUpdateBegin(bool init) { #if defined(ARDUINO_ARCH_ESP32) && defined(FLD_ESP32_USE_THREADS) if (init && Display_Task) { vTaskSuspend(Display_Task); // update is about to begin, disable task to prevent crash } else { // update has failed or create task requested if (Display_Task) vTaskResume(Display_Task); else xTaskCreatePinnedToCore( [](void * par) { // Function to implement the task // see https://www.freertos.org/vtaskdelayuntil.html const TickType_t xFrequency = REFRESH_RATE_MS * portTICK_PERIOD_MS / 2; TickType_t xLastWakeTime = xTaskGetTickCount(); for(;;) { delay(1); // DO NOT DELETE THIS LINE! It is needed to give the IDLE(0) task enough time and to keep the watchdog happy. // taskYIELD(), yield(), vTaskDelay() and esp_task_wdt_feed() didn't seem to work. vTaskDelayUntil(&xLastWakeTime, xFrequency); // release CPU, by doing nothing for REFRESH_RATE_MS millis FourLineDisplayUsermod::getInstance()->redraw(false); } }, "4LD", // Name of the task 3072, // Stack size in words NULL, // Task input parameter 1, // Priority of the task (not idle) &Display_Task, // Task handle ARDUINO_RUNNING_CORE ); } #endif } /* * 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 FourLineDisplayUsermod::addToJsonInfo(JsonObject& root) { //JsonObject user = root["u"]; //if (user.isNull()) user = root.createNestedObject("u"); //JsonArray data = user.createNestedArray(F("4LineDisplay")); //data.add(F("Loaded.")); //} /* * 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 FourLineDisplayUsermod::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 FourLineDisplayUsermod::readFromJsonState(JsonObject& root) { // if (!initDone) return; // prevent crash on boot applyPreset() //} void FourLineDisplayUsermod::appendConfigData() { oappend(F("dd=addDropdown('4LineDisplay','type');")); oappend(F("addOption(dd,'None',0);")); oappend(F("addOption(dd,'SSD1306',1);")); oappend(F("addOption(dd,'SH1106',2);")); oappend(F("addOption(dd,'SSD1306 128x64',3);")); oappend(F("addOption(dd,'SSD1305',4);")); oappend(F("addOption(dd,'SSD1305 128x64',5);")); oappend(F("addOption(dd,'SSD1309 128x64',9);")); oappend(F("addOption(dd,'SSD1306 SPI',6);")); oappend(F("addOption(dd,'SSD1306 SPI 128x64',7);")); oappend(F("addOption(dd,'SSD1309 SPI 128x64',8);")); oappend(F("addInfo('4LineDisplay:type',1,'
Change may require reboot','');")); oappend(F("addInfo('4LineDisplay:pin[]',0,'','SPI CS');")); oappend(F("addInfo('4LineDisplay:pin[]',1,'','SPI DC');")); oappend(F("addInfo('4LineDisplay:pin[]',2,'','SPI RST');")); } /* * 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 FourLineDisplayUsermod::addToConfig(JsonObject& root) { JsonObject top = root.createNestedObject(FPSTR(_name)); top[FPSTR(_enabled)] = enabled; top["type"] = type; JsonArray io_pin = top.createNestedArray("pin"); for (int i=0; i<3; i++) io_pin.add(ioPin[i]); top[FPSTR(_flip)] = (bool) flip; top[FPSTR(_contrast)] = contrast; top[FPSTR(_contrastFix)] = (bool) contrastFix; #ifndef ARDUINO_ARCH_ESP32 top[FPSTR(_refreshRate)] = refreshRate; #endif top[FPSTR(_screenTimeOut)] = screenTimeout/1000; top[FPSTR(_sleepMode)] = (bool) sleepMode; top[FPSTR(_clockMode)] = (bool) clockMode; top[FPSTR(_showSeconds)] = (bool) showSeconds; top[FPSTR(_busClkFrequency)] = ioFrequency/1000; DEBUG_PRINTLN(F("4 Line Display config saved.")); } /* * 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 :) */ bool FourLineDisplayUsermod::readFromConfig(JsonObject& root) { bool needsRedraw = false; DisplayType newType = type; int8_t oldPin[3]; for (unsigned i=0; i<3; i++) oldPin[i] = ioPin[i]; JsonObject top = root[FPSTR(_name)]; if (top.isNull()) { DEBUG_PRINT(FPSTR(_name)); DEBUG_PRINTLN(F(": No config found. (Using defaults.)")); return false; } enabled = top[FPSTR(_enabled)] | enabled; newType = top["type"] | newType; for (unsigned i=0; i<3; i++) ioPin[i] = top["pin"][i] | ioPin[i]; flip = top[FPSTR(_flip)] | flip; contrast = top[FPSTR(_contrast)] | contrast; #ifndef ARDUINO_ARCH_ESP32 refreshRate = top[FPSTR(_refreshRate)] | refreshRate; refreshRate = min(5000, max(250, (int)refreshRate)); #endif screenTimeout = (top[FPSTR(_screenTimeOut)] | screenTimeout/1000) * 1000; sleepMode = top[FPSTR(_sleepMode)] | sleepMode; clockMode = top[FPSTR(_clockMode)] | clockMode; showSeconds = top[FPSTR(_showSeconds)] | showSeconds; contrastFix = top[FPSTR(_contrastFix)] | contrastFix; if (newType == SSD1306_SPI || newType == SSD1306_SPI64) ioFrequency = min(20000, max(500, (int)(top[FPSTR(_busClkFrequency)] | ioFrequency/1000))) * 1000; // limit frequency else ioFrequency = min(3400, max(100, (int)(top[FPSTR(_busClkFrequency)] | ioFrequency/1000))) * 1000; // limit frequency DEBUG_PRINT(FPSTR(_name)); if (!initDone) { // first run: reading from cfg.json type = newType; DEBUG_PRINTLN(F(" config loaded.")); } else { DEBUG_PRINTLN(F(" config (re)loaded.")); // changing parameters from settings page bool pinsChanged = false; for (unsigned i=0; i<3; i++) if (ioPin[i] != oldPin[i]) { pinsChanged = true; break; } if (pinsChanged || type!=newType) { bool isSPI = (type == SSD1306_SPI || type == SSD1306_SPI64 || type == SSD1309_SPI64); bool newSPI = (newType == SSD1306_SPI || newType == SSD1306_SPI64 || newType == SSD1309_SPI64); if (isSPI) { if (pinsChanged || !newSPI) PinManager::deallocateMultiplePins((const uint8_t*)oldPin, 3, PinOwner::UM_FourLineDisplay); if (!newSPI) { // was SPI but is no longer SPI if (i2c_scl<0 || i2c_sda<0) { newType=NONE; } } else { // still SPI but pins changed PinManagerPinType cspins[3] = { { ioPin[0], true }, { ioPin[1], true }, { ioPin[2], true } }; if (ioPin[0]<0 || ioPin[1]<0 || ioPin[1]<0) { newType=NONE; } else if (!PinManager::allocateMultiplePins(cspins, 3, PinOwner::UM_FourLineDisplay)) { newType=NONE; } } } else if (newSPI) { // was I2C but is now SPI if (spi_sclk<0 || spi_mosi<0) { newType=NONE; } else { PinManagerPinType pins[3] = { { ioPin[0], true }, { ioPin[1], true }, { ioPin[2], true } }; if (ioPin[0]<0 || ioPin[1]<0 || ioPin[1]<0) { newType=NONE; } else if (!PinManager::allocateMultiplePins(pins, 3, PinOwner::UM_FourLineDisplay)) { newType=NONE; } } } else { // just I2C type changed } type = newType; switch (type) { case SSD1306: u8x8_Setup(u8x8->getU8x8(), u8x8_d_ssd1306_128x32_univision, u8x8_cad_ssd13xx_fast_i2c, u8x8_byte_arduino_hw_i2c, u8x8_gpio_and_delay_arduino); u8x8_SetPin_HW_I2C(u8x8->getU8x8(), U8X8_PIN_NONE, U8X8_PIN_NONE, U8X8_PIN_NONE); break; case SH1106: u8x8_Setup(u8x8->getU8x8(), u8x8_d_sh1106_128x64_winstar, u8x8_cad_ssd13xx_fast_i2c, u8x8_byte_arduino_hw_i2c, u8x8_gpio_and_delay_arduino); u8x8_SetPin_HW_I2C(u8x8->getU8x8(), U8X8_PIN_NONE, U8X8_PIN_NONE, U8X8_PIN_NONE); break; case SSD1306_64: u8x8_Setup(u8x8->getU8x8(), u8x8_d_ssd1306_128x64_noname, u8x8_cad_ssd13xx_fast_i2c, u8x8_byte_arduino_hw_i2c, u8x8_gpio_and_delay_arduino); u8x8_SetPin_HW_I2C(u8x8->getU8x8(), U8X8_PIN_NONE, U8X8_PIN_NONE, U8X8_PIN_NONE); break; case SSD1305: u8x8_Setup(u8x8->getU8x8(), u8x8_d_ssd1305_128x32_adafruit, u8x8_cad_ssd13xx_fast_i2c, u8x8_byte_arduino_hw_i2c, u8x8_gpio_and_delay_arduino); u8x8_SetPin_HW_I2C(u8x8->getU8x8(), U8X8_PIN_NONE, U8X8_PIN_NONE, U8X8_PIN_NONE); break; case SSD1305_64: u8x8_Setup(u8x8->getU8x8(), u8x8_d_ssd1305_128x64_adafruit, u8x8_cad_ssd13xx_fast_i2c, u8x8_byte_arduino_hw_i2c, u8x8_gpio_and_delay_arduino); u8x8_SetPin_HW_I2C(u8x8->getU8x8(), U8X8_PIN_NONE, U8X8_PIN_NONE, U8X8_PIN_NONE); break; case SSD1309_64: u8x8_Setup(u8x8->getU8x8(), u8x8_d_ssd1309_128x64_noname0, u8x8_cad_ssd13xx_fast_i2c, u8x8_byte_arduino_hw_i2c, u8x8_gpio_and_delay_arduino); u8x8_SetPin_HW_I2C(u8x8->getU8x8(), U8X8_PIN_NONE, U8X8_PIN_NONE, U8X8_PIN_NONE); break; case SSD1306_SPI: u8x8_Setup(u8x8->getU8x8(), u8x8_d_ssd1306_128x32_univision, u8x8_cad_001, u8x8_byte_arduino_hw_spi, u8x8_gpio_and_delay_arduino); u8x8_SetPin_4Wire_HW_SPI(u8x8->getU8x8(), ioPin[0], ioPin[1], ioPin[2]); // Pins are cs, dc, reset break; case SSD1306_SPI64: u8x8_Setup(u8x8->getU8x8(), u8x8_d_ssd1306_128x64_noname, u8x8_cad_001, u8x8_byte_arduino_hw_spi, u8x8_gpio_and_delay_arduino); u8x8_SetPin_4Wire_HW_SPI(u8x8->getU8x8(), ioPin[0], ioPin[1], ioPin[2]); // Pins are cs, dc, reset break; case SSD1309_SPI64: u8x8_Setup(u8x8->getU8x8(), u8x8_d_ssd1309_128x64_noname0, u8x8_cad_001, u8x8_byte_arduino_hw_spi, u8x8_gpio_and_delay_arduino); u8x8_SetPin_4Wire_HW_SPI(u8x8->getU8x8(), ioPin[0], ioPin[1], ioPin[2]); // Pins are cs, dc, reset default: u8x8_Setup(u8x8->getU8x8(), u8x8_d_null_cb, u8x8_cad_empty, u8x8_byte_empty, u8x8_dummy_cb); enabled = false; break; } startDisplay(); needsRedraw |= true; } else { u8x8->setBusClock(ioFrequency); // can be used for SPI too setVcomh(contrastFix); setContrast(contrast); setFlipMode(flip); } knownHour = 99; if (needsRedraw && !wakeDisplay()) redraw(true); else overlayLogo(3500); } // use "return !top["newestParameter"].isNull();" when updating Usermod with new features return !top[FPSTR(_contrastFix)].isNull(); } static FourLineDisplayUsermod usermod_v2_four_line_display_alt; REGISTER_USERMOD(usermod_v2_four_line_display_alt); ================================================ FILE: usermods/usermod_v2_klipper_percentage/library.json ================================================ { "name": "usermod_v2_klipper_percentage", "build": { "libArchive": false } } ================================================ FILE: usermods/usermod_v2_klipper_percentage/readme.md ================================================ # Klipper Percentage Usermod This usermod polls the Klipper API every 10s for the progressvalue. The leds are then filled with a solid color according to that progress percentage. the solid color is the secondary color of the segment. A corresponding curl command would be: ``` curl --location --request GET 'http://[]/printer/objects/query?virtual_sdcard=progress' ``` ## Usage Compile the source with the buildflag `-D USERMOD_KLIPPER_PERCENTAGE` added. You can also use the WLBD bot in the Discord by simply extending an existing build environment: ``` [env:esp32klipper] extends = env:esp32dev build_flags = ${common.build_flags_esp32} -D USERMOD_KLIPPER_PERCENTAGE ``` ## Settings ### Enabled: Checkbox to enable or disable the overlay ### Klipper IP: IP address of your Klipper instance you want to poll. ESP has to be restarted after change ### Direction : 0 = normal 1 = reversed 2 = center ----- Author: Sören Willrodt Discord: Sören#5281 ================================================ FILE: usermods/usermod_v2_klipper_percentage/usermod_v2_klipper_percentage.cpp ================================================ #include "wled.h" class klipper_percentage : public Usermod { private: unsigned long lastTime = 0; String ip = F("0.0.0.0"); WiFiClient wifiClient; char errorMessage[100] = ""; int printPercent = 0; int direction = 0; // 0 for along the strip, 1 for reversed direction static const char _name[]; static const char _enabled[]; bool enabled = false; void httpGet(WiFiClient &client, char *errorMessage) { // https://arduinojson.org/v6/example/http-client/ // is this the most compact way to do http get and put it in arduinojson object??? // would like async response ... ??? client.setTimeout(10000); if (!client.connect(ip.c_str(), 80)) { strcat(errorMessage, PSTR("Connection failed")); } else { // Send HTTP request client.println(F("GET /printer/objects/query?virtual_sdcard=progress HTTP/1.0")); client.print(F("Host: ")); client.println(ip); client.println(F("Connection: close")); if (client.println() == 0) { strcat(errorMessage, PSTR("Failed to send request")); } else { // Check HTTP status char status[32] = {0}; client.readBytesUntil('\r', status, sizeof(status)); if (strcmp_P(status, PSTR("HTTP/1.1 200 OK")) != 0) { strcat(errorMessage, PSTR("Unexpected response: ")); strcat(errorMessage, status); } else { // Skip HTTP headers char endOfHeaders[] = "\r\n\r\n"; if (!client.find(endOfHeaders)) { strcat(errorMessage, PSTR("Invalid response")); } } } } } public: void setup() { } void connected() { } void loop() { if (enabled) { if (WLED_CONNECTED) { if (millis() - lastTime > 10000) { httpGet(wifiClient, errorMessage); if (strcmp(errorMessage, "") == 0) { PSRAMDynamicJsonDocument klipperDoc(4096); // in practice about 2673 DeserializationError error = deserializeJson(klipperDoc, wifiClient); if (error) { strcat(errorMessage, PSTR("deserializeJson() failed: ")); strcat(errorMessage, error.c_str()); } printPercent = (int)(klipperDoc[F("result")][F("status")][F("virtual_sdcard")][F("progress")].as() * 100); DEBUG_PRINT(F("Percent: ")); DEBUG_PRINTLN((int)(klipperDoc[F("result")][F("status")][F("virtual_sdcard")][F("progress")].as() * 100)); DEBUG_PRINT(F("LEDs: ")); DEBUG_PRINTLN(direction == 2 ? (strip.getLengthTotal() / 2) * printPercent / 100 : strip.getLengthTotal() * printPercent / 100); } else { DEBUG_PRINTLN(errorMessage); DEBUG_PRINTLN(ip); } lastTime = millis(); } } } } void addToConfig(JsonObject &root) { JsonObject top = root.createNestedObject(F("Klipper Printing Percentage")); top[F("Enabled")] = enabled; top[F("Klipper IP")] = ip; top[F("Direction")] = direction; } bool readFromConfig(JsonObject &root) { // default settings values could be set here (or below using the 3-argument getJsonValue()) instead of in the class definition or constructor // setting them inside readFromConfig() is slightly more robust, handling the rare but plausible use case of single value being missing after boot (e.g. if the cfg.json was manually edited and a value was removed) JsonObject top = root[F("Klipper Printing Percentage")]; bool configComplete = !top.isNull(); configComplete &= getJsonValue(top[F("Klipper IP")], ip); configComplete &= getJsonValue(top[F("Enabled")], enabled); configComplete &= getJsonValue(top[F("Direction")], direction); return configComplete; } /* * 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) { JsonObject user = root["u"]; if (user.isNull()) user = root.createNestedObject("u"); JsonArray infoArr = user.createNestedArray(FPSTR(_name)); String uiDomString = F(""); infoArr.add(uiDomString); } void addToJsonState(JsonObject &root) { JsonObject usermod = root[FPSTR(_name)]; if (usermod.isNull()) { usermod = root.createNestedObject(FPSTR(_name)); } usermod["on"] = enabled; } void readFromJsonState(JsonObject &root) { JsonObject usermod = root[FPSTR(_name)]; if (!usermod.isNull()) { if (usermod[FPSTR(_enabled)].is()) { enabled = usermod[FPSTR(_enabled)].as(); } } } /* * handleOverlayDraw() is called just before every show() (LED strip update frame) after effects have set the colors. * Use this to blank out some LEDs or set them to a different color regardless of the set effect mode. * Commonly used for custom clocks (Cronixie, 7 segment) */ void handleOverlayDraw() { if (enabled) { if (direction == 0) // normal { for (int i = 0; i < strip.getLengthTotal() * printPercent / 100; i++) { strip.setPixelColor(i, strip.getSegment(0).colors[1]); } } else if (direction == 1) // reversed { for (int i = 0; i < strip.getLengthTotal() * printPercent / 100; i++) { strip.setPixelColor(strip.getLengthTotal() - i, strip.getSegment(0).colors[1]); } } else if (direction == 2) // center { for (int i = 0; i < (strip.getLengthTotal() / 2) * printPercent / 100; i++) { strip.setPixelColor((strip.getLengthTotal() / 2) + i, strip.getSegment(0).colors[1]); strip.setPixelColor((strip.getLengthTotal() / 2) - i, strip.getSegment(0).colors[1]); } } else { direction = 0; } } } /* * 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_KLIPPER; } }; const char klipper_percentage::_name[] PROGMEM = "Klipper_Percentage"; const char klipper_percentage::_enabled[] PROGMEM = "enabled"; static klipper_percentage usermod_v2_klipper_percentage; REGISTER_USERMOD(usermod_v2_klipper_percentage); ================================================ FILE: usermods/usermod_v2_ping_pong_clock/library.json ================================================ { "name": "usermod_v2_ping_pong_clock", "build": { "libArchive": false } } ================================================ FILE: usermods/usermod_v2_ping_pong_clock/readme.md ================================================ # Ping Pong LED Clock Contains a modification to use WLED in combination with the Ping Pong Ball LED Clock as built in [Instructables](https://www.instructables.com/Ping-Pong-Ball-LED-Clock/). ## Installation To install this Usermod, you instruct PlatformIO to compile the Project with the USERMOD_PING_PONG_CLOCK flag. WLED then automatically provides you with various settings on the Usermod Page. Note: Depending on the size of your clock, you may have to update the led indices for the individual numbers and the base indices. ================================================ FILE: usermods/usermod_v2_ping_pong_clock/usermod_v2_ping_pong_clock.cpp ================================================ #include "wled.h" class PingPongClockUsermod : public Usermod { private: // Private class members. You can declare variables and functions only accessible to your usermod here unsigned long lastTime = 0; bool colonOn = true; // ---- Variables modified by settings below ----- // set your config variables to their boot default value (this can also be done in readFromConfig() or a constructor if you prefer) bool pingPongClockEnabled = true; int colorR = 0xFF; int colorG = 0xFF; int colorB = 0xFF; // ---- Variables for correct LED numbering below, edit only if your clock is built different ---- int baseH = 43; // Address for the one place of the hours int baseHH = 7; // Address for the tens place of the hours int baseM = 133; // Address for the one place of the minutes int baseMM = 97; // Address for the tens place of the minutes int colon1 = 79; // Address for the first colon led int colon2 = 80; // Address for the second colon led // Matrix for the illumination of the numbers // Note: These only define the increments of the base address. e.g. to define the second Minute you have to add the baseMM to every led position const int numbers[10][10] = { { 0, 1, 4, 6, 13, 15, 18, 19, -1, -1 }, // 0: null { 13, 14, 15, 18, 19, -1, -1, -1, -1, -1 }, // 1: eins { 0, 4, 5, 6, 13, 14, 15, 19, -1, -1 }, // 2: zwei { 4, 5, 6, 13, 14, 15, 18, 19, -1, -1 }, // 3: drei { 1, 4, 5, 14, 15, 18, 19, -1, -1, -1 }, // 4: vier { 1, 4, 5, 6, 13, 14, 15, 18, -1, -1 }, // 5: fünf { 0, 5, 6, 10, 13, 14, 15, 18, -1, -1 }, // 6: sechs { 4, 6, 9, 13, 14, 19, -1, -1, -1, -1 }, // 7: sieben { 0, 1, 4, 5, 6, 13, 14, 15, 18, 19 }, // 8: acht { 1, 4, 5, 6, 9, 13, 14, 19, -1, -1 } // 9: neun }; public: void setup() { } void loop() { if (millis() - lastTime > 1000) { lastTime = millis(); colonOn = !colonOn; } } void addToJsonInfo(JsonObject& root) { JsonObject user = root["u"]; if (user.isNull()) user = root.createNestedObject("u"); JsonArray lightArr = user.createNestedArray("Uhrzeit-Anzeige"); //name lightArr.add(pingPongClockEnabled ? "aktiv" : "inaktiv"); //value lightArr.add(""); //unit } void addToConfig(JsonObject &root) { JsonObject top = root.createNestedObject("Ping Pong Clock"); top["enabled"] = pingPongClockEnabled; top["colorR"] = colorR; top["colorG"] = colorG; top["colorB"] = colorB; } bool readFromConfig(JsonObject &root) { JsonObject top = root["Ping Pong Clock"]; bool configComplete = !top.isNull(); configComplete &= getJsonValue(top["enabled"], pingPongClockEnabled); configComplete &= getJsonValue(top["colorR"], colorR); configComplete &= getJsonValue(top["colorG"], colorG); configComplete &= getJsonValue(top["colorB"], colorB); return configComplete; } void drawNumber(int base, int number) { for(int i = 0; i < 10; i++) { if(numbers[number][i] > -1) strip.setPixelColor(numbers[number][i] + base, RGBW32(colorR, colorG, colorB, 0)); } } void handleOverlayDraw() { if(pingPongClockEnabled){ if(colonOn) { strip.setPixelColor(colon1, RGBW32(colorR, colorG, colorB, 0)); strip.setPixelColor(colon2, RGBW32(colorR, colorG, colorB, 0)); } drawNumber(baseHH, (hour(localTime) / 10) % 10); drawNumber(baseH, hour(localTime) % 10); drawNumber(baseM, (minute(localTime) / 10) % 10); drawNumber(baseMM, minute(localTime) % 10); } } uint16_t getId() { return USERMOD_ID_PING_PONG_CLOCK; } }; static PingPongClockUsermod usermod_v2_ping_pong_clock; REGISTER_USERMOD(usermod_v2_ping_pong_clock); ================================================ FILE: usermods/usermod_v2_rotary_encoder_ui_ALT/library.json ================================================ { "name": "rotary_encoder_ui_ALT", "build": { "libArchive": false, "extraScript": "setup_deps.py" } } ================================================ FILE: usermods/usermod_v2_rotary_encoder_ui_ALT/platformio_override.sample.ini ================================================ [platformio] default_envs = esp32dev_re [env:esp32dev_re] extends = env:esp32dev_V4 custom_usermods = ${env:esp32dev_V4.custom_usermods} rotary_encoder_ui_ALT build_flags = ${env:esp32dev_V4.build_flags} -D USERMOD_ROTARY_ENCODER_GPIO=INPUT -D ENCODER_DT_PIN=21 -D ENCODER_CLK_PIN=23 -D ENCODER_SW_PIN=0 ================================================ FILE: usermods/usermod_v2_rotary_encoder_ui_ALT/readme.md ================================================ # Rotary Encoder UI Usermod ALT This usermod supports the UI of the `usermod_v2_rotary_encoder_ui_ALT`. ## Functionalities Press the encoder to cycle through the options: * Brightness * Speed * Intensity * Palette * Effect * Main Color (only if display is used) * Saturation (only if display is used) Press and hold the encoder to display Network Info. If AP is active, it will display the AP, SSID and Password Also shows if the timer is enabled. [See the pair of usermods in action](https://www.youtube.com/watch?v=ulZnBt9z3TI) ## Installation Copy the example `platformio_override.sample.ini` to the root directory of your particular build. ### Define Your Options * `ENCODER_DT_PIN` - defaults to 18 * `ENCODER_CLK_PIN` - defaults to 5 * `ENCODER_SW_PIN` - defaults to 19 * `USERMOD_ROTARY_ENCODER_GPIO` - GPIO functionality: `INPUT_PULLUP` to use internal pull-up `INPUT` to use pull-up on the PCB ### PlatformIO requirements No special requirements. ## Change Log 2021-10 * First public release ================================================ FILE: usermods/usermod_v2_rotary_encoder_ui_ALT/setup_deps.py ================================================ from platformio.package.meta import PackageSpec Import('env') libs = [PackageSpec(lib).name for lib in env.GetProjectOption("lib_deps",[])] # Check for partner usermod # Allow both "usermod_v2" and unqualified syntax if any(mod in ("four_line_display_ALT", "usermod_v2_four_line_display_ALT") for mod in libs): env.Append(CPPDEFINES=[("USERMOD_FOUR_LINE_DISPLAY")]) ================================================ FILE: usermods/usermod_v2_rotary_encoder_ui_ALT/usermod_v2_rotary_encoder_ui_ALT.cpp ================================================ #include "wled.h" // // Inspired by the original v2 usermods // * usermod_v2_rotary_encoder_ui // // v2 usermod that provides a rotary encoder-based UI. // // This usermod allows you to control: // // * Brightness // * Selected Effect // * Effect Speed // * Effect Intensity // * Palette // // Change between modes by pressing a button. // // Dependencies // * This Usermod works best coupled with // FourLineDisplayUsermod. // // If FourLineDisplayUsermod is used the folowing options are also enabled // // * main color // * saturation of main color // * display network (long press buttion) // #ifdef USERMOD_FOUR_LINE_DISPLAY #include "usermod_v2_four_line_display.h" #endif #ifdef USERMOD_MODE_SORT #error "Usermod Mode Sort is no longer required. Remove -D USERMOD_MODE_SORT from platformio.ini" #endif #ifndef ENCODER_DT_PIN #define ENCODER_DT_PIN 18 #endif #ifndef ENCODER_CLK_PIN #define ENCODER_CLK_PIN 5 #endif #ifndef ENCODER_SW_PIN #define ENCODER_SW_PIN 19 #endif #ifndef ENCODER_MAX_DELAY_MS // max delay between polling encoder pins #define ENCODER_MAX_DELAY_MS 8 // 8 milliseconds => max 120 change impulses in 1 second, for full turn of a 30/30 encoder (4 changes per segment, 30 segments for one turn) #endif #ifndef USERMOD_USE_PCF8574 #undef USE_PCF8574 #define USE_PCF8574 false #else #undef USE_PCF8574 #define USE_PCF8574 true #endif #ifndef PCF8574_ADDRESS #define PCF8574_ADDRESS 0x20 // some may start at 0x38 #endif #ifndef PCF8574_INT_PIN #define PCF8574_INT_PIN -1 // GPIO connected to INT pin on PCF8574 #endif // The last UI state, remove color and saturation option if display not active (too many options) #ifdef USERMOD_FOUR_LINE_DISPLAY #define LAST_UI_STATE 11 #else #define LAST_UI_STATE 4 #endif // Number of modes at the start of the list to not sort #define MODE_SORT_SKIP_COUNT 1 // Which list is being sorted static const char **listBeingSorted; /** * Modes and palettes are stored as strings that * end in a quote character. Compare two of them. * We are comparing directly within either * JSON_mode_names or JSON_palette_names. */ static int re_qstringCmp(const void *ap, const void *bp) { const char *a = listBeingSorted[*((byte *)ap)]; const char *b = listBeingSorted[*((byte *)bp)]; int i = 0; do { char aVal = pgm_read_byte_near(a + i); if (aVal >= 97 && aVal <= 122) { // Lowercase aVal -= 32; } char bVal = pgm_read_byte_near(b + i); if (bVal >= 97 && bVal <= 122) { // Lowercase bVal -= 32; } // Really we shouldn't ever get to '\0' if (aVal == '"' || bVal == '"' || aVal == '\0' || bVal == '\0') { // We're done. one is a substring of the other // or something happenend and the quote didn't stop us. if (aVal == bVal) { // Same value, probably shouldn't happen // with this dataset return 0; } else if (aVal == '"' || aVal == '\0') { return -1; } else { return 1; } } if (aVal == bVal) { // Same characters. Move to the next. i++; continue; } // We're done if (aVal < bVal) { return -1; } else { return 1; } } while (true); // We shouldn't get here. return 0; } static volatile uint8_t pcfPortData = 0; // port expander port state static volatile uint8_t addrPcf8574 = PCF8574_ADDRESS; // has to be accessible in ISR // Interrupt routine to read I2C rotary state // if we are to use PCF8574 port expander we will need to rely on interrupts as polling I2C every 2ms // is a waste of resources and causes 4LD to fail. // in such case rely on ISR to read pin values and store them into static variable static void IRAM_ATTR i2cReadingISR() { Wire.requestFrom(addrPcf8574, 1U); if (Wire.available()) { pcfPortData = Wire.read(); } } class RotaryEncoderUIUsermod : public Usermod { private: const int8_t fadeAmount; // Amount to change every step (brightness) unsigned long loopTime; unsigned long buttonPressedTime; unsigned long buttonWaitTime; bool buttonPressedBefore; bool buttonLongPressed; int8_t pinA; // DT from encoder int8_t pinB; // CLK from encoder int8_t pinC; // SW from encoder unsigned char select_state; // 0: brightness, 1: effect, 2: effect speed, ... uint16_t currentHue1; // default boot color byte currentSat1; uint8_t currentCCT; #ifdef USERMOD_FOUR_LINE_DISPLAY FourLineDisplayUsermod *display; #else void* display; #endif // Pointers the start of the mode names within JSON_mode_names const char **modes_qstrings; // Array of mode indexes in alphabetical order. byte *modes_alpha_indexes; // Pointers the start of the palette names within JSON_palette_names const char **palettes_qstrings; // Array of palette indexes in alphabetical order. byte *palettes_alpha_indexes; struct { // reduce memory footprint bool Enc_A : 1; bool Enc_B : 1; bool Enc_A_prev : 1; }; bool currentEffectAndPaletteInitialized; uint8_t effectCurrentIndex; uint8_t effectPaletteIndex; uint8_t knownMode; uint8_t knownPalette; byte presetHigh; byte presetLow; bool applyToAll; bool initDone; bool enabled; bool usePcf8574; int8_t pinIRQ; // strings to reduce flash memory usage (used more than twice) static const char _name[]; static const char _enabled[]; static const char _DT_pin[]; static const char _CLK_pin[]; static const char _SW_pin[]; static const char _presetHigh[]; static const char _presetLow[]; static const char _applyToAll[]; static const char _pcf8574[]; static const char _pcfAddress[]; static const char _pcfINTpin[]; /** * readPin() - read rotary encoder pin value */ byte readPin(uint8_t pin); /** * Sort the modes and palettes to the index arrays * modes_alpha_indexes and palettes_alpha_indexes. */ void sortModesAndPalettes(); byte *re_initIndexArray(int numModes); /** * Return an array of mode or palette names from the JSON string. * They don't end in '\0', they end in '"'. */ const char **re_findModeStrings(const char json[], int numModes); /** * Sort either the modes or the palettes using quicksort. */ void re_sortModes(const char **modeNames, byte *indexes, int count, int numSkip); public: RotaryEncoderUIUsermod() : fadeAmount(5) , buttonPressedTime(0) , buttonWaitTime(0) , buttonPressedBefore(false) , buttonLongPressed(false) , pinA(ENCODER_DT_PIN) , pinB(ENCODER_CLK_PIN) , pinC(ENCODER_SW_PIN) , select_state(0) , currentHue1(16) , currentSat1(255) , currentCCT(128) , display(nullptr) , modes_qstrings(nullptr) , modes_alpha_indexes(nullptr) , palettes_qstrings(nullptr) , palettes_alpha_indexes(nullptr) , currentEffectAndPaletteInitialized(false) , effectCurrentIndex(0) , effectPaletteIndex(0) , knownMode(0) , knownPalette(0) , presetHigh(0) , presetLow(0) , applyToAll(true) , initDone(false) , enabled(true) , usePcf8574(USE_PCF8574) , pinIRQ(PCF8574_INT_PIN) {} /* * 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() override { return USERMOD_ID_ROTARY_ENC_UI; } /** * Enable/Disable the usermod */ inline void enable(bool enable) { if (!(pinA<0 || pinB<0 || pinC<0)) enabled = enable; } /** * Get usermod enabled/disabled state */ inline bool isEnabled() { return enabled; } /** * 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() override; /** * connected() is called every time the WiFi is (re)connected * Use it to initialize network interfaces */ //void connected(); /** * loop() is called continuously. Here you can check for events, read sensors, etc. */ void loop() override; #ifndef WLED_DISABLE_MQTT //bool onMqttMessage(char* topic, char* payload) override; //void onMqttConnect(bool sessionPresent) override; #endif /** * handleButton() can be used to override default button behaviour. Returning true * will prevent button working in a default way. * Replicating button.cpp */ //bool handleButton(uint8_t b) override; /** * addToJsonInfo() can be used to add custom entries to the /json/info part of the JSON API. */ //void addToJsonInfo(JsonObject &root) override; /** * 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) override; /** * 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) override; /** * provide the changeable values */ void addToConfig(JsonObject &root) override; void appendConfigData() override; /** * restore the changeable values * readFromConfig() is called before setup() to populate properties from values stored in cfg.json * * The function should return true if configuration was successfully loaded or false if there was no configuration. */ bool readFromConfig(JsonObject &root) override; // custom methods void displayNetworkInfo(); void findCurrentEffectAndPalette(); bool changeState(const char *stateName, byte markedLine, byte markedCol, byte glyph); void lampUdated(); void changeBrightness(bool increase); void changeEffect(bool increase); void changeEffectSpeed(bool increase); void changeEffectIntensity(bool increase); void changeCustom(uint8_t par, bool increase); void changePalette(bool increase); void changeHue(bool increase); void changeSat(bool increase); void changePreset(bool increase); void changeCCT(bool increase); }; /** * readPin() - read rotary encoder pin value */ byte RotaryEncoderUIUsermod::readPin(uint8_t pin) { if (usePcf8574) { if (pin >= 100) pin -= 100; // PCF I/O ports return (pcfPortData>>pin) & 1; } else { return digitalRead(pin); } } /** * Sort the modes and palettes to the index arrays * modes_alpha_indexes and palettes_alpha_indexes. */ void RotaryEncoderUIUsermod::sortModesAndPalettes() { DEBUG_PRINT(F("Sorting modes: ")); DEBUG_PRINTLN(strip.getModeCount()); //modes_qstrings = re_findModeStrings(JSON_mode_names, strip.getModeCount()); modes_qstrings = strip.getModeDataSrc(); modes_alpha_indexes = re_initIndexArray(strip.getModeCount()); re_sortModes(modes_qstrings, modes_alpha_indexes, strip.getModeCount(), MODE_SORT_SKIP_COUNT); DEBUG_PRINT(F("Sorting palettes: ")); DEBUG_PRINT(getPaletteCount()); DEBUG_PRINT('/'); DEBUG_PRINTLN(customPalettes.size()); palettes_qstrings = re_findModeStrings(JSON_palette_names, getPaletteCount()); // allocates memory for all palette names palettes_alpha_indexes = re_initIndexArray(getPaletteCount()); // allocates memory for all palette indexes if (customPalettes.size()) { for (int i=0; i= 0 && PinManager::allocatePin(pinIRQ, false, PinOwner::UM_RotaryEncoderUI)) { pinMode(pinIRQ, INPUT_PULLUP); attachInterrupt(pinIRQ, i2cReadingISR, FALLING); // RISING, FALLING, CHANGE, ONLOW, ONHIGH DEBUG_PRINTLN(F("Interrupt attached.")); } else { DEBUG_PRINTLN(F("Unable to allocate interrupt pin, disabling.")); pinIRQ = -1; enabled = false; return; } } } else { PinManagerPinType pins[3] = { { pinA, false }, { pinB, false }, { pinC, false } }; if (pinA<0 || pinB<0 || !PinManager::allocateMultiplePins(pins, 3, PinOwner::UM_RotaryEncoderUI)) { pinA = pinB = pinC = -1; enabled = false; return; } #ifndef USERMOD_ROTARY_ENCODER_GPIO #define USERMOD_ROTARY_ENCODER_GPIO INPUT_PULLUP #endif pinMode(pinA, USERMOD_ROTARY_ENCODER_GPIO); pinMode(pinB, USERMOD_ROTARY_ENCODER_GPIO); if (pinC>=0) pinMode(pinC, USERMOD_ROTARY_ENCODER_GPIO); } loopTime = millis(); currentCCT = (approximateKelvinFromRGB(RGBW32(colPri[0], colPri[1], colPri[2], colPri[3])) - 1900) >> 5; if (!initDone) sortModesAndPalettes(); #ifdef USERMOD_FOUR_LINE_DISPLAY // This Usermod uses FourLineDisplayUsermod for the best experience. // But it's optional. But you want it. display = (FourLineDisplayUsermod*) UsermodManager::lookup(USERMOD_ID_FOUR_LINE_DISP); if (display != nullptr) { display->setMarkLine(1, 0); } #endif initDone = true; Enc_A = readPin(pinA); // Read encoder pins Enc_B = readPin(pinB); Enc_A_prev = Enc_A; } /* * 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 RotaryEncoderUIUsermod::loop() { if (!enabled) return; unsigned long currentTime = millis(); // get the current elapsed time if (strip.isUpdating() && ((currentTime - loopTime) < ENCODER_MAX_DELAY_MS)) return; // be nice, but not too nice // Initialize effectCurrentIndex and effectPaletteIndex to // current state. We do it here as (at least) effectCurrent // is not yet initialized when setup is called. if (!currentEffectAndPaletteInitialized) { findCurrentEffectAndPalette(); } if (modes_alpha_indexes[effectCurrentIndex] != effectCurrent || palettes_alpha_indexes[effectPaletteIndex] != effectPalette) { DEBUG_PRINTLN(F("Current mode or palette changed.")); currentEffectAndPaletteInitialized = false; } if (currentTime >= (loopTime + 2)) // 2ms since last check of encoder = 500Hz { bool buttonPressed = !readPin(pinC); //0=pressed, 1=released if (buttonPressed) { if (!buttonPressedBefore) buttonPressedTime = currentTime; buttonPressedBefore = true; if (currentTime-buttonPressedTime > 3000) { if (!buttonLongPressed) displayNetworkInfo(); //long press for network info buttonLongPressed = true; } } else if (!buttonPressed && buttonPressedBefore) { bool doublePress = buttonWaitTime; buttonWaitTime = 0; if (!buttonLongPressed) { if (doublePress) { toggleOnOff(); lampUdated(); } else { buttonWaitTime = currentTime; } } buttonLongPressed = false; buttonPressedBefore = false; } if (buttonWaitTime && currentTime-buttonWaitTime>350 && !buttonPressedBefore) { //same speed as in button.cpp buttonWaitTime = 0; char newState = select_state + 1; bool changedState = false; char lineBuffer[64]; do { // find new state switch (newState) { case 0: strcpy_P(lineBuffer, PSTR("Brightness")); changedState = true; break; case 1: if (!extractModeSlider(effectCurrent, 0, lineBuffer, 63)) newState++; else changedState = true; break; // speed case 2: if (!extractModeSlider(effectCurrent, 1, lineBuffer, 63)) newState++; else changedState = true; break; // intensity case 3: strcpy_P(lineBuffer, PSTR("Color Palette")); changedState = true; break; case 4: strcpy_P(lineBuffer, PSTR("Effect")); changedState = true; break; case 5: strcpy_P(lineBuffer, PSTR("Main Color")); changedState = true; break; case 6: strcpy_P(lineBuffer, PSTR("Saturation")); changedState = true; break; case 7: if (!(strip.getSegment(applyToAll ? strip.getFirstSelectedSegId() : strip.getMainSegmentId()).getLightCapabilities() & 0x04)) newState++; else { strcpy_P(lineBuffer, PSTR("CCT")); changedState = true; } break; case 8: if (presetHigh==0 || presetLow == 0) newState++; else { strcpy_P(lineBuffer, PSTR("Preset")); changedState = true; } break; case 9: case 10: case 11: if (!extractModeSlider(effectCurrent, newState-7, lineBuffer, 63)) newState++; else changedState = true; break; // custom } if (newState > LAST_UI_STATE) newState = 0; } while (!changedState); if (display != nullptr) { switch (newState) { case 0: changedState = changeState(lineBuffer, 1, 0, 1); break; //1 = sun case 1: changedState = changeState(lineBuffer, 1, 4, 2); break; //2 = skip forward case 2: changedState = changeState(lineBuffer, 1, 8, 3); break; //3 = fire case 3: changedState = changeState(lineBuffer, 2, 0, 4); break; //4 = custom palette case 4: changedState = changeState(lineBuffer, 3, 0, 5); break; //5 = puzzle piece case 5: changedState = changeState(lineBuffer, 255, 255, 7); break; //7 = brush case 6: changedState = changeState(lineBuffer, 255, 255, 8); break; //8 = contrast case 7: changedState = changeState(lineBuffer, 255, 255, 10); break; //10 = star case 8: changedState = changeState(lineBuffer, 255, 255, 11); break; //11 = heart case 9: changedState = changeState(lineBuffer, 255, 255, 10); break; //10 = star case 10: changedState = changeState(lineBuffer, 255, 255, 10); break; //10 = star case 11: changedState = changeState(lineBuffer, 255, 255, 10); break; //10 = star } } if (changedState) select_state = newState; } Enc_A = readPin(pinA); // Read encoder pins Enc_B = readPin(pinB); if ((Enc_A) && (!Enc_A_prev)) { // A has gone from high to low if (Enc_B == LOW) //changes to LOW so that then encoder registers a change at the very end of a pulse { // B is high so clockwise switch(select_state) { case 0: changeBrightness(true); break; case 1: changeEffectSpeed(true); break; case 2: changeEffectIntensity(true); break; case 3: changePalette(true); break; case 4: changeEffect(true); break; case 5: changeHue(true); break; case 6: changeSat(true); break; case 7: changeCCT(true); break; case 8: changePreset(true); break; case 9: changeCustom(1,true); break; case 10: changeCustom(2,true); break; case 11: changeCustom(3,true); break; } } else if (Enc_B == HIGH) { // B is low so counter-clockwise switch(select_state) { case 0: changeBrightness(false); break; case 1: changeEffectSpeed(false); break; case 2: changeEffectIntensity(false); break; case 3: changePalette(false); break; case 4: changeEffect(false); break; case 5: changeHue(false); break; case 6: changeSat(false); break; case 7: changeCCT(false); break; case 8: changePreset(false); break; case 9: changeCustom(1,false); break; case 10: changeCustom(2,false); break; case 11: changeCustom(3,false); break; } } } Enc_A_prev = Enc_A; // Store value of A for next time loopTime = currentTime; // Updates loopTime } } void RotaryEncoderUIUsermod::displayNetworkInfo() { #ifdef USERMOD_FOUR_LINE_DISPLAY display->networkOverlay(PSTR("NETWORK INFO"), 10000); #endif } void RotaryEncoderUIUsermod::findCurrentEffectAndPalette() { DEBUG_PRINTLN(F("Finding current mode and palette.")); currentEffectAndPaletteInitialized = true; effectCurrentIndex = 0; for (int i = 0; i < strip.getModeCount(); i++) { if (modes_alpha_indexes[i] == effectCurrent) { effectCurrentIndex = i; DEBUG_PRINTLN(F("Found current mode.")); break; } } effectPaletteIndex = 0; DEBUG_PRINTLN(effectPalette); for (unsigned i = 0; i < getPaletteCount()+customPalettes.size(); i++) { if (palettes_alpha_indexes[i] == effectPalette) { effectPaletteIndex = i; DEBUG_PRINTLN(F("Found palette.")); break; } } } bool RotaryEncoderUIUsermod::changeState(const char *stateName, byte markedLine, byte markedCol, byte glyph) { #ifdef USERMOD_FOUR_LINE_DISPLAY if (display != nullptr) { if (display->wakeDisplay()) { // Throw away wake up input display->redraw(true); return false; } display->overlay(stateName, 750, glyph); display->setMarkLine(markedLine, markedCol); } #endif return true; } void RotaryEncoderUIUsermod::lampUdated() { //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 //setValuesFromFirstSelectedSeg(); //to make transition work on main segment (should no longer be required) stateUpdated(CALL_MODE_BUTTON); updateInterfaces(CALL_MODE_BUTTON); } void RotaryEncoderUIUsermod::changeBrightness(bool increase) { #ifdef USERMOD_FOUR_LINE_DISPLAY if (display && display->wakeDisplay()) { display->redraw(true); // Throw away wake up input return; } display->updateRedrawTime(); #endif //bri = max(min((increase ? bri+fadeAmount : bri-fadeAmount), 255), 0); if (bri < 40) bri = max(min((increase ? bri+fadeAmount/2 : bri-fadeAmount/2), 255), 0); // slower steps when brightness < 16% else bri = max(min((increase ? bri+fadeAmount : bri-fadeAmount), 255), 0); lampUdated(); #ifdef USERMOD_FOUR_LINE_DISPLAY display->updateBrightness(); #endif } void RotaryEncoderUIUsermod::changeEffect(bool increase) { #ifdef USERMOD_FOUR_LINE_DISPLAY if (display && display->wakeDisplay()) { display->redraw(true); // Throw away wake up input return; } display->updateRedrawTime(); #endif effectCurrentIndex = max(min((increase ? effectCurrentIndex+1 : effectCurrentIndex-1), strip.getModeCount()-1), 0); effectCurrent = modes_alpha_indexes[effectCurrentIndex]; stateChanged = true; if (applyToAll) { for (unsigned i=0; ishowCurrentEffectOrPalette(effectCurrent, JSON_mode_names, 3); #endif } void RotaryEncoderUIUsermod::changeEffectSpeed(bool increase) { #ifdef USERMOD_FOUR_LINE_DISPLAY if (display && display->wakeDisplay()) { display->redraw(true); // Throw away wake up input return; } display->updateRedrawTime(); #endif effectSpeed = max(min((increase ? effectSpeed+fadeAmount : effectSpeed-fadeAmount), 255), 0); stateChanged = true; if (applyToAll) { for (unsigned i=0; iupdateSpeed(); #endif } void RotaryEncoderUIUsermod::changeEffectIntensity(bool increase) { #ifdef USERMOD_FOUR_LINE_DISPLAY if (display && display->wakeDisplay()) { display->redraw(true); // Throw away wake up input return; } display->updateRedrawTime(); #endif effectIntensity = max(min((increase ? effectIntensity+fadeAmount : effectIntensity-fadeAmount), 255), 0); stateChanged = true; if (applyToAll) { for (unsigned i=0; iupdateIntensity(); #endif } void RotaryEncoderUIUsermod::changeCustom(uint8_t par, bool increase) { uint8_t val = 0; #ifdef USERMOD_FOUR_LINE_DISPLAY if (display && display->wakeDisplay()) { display->redraw(true); // Throw away wake up input return; } display->updateRedrawTime(); #endif stateChanged = true; if (applyToAll) { uint8_t id = strip.getFirstSelectedSegId(); Segment& sid = strip.getSegment(id); switch (par) { case 3: val = sid.custom3 = max(min((increase ? sid.custom3+fadeAmount : sid.custom3-fadeAmount), 255), 0); break; case 2: val = sid.custom2 = max(min((increase ? sid.custom2+fadeAmount : sid.custom2-fadeAmount), 255), 0); break; default: val = sid.custom1 = max(min((increase ? sid.custom1+fadeAmount : sid.custom1-fadeAmount), 255), 0); break; } for (unsigned i=0; ioverlay(lineBuffer, 500, 10); // use star #endif } void RotaryEncoderUIUsermod::changePalette(bool increase) { #ifdef USERMOD_FOUR_LINE_DISPLAY if (display && display->wakeDisplay()) { display->redraw(true); // Throw away wake up input return; } display->updateRedrawTime(); #endif effectPaletteIndex = max(min((unsigned)(increase ? effectPaletteIndex+1 : effectPaletteIndex-1), getPaletteCount()+customPalettes.size()-1), 0U); effectPalette = palettes_alpha_indexes[effectPaletteIndex]; stateChanged = true; if (applyToAll) { for (unsigned i=0; ishowCurrentEffectOrPalette(effectPalette, JSON_palette_names, 2); #endif } void RotaryEncoderUIUsermod::changeHue(bool increase){ #ifdef USERMOD_FOUR_LINE_DISPLAY if (display && display->wakeDisplay()) { display->redraw(true); // Throw away wake up input return; } display->updateRedrawTime(); #endif currentHue1 = max(min((increase ? currentHue1+fadeAmount : currentHue1-fadeAmount), 255), 0); colorHStoRGB(currentHue1*256, currentSat1, colPri); stateChanged = true; if (applyToAll) { for (unsigned i=0; ioverlay(lineBuffer, 500, 7); // use brush #endif } void RotaryEncoderUIUsermod::changeSat(bool increase){ #ifdef USERMOD_FOUR_LINE_DISPLAY if (display && display->wakeDisplay()) { display->redraw(true); // Throw away wake up input return; } display->updateRedrawTime(); #endif currentSat1 = max(min((increase ? currentSat1+fadeAmount : currentSat1-fadeAmount), 255), 0); colorHStoRGB(currentHue1*256, currentSat1, colPri); if (applyToAll) { for (unsigned i=0; ioverlay(lineBuffer, 500, 8); // use contrast #endif } void RotaryEncoderUIUsermod::changePreset(bool increase) { #ifdef USERMOD_FOUR_LINE_DISPLAY if (display && display->wakeDisplay()) { display->redraw(true); // Throw away wake up input return; } display->updateRedrawTime(); #endif if (presetHigh && presetLow && presetHigh > presetLow) { StaticJsonDocument<64> root; char str[64]; sprintf_P(str, PSTR("%d~%d~%s"), presetLow, presetHigh, increase?"":"-"); root["ps"] = str; deserializeState(root.as(), CALL_MODE_BUTTON_PRESET); /* String apireq = F("win&PL=~"); if (!increase) apireq += '-'; apireq += F("&P1="); apireq += presetLow; apireq += F("&P2="); apireq += presetHigh; handleSet(nullptr, apireq, false); */ lampUdated(); #ifdef USERMOD_FOUR_LINE_DISPLAY sprintf(str, "%d", currentPreset); display->overlay(str, 500, 11); // use heart #endif } } void RotaryEncoderUIUsermod::changeCCT(bool increase){ #ifdef USERMOD_FOUR_LINE_DISPLAY if (display && display->wakeDisplay()) { display->redraw(true); // Throw away wake up input return; } display->updateRedrawTime(); #endif currentCCT = max(min((increase ? currentCCT+fadeAmount : currentCCT-fadeAmount), 255), 0); // if (applyToAll) { for (unsigned i=0; ioverlay(lineBuffer, 500, 10); // use star #endif } /* * 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 RotaryEncoderUIUsermod::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 RotaryEncoderUIUsermod::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 RotaryEncoderUIUsermod::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!")); } */ /** * addToConfig() (called from set.cpp) stores persistent properties to cfg.json */ void RotaryEncoderUIUsermod::addToConfig(JsonObject &root) { // we add JSON object: {"Rotary-Encoder":{"DT-pin":12,"CLK-pin":14,"SW-pin":13}} JsonObject top = root.createNestedObject(FPSTR(_name)); // usermodname top[FPSTR(_enabled)] = enabled; top[FPSTR(_DT_pin)] = pinA; top[FPSTR(_CLK_pin)] = pinB; top[FPSTR(_SW_pin)] = pinC; top[FPSTR(_presetLow)] = presetLow; top[FPSTR(_presetHigh)] = presetHigh; top[FPSTR(_applyToAll)] = applyToAll; top[FPSTR(_pcf8574)] = usePcf8574; top[FPSTR(_pcfAddress)] = addrPcf8574; top[FPSTR(_pcfINTpin)] = pinIRQ; DEBUG_PRINTLN(F("Rotary Encoder config saved.")); } void RotaryEncoderUIUsermod::appendConfigData() { oappend(F("addInfo('Rotary-Encoder:PCF8574-address',1,'(not hex!)');")); oappend(F("d.extra.push({'Rotary-Encoder':{pin:[['P0',100],['P1',101],['P2',102],['P3',103],['P4',104],['P5',105],['P6',106],['P7',107]]}});")); } /** * readFromConfig() is called before setup() to populate properties from values stored in cfg.json * * The function should return true if configuration was successfully loaded or false if there was no configuration. */ bool RotaryEncoderUIUsermod::readFromConfig(JsonObject &root) { // we look for JSON object: {"Rotary-Encoder":{"DT-pin":12,"CLK-pin":14,"SW-pin":13}} JsonObject top = root[FPSTR(_name)]; if (top.isNull()) { DEBUG_PRINT(FPSTR(_name)); DEBUG_PRINTLN(F(": No config found. (Using defaults.)")); return false; } int8_t newDTpin = top[FPSTR(_DT_pin)] | pinA; int8_t newCLKpin = top[FPSTR(_CLK_pin)] | pinB; int8_t newSWpin = top[FPSTR(_SW_pin)] | pinC; int8_t newIRQpin = top[FPSTR(_pcfINTpin)] | pinIRQ; bool oldPcf8574 = usePcf8574; presetHigh = top[FPSTR(_presetHigh)] | presetHigh; presetLow = top[FPSTR(_presetLow)] | presetLow; presetHigh = MIN(250,MAX(0,presetHigh)); presetLow = MIN(250,MAX(0,presetLow)); enabled = top[FPSTR(_enabled)] | enabled; applyToAll = top[FPSTR(_applyToAll)] | applyToAll; usePcf8574 = top[FPSTR(_pcf8574)] | usePcf8574; addrPcf8574 = top[FPSTR(_pcfAddress)] | addrPcf8574; DEBUG_PRINT(FPSTR(_name)); if (!initDone) { // first run: reading from cfg.json pinA = newDTpin; pinB = newCLKpin; pinC = newSWpin; DEBUG_PRINTLN(F(" config loaded.")); } else { DEBUG_PRINTLN(F(" config (re)loaded.")); // changing parameters from settings page if (pinA!=newDTpin || pinB!=newCLKpin || pinC!=newSWpin || pinIRQ!=newIRQpin) { if (oldPcf8574) { if (pinIRQ >= 0) { detachInterrupt(pinIRQ); PinManager::deallocatePin(pinIRQ, PinOwner::UM_RotaryEncoderUI); DEBUG_PRINTLN(F("Deallocated old IRQ pin.")); } pinIRQ = newIRQpin<100 ? newIRQpin : -1; // ignore PCF8574 pins } else { PinManager::deallocatePin(pinA, PinOwner::UM_RotaryEncoderUI); PinManager::deallocatePin(pinB, PinOwner::UM_RotaryEncoderUI); PinManager::deallocatePin(pinC, PinOwner::UM_RotaryEncoderUI); DEBUG_PRINTLN(F("Deallocated old pins.")); } pinA = newDTpin; pinB = newCLKpin; pinC = newSWpin; if (pinA<0 || pinB<0 || pinC<0) { enabled = false; return true; } setup(); } } // use "return !top["newestParameter"].isNull();" when updating Usermod with new features return !top[FPSTR(_pcfINTpin)].isNull(); } // strings to reduce flash memory usage (used more than twice) const char RotaryEncoderUIUsermod::_name[] PROGMEM = "Rotary-Encoder"; const char RotaryEncoderUIUsermod::_enabled[] PROGMEM = "enabled"; const char RotaryEncoderUIUsermod::_DT_pin[] PROGMEM = "DT-pin"; const char RotaryEncoderUIUsermod::_CLK_pin[] PROGMEM = "CLK-pin"; const char RotaryEncoderUIUsermod::_SW_pin[] PROGMEM = "SW-pin"; const char RotaryEncoderUIUsermod::_presetHigh[] PROGMEM = "preset-high"; const char RotaryEncoderUIUsermod::_presetLow[] PROGMEM = "preset-low"; const char RotaryEncoderUIUsermod::_applyToAll[] PROGMEM = "apply-2-all-seg"; const char RotaryEncoderUIUsermod::_pcf8574[] PROGMEM = "use-PCF8574"; const char RotaryEncoderUIUsermod::_pcfAddress[] PROGMEM = "PCF8574-address"; const char RotaryEncoderUIUsermod::_pcfINTpin[] PROGMEM = "PCF8574-INT-pin"; static RotaryEncoderUIUsermod usermod_v2_rotary_encoder_ui_alt; REGISTER_USERMOD(usermod_v2_rotary_encoder_ui_alt); ================================================ FILE: usermods/usermod_v2_word_clock/library.json ================================================ { "name": "usermod_v2_word_clock", "build": { "libArchive": false } } ================================================ FILE: usermods/usermod_v2_word_clock/readme.md ================================================ # Word Clock Usermod V2 This usermod drives an 11x10 pixel matrix wordclock with WLED. There are 4 additional dots for the minutes. The visualisation is described by 4 masks with LED numbers (single dots for minutes, minutes, hours and "clock"). The index of the LEDs in the masks always starts at 0, even if the ledOffset is not 0. There are 3 parameters that control behavior: active: enable/disable usermod diplayItIs: enable/disable display of "Es ist" on the clock ledOffset: number of LEDs before the wordclock LEDs ## Update for alternative wiring pattern Based on this fantastic work I added an alternative wiring pattern. The original used a long wire to connect DO to DI, from one line to the next line. I wired my clock in meander style. So the first LED in the second line is on the right. With this method, every other line was inverted and showed the wrong letter. I added a switch in usermod called "meander wiring?" to enable/disable the alternate wiring pattern. ## Installation Copy and update the example `platformio_override.ini.sample` from the Rotary Encoder UI usermod 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_WORDCLOCK` - define this to have this usermod included wled00\usermods_list.cpp ### PlatformIO requirements No special requirements. ## Change Log 2022/08/18 added meander wiring pattern. 2022/03/30 initial commit ================================================ FILE: usermods/usermod_v2_word_clock/usermod_v2_word_clock.cpp ================================================ #include "wled.h" /* * Usermods allow you to add own functionality to WLED more easily * See: https://github.com/wled-dev/WLED/wiki/Add-own-functionality * * This usermod can be used to drive a wordclock with a 11x10 pixel matrix with WLED. There are also 4 additional dots for the minutes. * The visualisation is described in 4 mask with LED numbers (single dots for minutes, minutes, hours and "clock/Uhr"). * There are 2 parameters to change the behaviour: * * active: enable/disable usermod * diplayItIs: enable/disable display of "Es ist" on the clock. */ class WordClockUsermod : public Usermod { private: unsigned long lastTime = 0; int lastTimeMinutes = -1; // set your config variables to their boot default value (this can also be done in readFromConfig() or a constructor if you prefer) bool usermodActive = false; bool displayItIs = false; int ledOffset = 100; bool meander = false; bool nord = false; // defines for mask sizes #define maskSizeLeds 114 #define maskSizeMinutes 12 #define maskSizeMinutesMea 12 #define maskSizeHours 6 #define maskSizeHoursMea 6 #define maskSizeItIs 5 #define maskSizeMinuteDots 4 // "minute" masks // Normal wiring const int maskMinutes[14][maskSizeMinutes] = { {107, 108, 109, -1, -1, -1, -1, -1, -1, -1, -1, -1}, // 0 - 00 { 7, 8, 9, 10, 40, 41, 42, 43, -1, -1, -1, -1}, // 1 - 05 fünf nach { 11, 12, 13, 14, 40, 41, 42, 43, -1, -1, -1, -1}, // 2 - 10 zehn nach { 26, 27, 28, 29, 30, 31, 32, -1, -1, -1, -1, -1}, // 3 - 15 viertel { 15, 16, 17, 18, 19, 20, 21, 40, 41, 42, 43, -1}, // 4 - 20 zwanzig nach { 7, 8, 9, 10, 33, 34, 35, 44, 45, 46, 47, -1}, // 5 - 25 fünf vor halb { 44, 45, 46, 47, -1, -1, -1, -1, -1, -1, -1, -1}, // 6 - 30 halb { 7, 8, 9, 10, 40, 41, 42, 43, 44, 45, 46, 47}, // 7 - 35 fünf nach halb { 15, 16, 17, 18, 19, 20, 21, 33, 34, 35, -1, -1}, // 8 - 40 zwanzig vor { 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, -1}, // 9 - 45 dreiviertel { 11, 12, 13, 14, 33, 34, 35, -1, -1, -1, -1, -1}, // 10 - 50 zehn vor { 7, 8, 9, 10, 33, 34, 35, -1, -1, -1, -1, -1}, // 11 - 55 fünf vor { 26, 27, 28, 29, 30, 31, 32, 40, 41, 42, 43, -1}, // 12 - 15 alternative viertel nach { 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, -1, -1} // 13 - 45 alternative viertel vor }; // Meander wiring const int maskMinutesMea[14][maskSizeMinutesMea] = { { 99, 100, 101, -1, -1, -1, -1, -1, -1, -1, -1, -1}, // 0 - 00 { 7, 8, 9, 10, 33, 34, 35, 36, -1, -1, -1, -1}, // 1 - 05 fünf nach { 18, 19, 20, 21, 33, 34, 35, 36, -1, -1, -1, -1}, // 2 - 10 zehn nach { 26, 27, 28, 29, 30, 31, 32, -1, -1, -1, -1, -1}, // 3 - 15 viertel { 11, 12, 13, 14, 15, 16, 17, 33, 34, 35, 36, -1}, // 4 - 20 zwanzig nach { 7, 8, 9, 10, 41, 42, 43, 44, 45, 46, 47, -1}, // 5 - 25 fünf vor halb { 44, 45, 46, 47, -1, -1, -1, -1, -1, -1, -1, -1}, // 6 - 30 halb { 7, 8, 9, 10, 33, 34, 35, 36, 44, 45, 46, 47}, // 7 - 35 fünf nach halb { 11, 12, 13, 14, 15, 16, 17, 41, 42, 43, -1, -1}, // 8 - 40 zwanzig vor { 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, -1}, // 9 - 45 dreiviertel { 18, 19, 20, 21, 41, 42, 43, -1, -1, -1, -1, -1}, // 10 - 50 zehn vor { 7, 8, 9, 10, 41, 42, 43, -1, -1, -1, -1, -1}, // 11 - 55 fünf vor { 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, -1}, // 12 - 15 alternative viertel nach { 26, 27, 28, 29, 30, 31, 32, 41, 42, 43, -1, -1} // 13 - 45 alternative viertel vor }; // hour masks // Normal wiring const int maskHours[13][maskSizeHours] = { { 55, 56, 57, -1, -1, -1}, // 01: ein { 55, 56, 57, 58, -1, -1}, // 01: eins { 62, 63, 64, 65, -1, -1}, // 02: zwei { 66, 67, 68, 69, -1, -1}, // 03: drei { 73, 74, 75, 76, -1, -1}, // 04: vier { 51, 52, 53, 54, -1, -1}, // 05: fünf { 77, 78, 79, 80, 81, -1}, // 06: sechs { 88, 89, 90, 91, 92, 93}, // 07: sieben { 84, 85, 86, 87, -1, -1}, // 08: acht {102, 103, 104, 105, -1, -1}, // 09: neun { 99, 100, 101, 102, -1, -1}, // 10: zehn { 49, 50, 51, -1, -1, -1}, // 11: elf { 94, 95, 96, 97, 98, -1} // 12: zwölf and 00: null }; // Meander wiring const int maskHoursMea[13][maskSizeHoursMea] = { { 63, 64, 65, -1, -1, -1}, // 01: ein { 62, 63, 64, 65, -1, -1}, // 01: eins { 55, 56, 57, 58, -1, -1}, // 02: zwei { 66, 67, 68, 69, -1, -1}, // 03: drei { 73, 74, 75, 76, -1, -1}, // 04: vier { 51, 52, 53, 54, -1, -1}, // 05: fünf { 83, 84, 85, 86, 87, -1}, // 06: sechs { 88, 89, 90, 91, 92, 93}, // 07: sieben { 77, 78, 79, 80, -1, -1}, // 08: acht {103, 104, 105, 106, -1, -1}, // 09: neun {106, 107, 108, 109, -1, -1}, // 10: zehn { 49, 50, 51, -1, -1, -1}, // 11: elf { 94, 95, 96, 97, 98, -1} // 12: zwölf and 00: null }; // mask "it is" const int maskItIs[maskSizeItIs] = {0, 1, 3, 4, 5}; // mask minute dots const int maskMinuteDots[maskSizeMinuteDots] = {110, 111, 112, 113}; // overall mask to define which LEDs are on int maskLedsOn[maskSizeLeds] = { 0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0 }; // update led mask void updateLedMask(const int wordMask[], int arraySize) { // loop over array for (int x=0; x < arraySize; x++) { // check if mask has a valid LED number if (wordMask[x] >= 0 && wordMask[x] < maskSizeLeds) { // turn LED on maskLedsOn[wordMask[x]] = 1; } } } // set hours void setHours(int hours, bool fullClock) { int index = hours; // handle 00:xx as 12:xx if (hours == 0) { index = 12; } // check if we get an overrun of 12 o´clock if (hours == 13) { index = 1; } // special handling for "ein Uhr" instead of "eins Uhr" if (hours == 1 && fullClock == true) { index = 0; } // update led mask if (meander) { updateLedMask(maskHoursMea[index], maskSizeHoursMea); } else { updateLedMask(maskHours[index], maskSizeHours); } } // set minutes void setMinutes(int index) { // update led mask if (meander) { updateLedMask(maskMinutesMea[index], maskSizeMinutesMea); } else { updateLedMask(maskMinutes[index], maskSizeMinutes); } } // set minutes dot void setSingleMinuteDots(int minutes) { // modulo to get minute dots int minutesDotCount = minutes % 5; // check if minute dots are active if (minutesDotCount > 0) { // activate all minute dots until number is reached for (int i = 0; i < minutesDotCount; i++) { // activate LED maskLedsOn[maskMinuteDots[i]] = 1; } } } // update the display void updateDisplay(uint8_t hours, uint8_t minutes) { // disable complete matrix at the bigging for (int x = 0; x < maskSizeLeds; x++) { maskLedsOn[x] = 0; } // display it is/es ist if activated if (displayItIs) { updateLedMask(maskItIs, maskSizeItIs); } // set single minute dots setSingleMinuteDots(minutes); // switch minutes switch (minutes / 5) { case 0: // full hour setMinutes(0); setHours(hours, true); break; case 1: // 5 nach setMinutes(1); setHours(hours, false); break; case 2: // 10 nach setMinutes(2); setHours(hours, false); break; case 3: if (nord) { // viertel nach setMinutes(12); setHours(hours, false); } else { // viertel setMinutes(3); setHours(hours + 1, false); }; break; case 4: // 20 nach setMinutes(4); setHours(hours, false); break; case 5: // 5 vor halb setMinutes(5); setHours(hours + 1, false); break; case 6: // halb setMinutes(6); setHours(hours + 1, false); break; case 7: // 5 nach halb setMinutes(7); setHours(hours + 1, false); break; case 8: // 20 vor setMinutes(8); setHours(hours + 1, false); break; case 9: // viertel vor if (nord) { setMinutes(13); } // dreiviertel else { setMinutes(9); } setHours(hours + 1, false); break; case 10: // 10 vor setMinutes(10); setHours(hours + 1, false); break; case 11: // 5 vor setMinutes(11); setHours(hours + 1, false); break; } } public: //Functions called by WLED /* * 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() { } /* * connected() is called every time the WiFi is (re)connected * Use it to initialize network interfaces */ void connected() { } /* * 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() { // do it every 5 seconds if (millis() - lastTime > 5000) { // check the time int minutes = minute(localTime); // check if we already updated this minute if (lastTimeMinutes != minutes) { // update the display with new time updateDisplay(hourFormat12(localTime), minute(localTime)); // remember last update time lastTimeMinutes = minutes; } // remember last update lastTime = millis(); } } /* * 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) { } */ /* * 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 make your settings editable through the Usermod Settings page automatically. * * Usermod Settings Overview: * - Numeric values are treated as floats in the browser. * - If the numeric value entered into the browser contains a decimal point, it will be parsed as a C float * before being returned to the Usermod. The float data type has only 6-7 decimal digits of precision, and * doubles are not supported, numbers will be rounded to the nearest float value when being parsed. * The range accepted by the input field is +/- 1.175494351e-38 to +/- 3.402823466e+38. * - If the numeric value entered into the browser doesn't contain a decimal point, it will be parsed as a * C int32_t (range: -2147483648 to 2147483647) before being returned to the usermod. * Overflows or underflows are truncated to the max/min value for an int32_t, and again truncated to the type * used in the Usermod when reading the value from ArduinoJson. * - Pin values can be treated differently from an integer value by using the key name "pin" * - "pin" can contain a single or array of integer values * - On the Usermod Settings page there is simple checking for pin conflicts and warnings for special pins * - Red color indicates a conflict. Yellow color indicates a pin with a warning (e.g. an input-only pin) * - Tip: use int8_t to store the pin value in the Usermod, so a -1 value (pin not set) can be used * * See usermod_v2_auto_save.h for an example that saves Flash space by reusing ArduinoJson key name strings * * If you need a dedicated settings page with custom layout for your Usermod, that takes a lot more work. * You will have to add the setting to the HTML, xml.cpp and set.cpp manually. * See the WLED Soundreactive fork (code and wiki) for reference. https://github.com/atuline/WLED * * I highly recommend checking out the basics of ArduinoJson serialization and deserialization in order to use custom settings! */ void addToConfig(JsonObject& root) { JsonObject top = root.createNestedObject(F("WordClockUsermod")); top[F("active")] = usermodActive; top[F("displayItIs")] = displayItIs; top[F("ledOffset")] = ledOffset; top[F("Meander wiring?")] = meander; top[F("Norddeutsch")] = nord; } void appendConfigData() { oappend(F("addInfo('WordClockUsermod:ledOffset', 1, 'Number of LEDs before the letters');")); oappend(F("addInfo('WordClockUsermod:Norddeutsch', 1, 'Viertel vor instead of Dreiviertel');")); } /* * 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 immediately after boot, or after saving on the Usermod Settings page) * * 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 :) * * Return true in case the config values returned from Usermod Settings were complete, or false if you'd like WLED to save your defaults to disk (so any missing values are editable in Usermod Settings) * * getJsonValue() returns false if the value is missing, or copies the value into the variable provided and returns true if the value is present * The configComplete variable is true only if the "exampleUsermod" object and all values are present. If any values are missing, WLED will know to call addToConfig() to save them * * This function is guaranteed to be called on boot, but could also be called every time settings are updated */ bool readFromConfig(JsonObject& root) { // default settings values could be set here (or below using the 3-argument getJsonValue()) instead of in the class definition or constructor // setting them inside readFromConfig() is slightly more robust, handling the rare but plausible use case of single value being missing after boot (e.g. if the cfg.json was manually edited and a value was removed) JsonObject top = root[F("WordClockUsermod")]; bool configComplete = !top.isNull(); configComplete &= getJsonValue(top[F("active")], usermodActive); configComplete &= getJsonValue(top[F("displayItIs")], displayItIs); configComplete &= getJsonValue(top[F("ledOffset")], ledOffset); configComplete &= getJsonValue(top[F("Meander wiring?")], meander); configComplete &= getJsonValue(top[F("Norddeutsch")], nord); return configComplete; } /* * handleOverlayDraw() is called just before every show() (LED strip update frame) after effects have set the colors. * Use this to blank out some LEDs or set them to a different color regardless of the set effect mode. * Commonly used for custom clocks (Cronixie, 7 segment) */ void handleOverlayDraw() { // check if usermod is active if (usermodActive == true) { // loop over all leds for (int x = 0; x < maskSizeLeds; x++) { // check mask if (maskLedsOn[x] == 0) { // set pixel off strip.setPixelColor(x + ledOffset, RGBW32(0,0,0,0)); } } } } /* * 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_WORDCLOCK; } //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! }; static WordClockUsermod usermod_v2_word_clock; REGISTER_USERMOD(usermod_v2_word_clock); ================================================ FILE: usermods/wireguard/library.json ================================================ { "name": "wireguard", "build": { "libArchive": false}, "dependencies": { "WireGuard-ESP32-Arduino":"https://github.com/kienvu58/WireGuard-ESP32-Arduino.git" } } ================================================ FILE: usermods/wireguard/readme.md ================================================ # WireGuard VPN This usermod will connect your WLED instance to a remote WireGuard subnet. Configuration is performed via the Usermod menu. There are no parameters to set in code! ## Installation Copy the `platformio_override.ini` file to the root project directory, review the build options, and select the `WLED_ESP32-WireGuard` environment. ## Author Aiden Vigue [vigue.me](https://vigue.me) [@acvigue](https://github.com/acvigue) aiden@vigue.me ================================================ FILE: usermods/wireguard/wireguard.cpp ================================================ #include #include "wled.h" class WireguardUsermod : public Usermod { public: void setup() { configTzTime(posix_tz, ntpServerName); } void connected() { if (wg.is_initialized()) { wg.end(); } } void loop() { if (millis() - lastTime > 5000) { if (is_enabled && WLED_CONNECTED) { if (!wg.is_initialized()) { struct tm timeinfo; if (getLocalTime(&timeinfo, 0)) { if (strlen(preshared_key) < 1) { wg.begin(local_ip, private_key, endpoint_address, public_key, endpoint_port, NULL); } else { wg.begin(local_ip, private_key, endpoint_address, public_key, endpoint_port, preshared_key); } } } } lastTime = millis(); } } void addToJsonInfo(JsonObject& root) { JsonObject user = root["u"]; if (user.isNull()) user = root.createNestedObject("u"); JsonArray infoArr = user.createNestedArray(F("WireGuard")); String uiDomString; struct tm timeinfo; if (!getLocalTime(&timeinfo, 0)) { uiDomString = "Time out of sync!"; } else { if (wg.is_initialized()) { uiDomString = "netif up!"; } else { uiDomString = "netif down :("; } } if (is_enabled) infoArr.add(uiDomString); } void appendConfigData() { oappend(F("addInfo('WireGuard:host',1,'Server Hostname');")); // 0 is field type, 1 is actual field oappend(F("addInfo('WireGuard:port',1,'Server Port');")); // 0 is field type, 1 is actual field oappend(F("addInfo('WireGuard:ip',1,'Device IP');")); // 0 is field type, 1 is actual field oappend(F("addInfo('WireGuard:psk',1,'Pre Shared Key (optional)');")); // 0 is field type, 1 is actual field oappend(F("addInfo('WireGuard:pem',1,'Private Key');")); // 0 is field type, 1 is actual field oappend(F("addInfo('WireGuard:pub',1,'Public Key');")); // 0 is field type, 1 is actual field oappend(F("addInfo('WireGuard:tz',1,'POSIX timezone string');")); // 0 is field type, 1 is actual field } void addToConfig(JsonObject& root) { JsonObject top = root.createNestedObject(F("WireGuard")); top[F("host")] = endpoint_address; top["port"] = endpoint_port; top["ip"] = local_ip.toString(); top["psk"] = preshared_key; top[F("pem")] = private_key; top[F("pub")] = public_key; top[F("tz")] = posix_tz; } bool readFromConfig(JsonObject& root) { JsonObject top = root[F("WireGuard")]; if (top[F("host")].isNull() || top["port"].isNull() || top["ip"].isNull() || top[F("pem")].isNull() || top[F("pub")].isNull() || top[F("tz")].isNull()) { is_enabled = false; return false; } else { const char* host = top[F("host")]; strncpy(endpoint_address, host, 100); const char* ip_s = top["ip"]; uint8_t ip[4]; sscanf(ip_s, "%u.%u.%u.%u", &ip[0], &ip[1], &ip[2], &ip[3]); local_ip = IPAddress(ip[0], ip[1], ip[2], ip[3]); const char* pem = top[F("pem")]; strncpy(private_key, pem, 45); const char* pub = top[F("pub")]; strncpy(public_key, pub, 45); const char* tz = top[F("tz")]; strncpy(posix_tz, tz, 150); endpoint_port = top[F("port")]; if (!top["psk"].isNull()) { const char* psk = top["psk"]; strncpy(preshared_key, psk, 45); } is_enabled = true; } return is_enabled; } uint16_t getId() { return USERMOD_ID_WIREGUARD; } private: WireGuard wg; char preshared_key[45]; char private_key[45]; IPAddress local_ip; char public_key[45]; char endpoint_address[100]; char posix_tz[150]; int endpoint_port = 0; bool is_enabled = false; unsigned long lastTime = 0; }; static WireguardUsermod wireguard; REGISTER_USERMOD(wireguard); ================================================ FILE: usermods/wizlights/library.json ================================================ { "name": "wizlights", "build": { "libArchive": false } } ================================================ FILE: usermods/wizlights/readme.md ================================================ # Controlling Wiz lights Enables controlling [WiZ](https://www.wizconnected.com/en/consumer/) lights that are part of the same network as the WLED controller. The mod takes the colors from the first few pixels and sends them to the lights. ## Configuration - Interval (ms) - How frequently to update the WiZ lights, in milliseconds. - Setting it too low may cause the ESP to become unresponsive. - Send Delay (ms) - An optional millisecond delay after updating each WiZ light. - Can help smooth out effects when using a large number of WiZ lights - Use Enhanced White - Uses the WiZ lights onboard white LEDs instead of sending maximum RGB values. - Tunable with warm and cool LEDs as supported by WiZ bulbs - Note: Only sent when max RGB value is set, the automatic brightness limiter must be disabled - ToDo: Have better logic for white value mixing to take advantage of the light's capabilities - Always Force Update - Can be enabled to always send update message to light even if the new value matches the old value. - Force update every x minutes - adjusts the default force update timeout of 5 minutes. - Setting to 0 is the same as enabling Always Force Update - Next, enter the IP addresses for the lights to be controlled, in order. The limit is 15 devices, but that number can be easily changed by updating _MAX_WIZ_LIGHTS_. ## Related project If you use these lights and python, make sure to check out the [pywizlight](https://github.com/sbidy/pywizlight) project. You can learn how to format the messages to control the lights from that project. ================================================ FILE: usermods/wizlights/wizlights.cpp ================================================ #include "wled.h" #include // Maximum number of lights supported #define MAX_WIZ_LIGHTS 15 WiFiUDP UDP; class WizLightsUsermod : public Usermod { private: unsigned long lastTime = 0; long updateInterval; long sendDelay; long forceUpdateMinutes; bool forceUpdate; bool useEnhancedWhite; long warmWhite; long coldWhite; IPAddress lightsIP[MAX_WIZ_LIGHTS]; // Stores Light IP addresses bool lightsValid[MAX_WIZ_LIGHTS]; // Stores Light IP address validity uint32_t colorsSent[MAX_WIZ_LIGHTS]; // Stores last color sent for each light public: // Send JSON blob to WiZ Light over UDP // RGB or C/W white // TODO: // Better utilize WLED existing white mixing logic void wizSendColor(IPAddress ip, uint32_t color) { UDP.beginPacket(ip, 38899); // If no LED color, turn light off. Note wiz light setting for "Off fade-out" will be applied by the light itself. if (color == 0) { UDP.print("{\"method\":\"setPilot\",\"params\":{\"state\":false}}"); // If color is WHITE, try and use the lights WHITE LEDs instead of mixing RGB LEDs } else if (color == 16777215 && useEnhancedWhite){ // set cold white light only if (coldWhite > 0 && warmWhite == 0){ UDP.print("{\"method\":\"setPilot\",\"params\":{\"c\":"); UDP.print(coldWhite) ;UDP.print("}}");} // set warm white light only if (warmWhite > 0 && coldWhite == 0){ UDP.print("{\"method\":\"setPilot\",\"params\":{\"w\":"); UDP.print(warmWhite) ;UDP.print("}}");} // set combination of warm and cold white light if (coldWhite > 0 && warmWhite > 0){ UDP.print("{\"method\":\"setPilot\",\"params\":{\"c\":"); UDP.print(coldWhite) ;UDP.print(",\"w\":"); UDP.print(warmWhite); UDP.print("}}");} // Send color as RGB } else { UDP.print("{\"method\":\"setPilot\",\"params\":{\"r\":"); UDP.print(R(color)); UDP.print(",\"g\":"); UDP.print(G(color)); UDP.print(",\"b\":"); UDP.print(B(color)); UDP.print("}}"); } UDP.endPacket(); } // Override definition so it compiles void setup() { } // TODO: Check millis() rollover void loop() { // Make sure we are connected first if (!WLED_CONNECTED) return; unsigned long ellapsedTime = millis() - lastTime; if (ellapsedTime > updateInterval) { bool update = false; for (uint8_t i = 0; i < MAX_WIZ_LIGHTS; i++) { if (!lightsValid[i]) { continue; } uint32_t newColor = strip.getPixelColor(i); if (forceUpdate || (newColor != colorsSent[i]) || (ellapsedTime > forceUpdateMinutes*60000)){ wizSendColor(lightsIP[i], newColor); colorsSent[i] = newColor; update = true; delay(sendDelay); } } if (update) lastTime = millis(); } } void addToConfig(JsonObject& root) { JsonObject top = root.createNestedObject("wizLightsUsermod"); top["Interval (ms)"] = updateInterval; top["Send Delay (ms)"] = sendDelay; top["Use Enhanced White *"] = useEnhancedWhite; top["* Warm White Value (0-255)"] = warmWhite; top["* Cold White Value (0-255)"] = coldWhite; top["Always Force Update"] = forceUpdate; top["Force Update Every x Minutes"] = forceUpdateMinutes; for (uint8_t i = 0; i < MAX_WIZ_LIGHTS; i++) { top[getJsonLabel(i)] = lightsIP[i].toString(); } } bool readFromConfig(JsonObject& root) { JsonObject top = root["wizLightsUsermod"]; bool configComplete = !top.isNull(); configComplete &= getJsonValue(top["Interval (ms)"], updateInterval, 1000); // How frequently to update the wiz lights configComplete &= getJsonValue(top["Send Delay (ms)"], sendDelay, 0); // Optional delay after sending each UDP message configComplete &= getJsonValue(top["Use Enhanced White *"], useEnhancedWhite, false); // When color is white use wiz white LEDs instead of mixing RGB configComplete &= getJsonValue(top["* Warm White Value (0-255)"], warmWhite, 0); // Warm White LED value for Enhanced White configComplete &= getJsonValue(top["* Cold White Value (0-255)"], coldWhite, 50); // Cold White LED value for Enhanced White configComplete &= getJsonValue(top["Always Force Update"], forceUpdate, false); // Update wiz light every loop, even if color value has not changed configComplete &= getJsonValue(top["Force Update Every x Minutes"], forceUpdateMinutes, 5); // Update wiz light if color value has not changed, every x minutes // Read list of IPs String tempIp; for (uint8_t i = 0; i < MAX_WIZ_LIGHTS; i++) { configComplete &= getJsonValue(top[getJsonLabel(i)], tempIp, "0.0.0.0"); lightsValid[i] = lightsIP[i].fromString(tempIp); // If the IP is not valid, force the value to be empty if (!lightsValid[i]){lightsIP[i].fromString("0.0.0.0");} } return configComplete; } // Create label for the usermod page (I cannot make it work with JSON arrays...) String getJsonLabel(uint8_t i) {return "WiZ Light IP #" + String(i+1);} uint16_t getId(){return USERMOD_ID_WIZLIGHTS;} }; static WizLightsUsermod wizlights; REGISTER_USERMOD(wizlights); ================================================ FILE: usermods/word-clock-matrix/library.json ================================================ { "name": "word-clock-matrix", "build": { "libArchive": false } } ================================================ FILE: usermods/word-clock-matrix/readme.md ================================================ ## Word clock usermod By @bwente See https://www.hackster.io/bwente/word-clock-with-just-two-components-073834 for the hardware guide!
Includes a customizable feature to reduce the brightness at night. ![image](https://user-images.githubusercontent.com/371964/197094071-f8ccaf59-1d85-4dd2-8e09-1389675291e1.png) ![image](https://user-images.githubusercontent.com/371964/197094211-6c736257-95ff-491f-9f0d-35d5135ecfea.png) ![mini_8x8_word_clock_reverse_stencil_sZFti6chj4(1)](https://user-images.githubusercontent.com/371964/197094410-7c275f3f-743b-477a-bc15-5e7bdbcbd833.svg) ![mini_8x8_word_clock_box_epUWJOBOhr(1)](https://user-images.githubusercontent.com/371964/197094496-fa49b355-164b-4bf5-84fd-f22f5206c645.svg) ================================================ FILE: usermods/word-clock-matrix/word-clock-matrix.cpp ================================================ #include "wled.h" /* * Things to do... * Turn on ntp clock 24h format * 64 LEDS */ class WordClockMatrix : public Usermod { private: unsigned long lastTime = 0; uint8_t minuteLast = 99; int dayBrightness = 128; int nightBrightness = 16; public: void setup() { Serial.println("Hello from my usermod!"); //saveMacro(14, "A=128", false); //saveMacro(15, "A=64", false); //saveMacro(16, "A=16", false); //saveMacro(1, "&FX=0&R=255&G=255&B=255", false); //strip.getSegment(1).setOption(SEG_OPTION_SELECTED, true); //select first two segments (background color + FX settable) Segment &seg = strip.getSegment(0); seg.colors[0] = ((0 << 24) | ((0 & 0xFF) << 16) | ((0 & 0xFF) << 8) | ((0 & 0xFF))); strip.getSegment(0).setOption(0, false); strip.getSegment(0).setOption(2, false); //other segments are text for (int i = 1; i < 10; i++) { Segment &text_seg = strip.getSegment(i); text_seg.colors[0] = ((0 << 24) | ((0 & 0xFF) << 16) | ((190 & 0xFF) << 8) | ((180 & 0xFF))); strip.getSegment(i).setOption(0, true); strip.setBrightness(64); } } void connected() { Serial.println("Connected to WiFi!"); } void selectWordSegments(bool state) { for (int i = 1; i < 10; i++) { //WS2812FX::Segment &seg = strip.getSegment(i); strip.getSegment(i).setOption(0, state); // strip.getSegment(1).setOption(SEG_OPTION_SELECTED, true); //seg.mode = 12; //seg.palette = 1; //strip.setBrightness(255); } strip.getSegment(0).setOption(0, !state); } void hourChime() { //strip.resetSegments(); selectWordSegments(true); colorUpdated(CALL_MODE_FX_CHANGED); savePreset(13); selectWordSegments(false); //strip.getSegment(0).setOption(0, true); strip.getSegment(0).setOption(2, true); applyPreset(12); colorUpdated(CALL_MODE_FX_CHANGED); } void displayTime(byte hour, byte minute) { bool isToHour = false; //true if minute > 30 strip.getSegment(0).setGeometry(0, 64); // background strip.getSegment(1).setGeometry(0, 2); //It is strip.getSegment(2).setGeometry(0, 0); strip.getSegment(3).setGeometry(0, 0); //disable minutes strip.getSegment(4).setGeometry(0, 0); //past strip.getSegment(6).setGeometry(0, 0); //to strip.getSegment(8).setGeometry(0, 0); //disable o'clock if (hour < 24) //valid time, display { if (minute == 30) { strip.getSegment(2).setGeometry(3, 6); //half strip.getSegment(3).setGeometry(0, 0); //minutes } else if (minute == 15 || minute == 45) { strip.getSegment(3).setGeometry(0, 0); //minutes } else if (minute == 10) { //strip.getSegment(5).setGeometry(6, 8); //ten } else if (minute == 5) { //strip.getSegment(5).setGeometry(16, 18); //five } else if (minute == 0) { strip.getSegment(3).setGeometry(0, 0); //minutes //hourChime(); } else { strip.getSegment(3).setGeometry(18, 22); //minutes } //past or to? if (minute == 0) { //full hour strip.getSegment(3).setGeometry(0, 0); //disable minutes strip.getSegment(4).setGeometry(0, 0); //disable past strip.getSegment(6).setGeometry(0, 0); //disable to strip.getSegment(8).setGeometry(60, 64); //o'clock } else if (minute > 34) { //strip.getSegment(6).setGeometry(22, 24); //to //minute = 60 - minute; isToHour = true; } else { //strip.getSegment(4).setGeometry(24, 27); //past //isToHour = false; } } //byte minuteRem = minute %10; if (minute <= 4) { strip.getSegment(3).setGeometry(0, 0); //nothing strip.getSegment(5).setGeometry(0, 0); //nothing strip.getSegment(6).setGeometry(0, 0); //nothing strip.getSegment(8).setGeometry(60, 64); //o'clock } else if (minute <= 9) { strip.getSegment(5).setGeometry(16, 18); // five past strip.getSegment(4).setGeometry(24, 27); //past } else if (minute <= 14) { strip.getSegment(5).setGeometry(6, 8); // ten past strip.getSegment(4).setGeometry(24, 27); //past } else if (minute <= 19) { strip.getSegment(5).setGeometry(8, 12); // quarter past strip.getSegment(3).setGeometry(0, 0); //minutes strip.getSegment(4).setGeometry(24, 27); //past } else if (minute <= 24) { strip.getSegment(5).setGeometry(12, 16); // twenty past strip.getSegment(4).setGeometry(24, 27); //past } else if (minute <= 29) { strip.getSegment(5).setGeometry(12, 18); // twenty-five past strip.getSegment(4).setGeometry(24, 27); //past } else if (minute <= 34) { strip.getSegment(5).setGeometry(3, 6); // half past strip.getSegment(3).setGeometry(0, 0); //minutes strip.getSegment(4).setGeometry(24, 27); //past } else if (minute <= 39) { strip.getSegment(5).setGeometry(12, 18); // twenty-five to strip.getSegment(6).setGeometry(22, 24); //to } else if (minute <= 44) { strip.getSegment(5).setGeometry(12, 16); // twenty to strip.getSegment(6).setGeometry(22, 24); //to } else if (minute <= 49) { strip.getSegment(5).setGeometry(8, 12); // quarter to strip.getSegment(3).setGeometry(0, 0); //minutes strip.getSegment(6).setGeometry(22, 24); //to } else if (minute <= 54) { strip.getSegment(5).setGeometry(6, 8); // ten to strip.getSegment(6).setGeometry(22, 24); //to } else if (minute <= 59) { strip.getSegment(5).setGeometry(16, 18); // five to strip.getSegment(6).setGeometry(22, 24); //to } //hours if (hour > 23) return; if (isToHour) hour++; if (hour > 12) hour -= 12; if (hour == 0) hour = 12; switch (hour) { case 1: strip.getSegment(7).setGeometry(27, 29); break; //one case 2: strip.getSegment(7).setGeometry(35, 37); break; //two case 3: strip.getSegment(7).setGeometry(29, 32); break; //three case 4: strip.getSegment(7).setGeometry(32, 35); break; //four case 5: strip.getSegment(7).setGeometry(37, 40); break; //five case 6: strip.getSegment(7).setGeometry(43, 45); break; //six case 7: strip.getSegment(7).setGeometry(40, 43); break; //seven case 8: strip.getSegment(7).setGeometry(45, 48); break; //eight case 9: strip.getSegment(7).setGeometry(48, 50); break; //nine case 10: strip.getSegment(7).setGeometry(54, 56); break; //ten case 11: strip.getSegment(7).setGeometry(50, 54); break; //eleven case 12: strip.getSegment(7).setGeometry(56, 60); break; //twelve } selectWordSegments(true); applyPreset(1); } void timeOfDay() { // NOT USED: use timed macros instead //Used to set brightness dependant of time of day - lights dimmed at night //monday to thursday and sunday if ((weekday(localTime) == 6) | (weekday(localTime) == 7)) { if ((hour(localTime) > 0) | (hour(localTime) < 8)) { strip.setBrightness(nightBrightness); } else { strip.setBrightness(dayBrightness); } } else { if ((hour(localTime) < 6) | (hour(localTime) >= 22)) { strip.setBrightness(nightBrightness); } else { strip.setBrightness(dayBrightness); } } } //loop. You can use "if (WLED_CONNECTED)" to check for successful connection void loop() { if (millis() - lastTime > 1000) { //Serial.println("I'm alive!"); Serial.println(hour(localTime)); lastTime = millis(); } if (minute(localTime) != minuteLast) { updateLocalTime(); //timeOfDay(); minuteLast = minute(localTime); displayTime(hour(localTime), minute(localTime)); if (minute(localTime) == 0) { hourChime(); } if (minute(localTime) == 1) { //turn off background segment; strip.getSegment(0).setOption(2, false); //applyPreset(13); } } } void addToConfig(JsonObject& root) { JsonObject modName = root.createNestedObject("id"); modName[F("mdns")] = "wled-word-clock"; modName[F("name")] = "WLED WORD CLOCK"; } uint16_t getId() { return 500; } }; static WordClockMatrix word_clock_matrix; REGISTER_USERMOD(word_clock_matrix); ================================================ FILE: wled00/FX.cpp ================================================ /* WS2812FX.cpp contains all effect methods Harm Aldick - 2016 www.aldick.org Copyright (c) 2016 Harm Aldick Licensed under the EUPL v. 1.2 or later Adapted from code originally licensed under the MIT license Modified heavily for WLED */ #include "wled.h" #include "FX.h" #include "fcn_declare.h" #define FX_FALLBACK_STATIC { mode_static(); return; } #if !(defined(WLED_DISABLE_PARTICLESYSTEM2D) && defined(WLED_DISABLE_PARTICLESYSTEM1D)) #include "FXparticleSystem.h" // include particle system code only if at least one system is enabled #ifdef WLED_DISABLE_PARTICLESYSTEM2D #define WLED_PS_DONT_REPLACE_2D_FX #endif #ifdef WLED_DISABLE_PARTICLESYSTEM1D #define WLED_PS_DONT_REPLACE_1D_FX #endif #ifdef ESP8266 #if !defined(WLED_DISABLE_PARTICLESYSTEM2D) && !defined(WLED_DISABLE_PARTICLESYSTEM1D) #error ESP8266 does not support 1D and 2D particle systems simultaneously. Please disable one of them. #endif #endif #else #define WLED_PS_DONT_REPLACE_1D_FX #define WLED_PS_DONT_REPLACE_2D_FX #endif #ifdef WLED_PS_DONT_REPLACE_FX #define WLED_PS_DONT_REPLACE_1D_FX #define WLED_PS_DONT_REPLACE_2D_FX #endif ////////////// // DEV INFO // ////////////// /* information for FX metadata strings: https://kno.wled.ge/interfaces/json-api/#effect-metadata Audio Reactive: use the following code to pass usermod variables to effect uint8_t *binNum = (uint8_t*)&SEGENV.aux1, *maxVol = (uint8_t*)(&SEGENV.aux1+1); // just in case assignment bool samplePeak = false; float FFT_MajorPeak = 1.0; uint8_t *fftResult = nullptr; float *fftBin = nullptr; um_data_t *um_data = getAudioData(); volumeSmth = *(float*) um_data->u_data[0]; volumeRaw = *(float*) um_data->u_data[1]; fftResult = (uint8_t*) um_data->u_data[2]; samplePeak = *(uint8_t*) um_data->u_data[3]; FFT_MajorPeak = *(float*) um_data->u_data[4]; my_magnitude = *(float*) um_data->u_data[5]; maxVol = (uint8_t*) um_data->u_data[6]; // requires UI element (SEGMENT.customX?), changes source element binNum = (uint8_t*) um_data->u_data[7]; // requires UI element (SEGMENT.customX?), changes source element fftBin = (float*) um_data->u_data[8]; */ #define IBN 5100 // paletteBlend: 0 - wrap when moving, 1 - always wrap, 2 - never wrap, 3 - none (undefined) #define PALETTE_SOLID_WRAP (paletteBlend == 1 || paletteBlend == 3) #define PALETTE_MOVING_WRAP !(paletteBlend == 2 || (paletteBlend == 0 && SEGMENT.speed == 0)) #define indexToVStrip(index, stripNr) ((index) | (int((stripNr)+1)<<16)) // a few constants needed for AudioReactive effects // for 22Khz sampling #define MAX_FREQUENCY 11025 // sample frequency / 2 (as per Nyquist criterion) #define MAX_FREQ_LOG10 4.04238f // log10(MAX_FREQUENCY) // for 20Khz sampling //#define MAX_FREQUENCY 10240 //#define MAX_FREQ_LOG10 4.0103f // for 10Khz sampling //#define MAX_FREQUENCY 5120 //#define MAX_FREQ_LOG10 3.71f // effect utility functions static uint8_t sin_gap(uint16_t in) { if (in & 0x100) return 0; return sin8_t(in + 192); // correct phase shift of sine so that it starts and stops at 0 } static uint16_t triwave16(uint16_t in) { if (in < 0x8000) return in *2; return 0xFFFF - (in - 0x8000)*2; } /* * Generates a tristate square wave w/ attac & decay * @param x input value 0-255 * @param pulsewidth 0-127 * @param attdec attack & decay, max. pulsewidth / 2 * @returns signed waveform value */ static int8_t tristate_square8(uint8_t x, uint8_t pulsewidth, uint8_t attdec) { int8_t a = 127; if (x > 127) { a = -127; x -= 127; } if (x < attdec) { //inc to max return (int16_t) x * a / attdec; } else if (x < pulsewidth - attdec) { //max return a; } else if (x < pulsewidth) { //dec to 0 return (int16_t) (pulsewidth - x) * a / attdec; } return 0; } static um_data_t* getAudioData() { um_data_t *um_data; if (!UsermodManager::getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) { // add support for no audio um_data = simulateSound(SEGMENT.soundSim); } return um_data; } // effect functions /* * No blinking. Just plain old static light. */ void mode_static(void) { SEGMENT.fill(SEGCOLOR(0)); } static const char _data_FX_MODE_STATIC[] PROGMEM = "Solid"; /* * Copy a segment and perform (optional) color adjustments */ void mode_copy_segment(void) { uint32_t sourceid = SEGMENT.custom3; if (sourceid >= strip.getSegmentsNum() || sourceid == strip.getCurrSegmentId()) { // invalid source SEGMENT.fadeToBlackBy(5); // fade out } Segment& sourcesegment = strip.getSegment(sourceid); if (sourcesegment.isActive()) { uint32_t sourcecolor; uint32_t destcolor; if(sourcesegment.is2D()) { // 2D source, note: 2D to 1D just copies the first row (or first column if 'Switch axis' is checked in FX) for (unsigned y = 0; y < SEGMENT.vHeight(); y++) { for (unsigned x = 0; x < SEGMENT.vWidth(); x++) { unsigned sx = x; // source coordinates unsigned sy = y; if(SEGMENT.check1) std::swap(sx, sy); // flip axis if(SEGMENT.check2) { sourcecolor = strip.getPixelColorXY(sx + sourcesegment.start, sy + sourcesegment.startY); // read from global buffer (reads the last rendered frame) } else { sourcesegment.setDrawDimensions(); // set to source segment dimensions sourcecolor = sourcesegment.getPixelColorXY(sx, sy); // read from segment buffer } destcolor = adjust_color(sourcecolor, SEGMENT.intensity, SEGMENT.custom1, SEGMENT.custom2); SEGMENT.setDrawDimensions(); // reset to current segment dimensions SEGMENT.setPixelColorXY(x, y, destcolor); } } } else { // 1D source, source can be expanded into 2D for (unsigned i = 0; i < SEGMENT.vLength(); i++) { if(SEGMENT.check2) { sourcecolor = strip.getPixelColorNoMap(i + sourcesegment.start); // read from global buffer (reads the last rendered frame) } else { sourcesegment.setDrawDimensions(); // set to source segment dimensions sourcecolor = sourcesegment.getPixelColor(i); } destcolor = adjust_color(sourcecolor, SEGMENT.intensity, SEGMENT.custom1, SEGMENT.custom2); SEGMENT.setDrawDimensions(); // reset to current segment dimensions SEGMENT.setPixelColor(i, destcolor); } } } } static const char _data_FX_MODE_COPY[] PROGMEM = "Copy Segment@,Color shift,Lighten,Brighten,ID,Axis(2D),FullStack(last frame);;;12;ix=0,c1=0,c2=0,c3=0"; /* * Blink/strobe function * Alternate between color1 and color2 * if(strobe == true) then create a strobe effect */ void blink(uint32_t color1, uint32_t color2, bool strobe, bool do_palette) { uint32_t cycleTime = (255 - SEGMENT.speed)*20; uint32_t onTime = FRAMETIME; if (!strobe) onTime += ((cycleTime * SEGMENT.intensity) >> 8); cycleTime += FRAMETIME*2; uint32_t it = strip.now / cycleTime; uint32_t rem = strip.now % cycleTime; bool on = false; if (it != SEGENV.step //new iteration, force on state for one frame, even if set time is too brief || rem <= onTime) { on = true; } SEGENV.step = it; //save previous iteration uint32_t color = on ? color1 : color2; if (color == color1 && do_palette) { for (unsigned i = 0; i < SEGLEN; i++) { SEGMENT.setPixelColor(i, SEGMENT.color_from_palette(i, true, PALETTE_SOLID_WRAP, 0)); } } else SEGMENT.fill(color); } /* * Normal blinking. Intensity sets duty cycle. */ void mode_blink(void) { blink(SEGCOLOR(0), SEGCOLOR(1), false, true); } static const char _data_FX_MODE_BLINK[] PROGMEM = "Blink@!,Duty cycle;!,!;!;01"; /* * Classic Blink effect. Cycling through the rainbow. */ void mode_blink_rainbow(void) { blink(SEGMENT.color_wheel(SEGENV.call & 0xFF), SEGCOLOR(1), false, false); } static const char _data_FX_MODE_BLINK_RAINBOW[] PROGMEM = "Blink Rainbow@Frequency,Blink duration;!,!;!;01"; /* * Classic Strobe effect. */ void mode_strobe(void) { return blink(SEGCOLOR(0), SEGCOLOR(1), true, true); } static const char _data_FX_MODE_STROBE[] PROGMEM = "Strobe@!;!,!;!;01"; /* * Classic Strobe effect. Cycling through the rainbow. */ void mode_strobe_rainbow(void) { return blink(SEGMENT.color_wheel(SEGENV.call & 0xFF), SEGCOLOR(1), true, false); } static const char _data_FX_MODE_STROBE_RAINBOW[] PROGMEM = "Strobe Rainbow@!;,!;!;01"; /* * Color wipe function * LEDs are turned on (color1) in sequence, then turned off (color2) in sequence. * if (bool rev == true) then LEDs are turned off in reverse order */ void color_wipe(bool rev, bool useRandomColors) { if (SEGLEN <= 1) FX_FALLBACK_STATIC; uint32_t cycleTime = 750 + (255 - SEGMENT.speed)*150; uint32_t perc = strip.now % cycleTime; unsigned prog = (perc * 65535) / cycleTime; bool back = (prog > 32767); if (back) { prog -= 32767; if (SEGENV.step == 0) SEGENV.step = 1; } else { if (SEGENV.step == 2) SEGENV.step = 3; //trigger color change } if (useRandomColors) { if (SEGENV.call == 0) { SEGENV.aux0 = hw_random8(); SEGENV.step = 3; } if (SEGENV.step == 1) { //if flag set, change to new random color SEGENV.aux1 = get_random_wheel_index(SEGENV.aux0); SEGENV.step = 2; } if (SEGENV.step == 3) { SEGENV.aux0 = get_random_wheel_index(SEGENV.aux1); SEGENV.step = 0; } } unsigned ledIndex = (prog * SEGLEN) >> 15; uint16_t rem = (prog * SEGLEN) * 2; //mod 0xFFFF by truncating rem /= (SEGMENT.intensity +1); if (rem > 255) rem = 255; uint32_t col1 = useRandomColors? SEGMENT.color_wheel(SEGENV.aux1) : SEGCOLOR(1); for (unsigned i = 0; i < SEGLEN; i++) { unsigned index = (rev && back)? SEGLEN -1 -i : i; uint32_t col0 = useRandomColors? SEGMENT.color_wheel(SEGENV.aux0) : SEGMENT.color_from_palette(index, true, PALETTE_SOLID_WRAP, 0); if (i < ledIndex) { SEGMENT.setPixelColor(index, back? col1 : col0); } else { SEGMENT.setPixelColor(index, back? col0 : col1); if (i == ledIndex) SEGMENT.setPixelColor(index, color_blend(back? col0 : col1, back? col1 : col0, uint8_t(rem))); } } } /* * Lights all LEDs one after another. */ void mode_color_wipe(void) { color_wipe(false, false); } static const char _data_FX_MODE_COLOR_WIPE[] PROGMEM = "Wipe@!,!;!,!;!"; /* * Lights all LEDs one after another. Turns off opposite */ void mode_color_sweep(void) { color_wipe(true, false); } static const char _data_FX_MODE_COLOR_SWEEP[] PROGMEM = "Sweep@!,!;!,!;!"; /* * Turns all LEDs after each other to a random color. * Then starts over with another color. */ void mode_color_wipe_random(void) { color_wipe(false, true); } static const char _data_FX_MODE_COLOR_WIPE_RANDOM[] PROGMEM = "Wipe Random@!;;!"; /* * Random color introduced alternating from start and end of strip. */ void mode_color_sweep_random(void) { color_wipe(true, true); } static const char _data_FX_MODE_COLOR_SWEEP_RANDOM[] PROGMEM = "Sweep Random@!;;!"; /* * Lights all LEDs up in one random color. Then switches them * to the next random color. */ void mode_random_color(void) { uint32_t cycleTime = 200 + (255 - SEGMENT.speed)*50; uint32_t it = strip.now / cycleTime; uint32_t rem = strip.now % cycleTime; unsigned fadedur = (cycleTime * SEGMENT.intensity) >> 8; uint32_t fade = 255; if (fadedur) { fade = (rem * 255) / fadedur; if (fade > 255) fade = 255; } if (SEGENV.call == 0) { SEGENV.aux0 = hw_random8(); SEGENV.step = 2; } if (it != SEGENV.step) //new color { SEGENV.aux1 = SEGENV.aux0; SEGENV.aux0 = get_random_wheel_index(SEGENV.aux0); //aux0 will store our random color wheel index SEGENV.step = it; } SEGMENT.fill(color_blend(SEGMENT.color_wheel(SEGENV.aux1), SEGMENT.color_wheel(SEGENV.aux0), uint8_t(fade))); } static const char _data_FX_MODE_RANDOM_COLOR[] PROGMEM = "Random Colors@!,Fade time;;!;01"; /* * Lights every LED in a random color. Changes all LED at the same time * to new random colors. */ void mode_dynamic(void) { if (!SEGENV.allocateData(SEGLEN)) FX_FALLBACK_STATIC; //allocation failed if(SEGENV.call == 0) { //SEGMENT.fill(BLACK); for (unsigned i = 0; i < SEGLEN; i++) SEGENV.data[i] = hw_random8(); } uint32_t cycleTime = 50 + (255 - SEGMENT.speed)*15; uint32_t it = strip.now / cycleTime; if (it != SEGENV.step && SEGMENT.speed != 0) //new color { for (unsigned i = 0; i < SEGLEN; i++) { if (hw_random8() <= SEGMENT.intensity) SEGENV.data[i] = hw_random8(); // random color index } SEGENV.step = it; } if (SEGMENT.check1) { for (unsigned i = 0; i < SEGLEN; i++) { SEGMENT.blendPixelColor(i, SEGMENT.color_wheel(SEGENV.data[i]), 16); } } else { for (unsigned i = 0; i < SEGLEN; i++) { SEGMENT.setPixelColor(i, SEGMENT.color_wheel(SEGENV.data[i])); } } } static const char _data_FX_MODE_DYNAMIC[] PROGMEM = "Dynamic@!,!,,,,Smooth;;!"; /* * effect "Dynamic" with smooth color-fading */ void mode_dynamic_smooth(void) { bool old = SEGMENT.check1; SEGMENT.check1 = true; mode_dynamic(); SEGMENT.check1 = old; } static const char _data_FX_MODE_DYNAMIC_SMOOTH[] PROGMEM = "Dynamic Smooth@!,!;;!"; /* * Does the "standby-breathing" of well known i-Devices. */ void mode_breath(void) { unsigned var = 0; unsigned counter = (strip.now * ((SEGMENT.speed >> 3) +10)) & 0xFFFFU; counter = (counter >> 2) + (counter >> 4); //0-16384 + 0-2048 if (counter < 16384) { if (counter > 8192) counter = 8192 - (counter - 8192); var = sin16_t(counter) / 103; //close to parabolic in range 0-8192, max val. 23170 } uint8_t lum = 30 + var; for (unsigned i = 0; i < SEGLEN; i++) { SEGMENT.setPixelColor(i, color_blend(SEGCOLOR(1), SEGMENT.color_from_palette(i, true, PALETTE_SOLID_WRAP, 0), lum)); } } static const char _data_FX_MODE_BREATH[] PROGMEM = "Breathe@!;!,!;!;01"; /* * Fades the LEDs between two colors */ void mode_fade(void) { unsigned counter = (strip.now * ((SEGMENT.speed >> 3) +10)); uint8_t lum = triwave16(counter) >> 8; for (unsigned i = 0; i < SEGLEN; i++) { SEGMENT.setPixelColor(i, color_blend(SEGCOLOR(1), SEGMENT.color_from_palette(i, true, PALETTE_SOLID_WRAP, 0), lum)); } } static const char _data_FX_MODE_FADE[] PROGMEM = "Fade@!;!,!;!;01"; /* * Scan mode parent function */ void scan(bool dual) { if (SEGLEN <= 1) FX_FALLBACK_STATIC; uint32_t cycleTime = 750 + (255 - SEGMENT.speed)*150; uint32_t perc = strip.now % cycleTime; int prog = (perc * 65535) / cycleTime; int size = 1 + ((SEGMENT.intensity * SEGLEN) >> 9); int ledIndex = (prog * ((SEGLEN *2) - size *2)) >> 16; if (!SEGMENT.check2) SEGMENT.fill(SEGCOLOR(1)); int led_offset = ledIndex - (SEGLEN - size); led_offset = abs(led_offset); if (dual) { for (int j = led_offset; j < led_offset + size; j++) { unsigned i2 = SEGLEN -1 -j; SEGMENT.setPixelColor(i2, SEGMENT.color_from_palette(i2, true, PALETTE_SOLID_WRAP, (SEGCOLOR(2))? 2:0)); } } for (int j = led_offset; j < led_offset + size; j++) { SEGMENT.setPixelColor(j, SEGMENT.color_from_palette(j, true, PALETTE_SOLID_WRAP, 0)); } } /* * Runs a single pixel back and forth. */ void mode_scan(void) { scan(false); } static const char _data_FX_MODE_SCAN[] PROGMEM = "Scan@!,# of dots,,,,,Overlay;!,!,!;!"; /* * Runs two pixel back and forth in opposite directions. */ void mode_dual_scan(void) { scan(true); } static const char _data_FX_MODE_DUAL_SCAN[] PROGMEM = "Scan Dual@!,# of dots,,,,,Overlay;!,!,!;!"; /* * Cycles all LEDs at once through a rainbow. */ void mode_rainbow(void) { unsigned counter = (strip.now * ((SEGMENT.speed >> 2) +2)) & 0xFFFF; counter = counter >> 8; if (SEGMENT.intensity < 128){ SEGMENT.fill(color_blend(SEGMENT.color_wheel(counter),WHITE,uint8_t(128-SEGMENT.intensity))); } else { SEGMENT.fill(SEGMENT.color_wheel(counter)); } } static const char _data_FX_MODE_RAINBOW[] PROGMEM = "Colorloop@!,Saturation;;!;01"; /* * Cycles a rainbow over the entire string of LEDs. */ void mode_rainbow_cycle(void) { unsigned counter = (strip.now * ((SEGMENT.speed >> 2) +2)) & 0xFFFF; counter = counter >> 8; for (unsigned i = 0; i < SEGLEN; i++) { //intensity/29 = 0 (1/16) 1 (1/8) 2 (1/4) 3 (1/2) 4 (1) 5 (2) 6 (4) 7 (8) 8 (16) uint8_t index = (i * (16 << (SEGMENT.intensity /29)) / SEGLEN) + counter; SEGMENT.setPixelColor(i, SEGMENT.color_wheel(index)); } } static const char _data_FX_MODE_RAINBOW_CYCLE[] PROGMEM = "Rainbow@!,Size;;!"; /* * Alternating pixels running function. */ static void running(uint32_t color1, uint32_t color2, bool theatre = false) { int width = (theatre ? 3 : 1) + (SEGMENT.intensity >> 4); // window uint32_t cycleTime = 50 + (255 - SEGMENT.speed); uint32_t it = strip.now / cycleTime; bool usePalette = color1 == SEGCOLOR(0); for (unsigned i = 0; i < SEGLEN; i++) { uint32_t col = color2; if (usePalette) color1 = SEGMENT.color_from_palette(i, true, PALETTE_SOLID_WRAP, 0); if (theatre) { if ((i % width) == SEGENV.aux0) col = color1; } else { int pos = (i % (width<<1)); if ((pos < SEGENV.aux0-width) || ((pos >= SEGENV.aux0) && (pos < SEGENV.aux0+width))) col = color1; } SEGMENT.setPixelColor(i,col); } if (it != SEGENV.step) { SEGENV.aux0 = (SEGENV.aux0 +1) % (theatre ? width : (width<<1)); SEGENV.step = it; } } /* * Theatre-style crawling lights. * Inspired by the Adafruit examples. */ void mode_theater_chase(void) { running(SEGCOLOR(0), SEGCOLOR(1), true); } static const char _data_FX_MODE_THEATER_CHASE[] PROGMEM = "Theater@!,Gap size;!,!;!"; /* * Theatre-style crawling lights with rainbow effect. * Inspired by the Adafruit examples. */ void mode_theater_chase_rainbow(void) { running(SEGMENT.color_wheel(SEGENV.step), SEGCOLOR(1), true); } static const char _data_FX_MODE_THEATER_CHASE_RAINBOW[] PROGMEM = "Theater Rainbow@!,Gap size;,!;!"; /* * Running lights effect with smooth sine transition base. */ static void running_base(bool saw, bool dual=false) { unsigned x_scale = SEGMENT.intensity >> 2; uint32_t counter = (strip.now * SEGMENT.speed) >> 9; for (unsigned i = 0; i < SEGLEN; i++) { unsigned a = i*x_scale - counter; if (saw) { a &= 0xFF; if (a < 16) { a = 192 + a*8; } else { a = map(a,16,255,64,192); } a = 255 - a; } uint8_t s = dual ? sin_gap(a) : sin8_t(a); uint32_t ca = color_blend(SEGCOLOR(1), SEGMENT.color_from_palette(i, true, PALETTE_SOLID_WRAP, 0), s); if (dual) { unsigned b = (SEGLEN-1-i)*x_scale - counter; uint8_t t = sin_gap(b); uint32_t cb = color_blend(SEGCOLOR(1), SEGMENT.color_from_palette(i, true, PALETTE_SOLID_WRAP, 2), t); ca = color_blend(ca, cb, uint8_t(127)); } SEGMENT.setPixelColor(i, ca); } } /* * Running lights in opposite directions. * Idea: Make the gap width controllable with a third slider in the future */ void mode_running_dual(void) { running_base(false, true); } static const char _data_FX_MODE_RUNNING_DUAL[] PROGMEM = "Running Dual@!,Wave width;L,!,R;!"; /* * Running lights effect with smooth sine transition. */ void mode_running_lights(void) { running_base(false); } static const char _data_FX_MODE_RUNNING_LIGHTS[] PROGMEM = "Running@!,Wave width;!,!;!"; /* * Running lights effect with sawtooth transition. */ void mode_saw(void) { running_base(true); } static const char _data_FX_MODE_SAW[] PROGMEM = "Saw@!,Width;!,!;!"; /* * Blink several LEDs in random colors on, reset, repeat. * Inspired by www.tweaking4all.com/hardware/arduino/adruino-led-strip-effects/ */ void mode_twinkle(void) { SEGMENT.fade_out(224); uint32_t cycleTime = 20 + (255 - SEGMENT.speed)*5; uint32_t it = strip.now / cycleTime; if (it != SEGENV.step) { unsigned maxOn = map(SEGMENT.intensity, 0, 255, 1, SEGLEN); // make sure at least one LED is on if (SEGENV.aux0 >= maxOn) { SEGENV.aux0 = 0; SEGENV.aux1 = hw_random(); //new seed for our PRNG } SEGENV.aux0++; SEGENV.step = it; } uint16_t PRNG16 = SEGENV.aux1; for (unsigned i = 0; i < SEGENV.aux0; i++) { PRNG16 = (uint16_t)(PRNG16 * 2053) + 13849; // next 'random' number uint32_t p = (uint32_t)SEGLEN * (uint32_t)PRNG16; unsigned j = p >> 16; SEGMENT.setPixelColor(j, SEGMENT.color_from_palette(j, true, PALETTE_SOLID_WRAP, 0)); } } static const char _data_FX_MODE_TWINKLE[] PROGMEM = "Twinkle@!,!;!,!;!;;m12=0"; //pixels /* * Dissolve function */ void dissolve(uint32_t color) { unsigned dataSize = sizeof(uint32_t) * SEGLEN; if (!SEGENV.allocateData(dataSize)) FX_FALLBACK_STATIC; //allocation failed uint32_t* pixels = reinterpret_cast(SEGENV.data); if (SEGENV.call == 0) { for (unsigned i = 0; i < SEGLEN; i++) pixels[i] = SEGCOLOR(1); SEGENV.aux0 = 1; } for (unsigned j = 0; j <= SEGLEN / 15; j++) { if (hw_random8() <= SEGMENT.intensity) { for (size_t times = 0; times < 10; times++) { //attempt to spawn a new pixel 10 times unsigned i = hw_random16(SEGLEN); if (SEGENV.aux0) { //dissolve to primary/palette if (pixels[i] == SEGCOLOR(1)) { pixels[i] = color == SEGCOLOR(0) ? SEGMENT.color_from_palette(i, true, PALETTE_SOLID_WRAP, 0) : color; break; //only spawn 1 new pixel per frame } } else { //dissolve to secondary if (pixels[i] != SEGCOLOR(1)) { pixels[i] = SEGCOLOR(1); break; } } } } } unsigned incompletePixels = 0; for (unsigned i = 0; i < SEGLEN; i++) { SEGMENT.setPixelColor(i, pixels[i]); // fix for #4401 if (SEGMENT.check2) { if (SEGENV.aux0) { if (pixels[i] == SEGCOLOR(1)) incompletePixels++; } else { if (pixels[i] != SEGCOLOR(1)) incompletePixels++; } } } if (SEGENV.step > (255 - SEGMENT.speed) + 15U) { SEGENV.aux0 = !SEGENV.aux0; SEGENV.step = 0; } else { if (SEGMENT.check2) { if (incompletePixels == 0) SEGENV.step++; // only advance step once all pixels have changed } else SEGENV.step++; } } /* * Blink several LEDs on and then off */ void mode_dissolve(void) { dissolve(SEGMENT.check1 ? SEGMENT.color_wheel(hw_random8()) : SEGCOLOR(0)); } static const char _data_FX_MODE_DISSOLVE[] PROGMEM = "Dissolve@Repeat speed,Dissolve speed,,,,Random,Complete;!,!;!"; /* * Blink several LEDs on and then off in random colors */ void mode_dissolve_random(void) { dissolve(SEGMENT.color_wheel(hw_random8())); } static const char _data_FX_MODE_DISSOLVE_RANDOM[] PROGMEM = "Dissolve Rnd@Repeat speed,Dissolve speed;,!;!"; /* * Blinks one LED at a time. * Inspired by www.tweaking4all.com/hardware/arduino/adruino-led-strip-effects/ */ void mode_sparkle(void) { if (!SEGMENT.check2) for (unsigned i = 0; i < SEGLEN; i++) { SEGMENT.setPixelColor(i, SEGMENT.color_from_palette(i, true, PALETTE_SOLID_WRAP, 1)); } uint32_t cycleTime = 10 + (255 - SEGMENT.speed)*2; uint32_t it = strip.now / cycleTime; if (it != SEGENV.step) { SEGENV.aux0 = hw_random16(SEGLEN); // aux0 stores the random led index SEGENV.step = it; } SEGMENT.setPixelColor(SEGENV.aux0, SEGCOLOR(0)); } static const char _data_FX_MODE_SPARKLE[] PROGMEM = "Sparkle@!,,,,,,Overlay;!,!;!;;m12=0"; /* * Lights all LEDs in the color. Flashes single col 1 pixels randomly. (List name: Sparkle Dark) * Inspired by www.tweaking4all.com/hardware/arduino/adruino-led-strip-effects/ */ void mode_flash_sparkle(void) { if (!SEGMENT.check2) for (unsigned i = 0; i < SEGLEN; i++) { SEGMENT.setPixelColor(i, SEGMENT.color_from_palette(i, true, PALETTE_SOLID_WRAP, 0)); } if (strip.now - SEGENV.aux0 > SEGENV.step) { if(hw_random8((255-SEGMENT.intensity) >> 4) == 0) { SEGMENT.setPixelColor(hw_random16(SEGLEN), SEGCOLOR(1)); //flash } SEGENV.step = strip.now; SEGENV.aux0 = 255-SEGMENT.speed; } } static const char _data_FX_MODE_FLASH_SPARKLE[] PROGMEM = "Sparkle Dark@!,!,,,,,Overlay;Bg,Fx;!;;m12=0"; /* * Like flash sparkle. With more flash. * Inspired by www.tweaking4all.com/hardware/arduino/adruino-led-strip-effects/ */ void mode_hyper_sparkle(void) { if (!SEGMENT.check2) for (unsigned i = 0; i < SEGLEN; i++) { SEGMENT.setPixelColor(i, SEGMENT.color_from_palette(i, true, PALETTE_SOLID_WRAP, 0)); } if (strip.now - SEGENV.aux0 > SEGENV.step) { if (hw_random8((255-SEGMENT.intensity) >> 4) == 0) { int len = max(1, (int)SEGLEN/3); for (int i = 0; i < len; i++) { SEGMENT.setPixelColor(hw_random16(SEGLEN), SEGCOLOR(1)); } } SEGENV.step = strip.now; SEGENV.aux0 = 255-SEGMENT.speed; } } static const char _data_FX_MODE_HYPER_SPARKLE[] PROGMEM = "Sparkle+@!,!,,,,,Overlay;Bg,Fx;!;;m12=0"; /* * Strobe effect with different strobe count and pause, controlled by speed. */ void mode_multi_strobe(void) { for (unsigned i = 0; i < SEGLEN; i++) { SEGMENT.setPixelColor(i, SEGMENT.color_from_palette(i, true, PALETTE_SOLID_WRAP, 1)); } SEGENV.aux0 = 50 + 20*(uint16_t)(255-SEGMENT.speed); unsigned count = 2 * ((SEGMENT.intensity / 10) + 1); if(SEGENV.aux1 < count) { if((SEGENV.aux1 & 1) == 0) { SEGMENT.fill(SEGCOLOR(0)); SEGENV.aux0 = 15; } else { SEGENV.aux0 = 50; } } if (strip.now - SEGENV.aux0 > SEGENV.step) { SEGENV.aux1++; if (SEGENV.aux1 > count) SEGENV.aux1 = 0; SEGENV.step = strip.now; } } static const char _data_FX_MODE_MULTI_STROBE[] PROGMEM = "Strobe Mega@!,!;!,!;!;01"; /* * Android loading circle, refactored by @dedehai */ void mode_android(void) { if (!SEGENV.allocateData(sizeof(uint32_t))) FX_FALLBACK_STATIC; uint32_t* counter = reinterpret_cast(SEGENV.data); unsigned size = SEGENV.aux1 >> 1; // upper 15 bit unsigned shrinking = SEGENV.aux1 & 0x01; // lowest bit if(strip.now >= SEGENV.step) { SEGENV.step = strip.now + 3 + ((8 * (uint32_t)(255 - SEGMENT.speed)) / SEGLEN); if (size > (SEGMENT.intensity * SEGLEN) / 255) shrinking = 1; else if (size < 2) shrinking = 0; if (!shrinking) { // growing if ((*counter % 3) == 1) SEGENV.aux0++; // advance start position else size++; } else { // shrinking SEGENV.aux0++; if ((*counter % 3) != 1) size--; } SEGENV.aux1 = size << 1 | shrinking; // save back (*counter)++; if (SEGENV.aux0 >= SEGLEN) SEGENV.aux0 = 0; } uint32_t start = SEGENV.aux0; uint32_t end = (SEGENV.aux0 + size) % SEGLEN; for (unsigned i = 0; i < SEGLEN; i++) { if ((start < end && i >= start && i < end) || (start >= end && (i >= start || i < end))) SEGMENT.setPixelColor(i, SEGCOLOR(0)); else SEGMENT.setPixelColor(i, SEGMENT.color_from_palette(i, true, PALETTE_SOLID_WRAP, 1)); } } static const char _data_FX_MODE_ANDROID[] PROGMEM = "Android@!,Width;!,!;!;;m12=1"; //vertical /* * color chase function. * color1 = background color * color2 and color3 = colors of two adjacent leds */ static void chase(uint32_t color1, uint32_t color2, uint32_t color3, bool do_palette) { uint16_t counter = strip.now * ((SEGMENT.speed >> 2) + 1); uint16_t a = (counter * SEGLEN) >> 16; bool chase_random = (SEGMENT.mode == FX_MODE_CHASE_RANDOM); if (chase_random) { if (a < SEGENV.step) //we hit the start again, choose new color for Chase random { SEGENV.aux1 = SEGENV.aux0; //store previous random color SEGENV.aux0 = get_random_wheel_index(SEGENV.aux0); } color1 = SEGMENT.color_wheel(SEGENV.aux0); } SEGENV.step = a; // Use intensity setting to vary chase up to 1/2 string length unsigned size = 1 + ((SEGMENT.intensity * SEGLEN) >> 10); uint16_t b = a + size; //"trail" of chase, filled with color1 if (b > SEGLEN) b -= SEGLEN; uint16_t c = b + size; if (c > SEGLEN) c -= SEGLEN; //background if (do_palette) { for (unsigned i = 0; i < SEGLEN; i++) { SEGMENT.setPixelColor(i, SEGMENT.color_from_palette(i, true, PALETTE_SOLID_WRAP, 1)); } } else SEGMENT.fill(color1); //if random, fill old background between a and end if (chase_random) { color1 = SEGMENT.color_wheel(SEGENV.aux1); for (unsigned i = a; i < SEGLEN; i++) SEGMENT.setPixelColor(i, color1); } //fill between points a and b with color2 if (a < b) { for (unsigned i = a; i < b; i++) SEGMENT.setPixelColor(i, color2); } else { for (unsigned i = a; i < SEGLEN; i++) //fill until end SEGMENT.setPixelColor(i, color2); for (unsigned i = 0; i < b; i++) //fill from start until b SEGMENT.setPixelColor(i, color2); } //fill between points b and c with color2 if (b < c) { for (unsigned i = b; i < c; i++) SEGMENT.setPixelColor(i, color3); } else { for (unsigned i = b; i < SEGLEN; i++) //fill until end SEGMENT.setPixelColor(i, color3); for (unsigned i = 0; i < c; i++) //fill from start until c SEGMENT.setPixelColor(i, color3); } } /* * Bicolor chase, more primary color. */ void mode_chase_color(void) { chase(SEGCOLOR(1), (SEGCOLOR(2)) ? SEGCOLOR(2) : SEGCOLOR(0), SEGCOLOR(0), true); } static const char _data_FX_MODE_CHASE_COLOR[] PROGMEM = "Chase@!,Width;!,!,!;!"; /* * Primary running followed by random color. */ void mode_chase_random(void) { chase(SEGCOLOR(1), (SEGCOLOR(2)) ? SEGCOLOR(2) : SEGCOLOR(0), SEGCOLOR(0), false); } static const char _data_FX_MODE_CHASE_RANDOM[] PROGMEM = "Chase Random@!,Width;!,,!;!"; /* * Primary, secondary running on rainbow. */ void mode_chase_rainbow(void) { unsigned color_sep = 256 / SEGLEN; if (color_sep == 0) color_sep = 1; // correction for segments longer than 256 LEDs unsigned color_index = SEGENV.call & 0xFF; uint32_t color = SEGMENT.color_wheel(((SEGENV.step * color_sep) + color_index) & 0xFF); chase(color, SEGCOLOR(0), SEGCOLOR(1), false); } static const char _data_FX_MODE_CHASE_RAINBOW[] PROGMEM = "Chase Rainbow@!,Width;!,!;!"; /* * Primary running on rainbow. */ void mode_chase_rainbow_white(void) { uint16_t n = SEGENV.step; uint16_t m = (SEGENV.step + 1) % SEGLEN; uint32_t color2 = SEGMENT.color_wheel(((n * 256 / SEGLEN) + (SEGENV.call & 0xFF)) & 0xFF); uint32_t color3 = SEGMENT.color_wheel(((m * 256 / SEGLEN) + (SEGENV.call & 0xFF)) & 0xFF); chase(SEGCOLOR(0), color2, color3, false); } static const char _data_FX_MODE_CHASE_RAINBOW_WHITE[] PROGMEM = "Rainbow Runner@!,Size;Bg;!"; /* * Red - Amber - Green - Blue lights running */ void mode_colorful(void) { unsigned numColors = 4; //3, 4, or 5 uint32_t cols[9]{0x00FF0000,0x00EEBB00,0x0000EE00,0x000077CC}; if (SEGMENT.intensity > 160 || SEGMENT.palette) { //palette or color if (!SEGMENT.palette) { numColors = 3; for (size_t i = 0; i < 3; i++) cols[i] = SEGCOLOR(i); } else { unsigned fac = 80; if (SEGMENT.palette == 52) {numColors = 5; fac = 61;} //C9 2 has 5 colors for (size_t i = 0; i < numColors; i++) { cols[i] = SEGMENT.color_from_palette(i*fac, false, true, 255); } } } else if (SEGMENT.intensity < 80) //pastel (easter) colors { cols[0] = 0x00FF8040; cols[1] = 0x00E5D241; cols[2] = 0x0077FF77; cols[3] = 0x0077F0F0; } for (size_t i = numColors; i < numColors*2 -1U; i++) cols[i] = cols[i-numColors]; uint32_t cycleTime = 50 + (8 * (uint32_t)(255 - SEGMENT.speed)); uint32_t it = strip.now / cycleTime; if (it != SEGENV.step) { if (SEGMENT.speed > 0) SEGENV.aux0++; if (SEGENV.aux0 >= numColors) SEGENV.aux0 = 0; SEGENV.step = it; } for (unsigned i = 0; i < SEGLEN; i+= numColors) { for (unsigned j = 0; j < numColors; j++) SEGMENT.setPixelColor(i + j, cols[SEGENV.aux0 + j]); } } static const char _data_FX_MODE_COLORFUL[] PROGMEM = "Colorful@!,Saturation;1,2,3;!"; /* * Emulates a traffic light. */ void mode_traffic_light(void) { if (SEGLEN <= 1) FX_FALLBACK_STATIC; for (unsigned i=0; i < SEGLEN; i++) SEGMENT.setPixelColor(i, SEGMENT.color_from_palette(i, true, PALETTE_SOLID_WRAP, 1)); uint32_t mdelay = 500; for (unsigned i = 0; i < SEGLEN-2 ; i+=3) { switch (SEGENV.aux0) { case 0: SEGMENT.setPixelColor(i, 0x00FF0000); mdelay = 150 + (100 * (uint32_t)(255 - SEGMENT.speed));break; case 1: SEGMENT.setPixelColor(i, 0x00FF0000); mdelay = 150 + (20 * (uint32_t)(255 - SEGMENT.speed)); SEGMENT.setPixelColor(i+1, 0x00EECC00); break; case 2: SEGMENT.setPixelColor(i+2, 0x0000FF00); mdelay = 150 + (100 * (uint32_t)(255 - SEGMENT.speed));break; case 3: SEGMENT.setPixelColor(i+1, 0x00EECC00); mdelay = 150 + (20 * (uint32_t)(255 - SEGMENT.speed));break; } } if (strip.now - SEGENV.step > mdelay) { SEGENV.aux0++; if (SEGENV.aux0 == 1 && SEGMENT.intensity > 140) SEGENV.aux0 = 2; //skip Red + Amber, to get US-style sequence if (SEGENV.aux0 > 3) SEGENV.aux0 = 0; SEGENV.step = strip.now; } } static const char _data_FX_MODE_TRAFFIC_LIGHT[] PROGMEM = "Traffic Light@!,US style;,!;!"; /* * Sec flashes running on prim. */ #define FLASH_COUNT 4 void mode_chase_flash(void) { if (SEGLEN <= 1) FX_FALLBACK_STATIC; unsigned now = strip.now; // save time for delay calculation bool advance = true; unsigned flash_step = SEGENV.aux1 % ((FLASH_COUNT * 2) + 1); if (now < SEGENV.step) advance = false; // limit update rate but render every frame for smooth transitions else SEGENV.aux1++; for (unsigned i = 0; i < SEGLEN; i++) { SEGMENT.setPixelColor(i, SEGMENT.color_from_palette(i, true, PALETTE_SOLID_WRAP, 0)); } unsigned index = SEGENV.aux0; unsigned n = index; unsigned m = (index + 1) % SEGLEN; unsigned delay = 10 + ((30 * (uint16_t)(255 - SEGMENT.speed)) / SEGLEN); if(flash_step < (FLASH_COUNT * 2)) { if(flash_step % 2 == 0) { SEGMENT.setPixelColor( n, SEGCOLOR(1)); SEGMENT.setPixelColor( m, SEGCOLOR(1)); delay = 20; } else { delay = 30; } } else if (advance) { SEGENV.aux0 = m; // advance to next position } if (advance) SEGENV.step = now + delay; // set next update time } static const char _data_FX_MODE_CHASE_FLASH[] PROGMEM = "Chase Flash@!;Bg,Fx;!"; /* * Prim flashes running, followed by random color. */ void mode_chase_flash_random(void) { if (SEGLEN <= 1) FX_FALLBACK_STATIC; unsigned now = strip.now; // save time for delay calculation bool advance = true; if (now < SEGENV.step) { SEGENV.call--; // revert increment to skip moving the animation forward and just render the same frame again advance = false; } unsigned flash_step = SEGENV.call % ((FLASH_COUNT * 2) + 1); for (int i = 0; i < SEGENV.aux1; i++) { SEGMENT.setPixelColor(i, SEGMENT.color_wheel(SEGENV.aux0)); } unsigned delay = 1 + ((10 * (uint16_t)(255 - SEGMENT.speed)) / SEGLEN); if(flash_step < (FLASH_COUNT * 2)) { unsigned n = SEGENV.aux1; unsigned m = (SEGENV.aux1 + 1) % SEGLEN; if(flash_step % 2 == 0) { SEGMENT.setPixelColor( n, SEGCOLOR(0)); SEGMENT.setPixelColor( m, SEGCOLOR(0)); delay = 20; } else { SEGMENT.setPixelColor( n, SEGMENT.color_wheel(SEGENV.aux0)); SEGMENT.setPixelColor( m, SEGCOLOR(1)); delay = 30; } } else if (advance) { SEGENV.aux1 = (SEGENV.aux1 + 1) % SEGLEN; if (SEGENV.aux1 == 0) { SEGENV.aux0 = get_random_wheel_index(SEGENV.aux0); } } if (advance) SEGENV.step = now + delay; // set next update time } static const char _data_FX_MODE_CHASE_FLASH_RANDOM[] PROGMEM = "Chase Flash Rnd@!;!,!;!"; /* * Alternating color/sec pixels running. */ void mode_running_color(void) { running(SEGCOLOR(0), SEGCOLOR(1)); } static const char _data_FX_MODE_RUNNING_COLOR[] PROGMEM = "Chase 2@!,Width;!,!;!"; /* * Random colored pixels running. ("Stream") */ void mode_running_random(void) { uint32_t cycleTime = 25 + (3 * (uint32_t)(255 - SEGMENT.speed)); uint32_t it = strip.now / cycleTime; if (SEGENV.call == 0) SEGENV.aux0 = hw_random(); // random seed for PRNG on start unsigned zoneSize = ((255-SEGMENT.intensity) >> 4) +1; uint16_t PRNG16 = SEGENV.aux0; unsigned z = it % zoneSize; bool nzone = (!z && it != SEGENV.aux1); for (int i=SEGLEN-1; i >= 0; i--) { if (nzone || z >= zoneSize) { unsigned lastrand = PRNG16 >> 8; int16_t diff = 0; while (abs(diff) < 42) { // make sure the difference between adjacent colors is big enough PRNG16 = (uint16_t)(PRNG16 * 2053) + 13849; // next zone, next 'random' number diff = (PRNG16 >> 8) - lastrand; } if (nzone) { SEGENV.aux0 = PRNG16; // save next starting seed nzone = false; } z = 0; } SEGMENT.setPixelColor(i, SEGMENT.color_wheel(PRNG16 >> 8)); z++; } SEGENV.aux1 = it; } static const char _data_FX_MODE_RUNNING_RANDOM[] PROGMEM = "Stream@!,Zone size;;!"; /* * K.I.T.T. */ void mode_larson_scanner(void) { if (SEGLEN <= 1) FX_FALLBACK_STATIC; const unsigned speed = FRAMETIME * map(SEGMENT.speed, 0, 255, 96, 2); // map into useful range const unsigned pixels = SEGLEN / speed; // how many pixels to advance per frame SEGMENT.fade_out(255-SEGMENT.intensity); if (SEGENV.step > strip.now) return; // we have a pause unsigned index = SEGENV.aux1 + pixels; // are we slow enough to use frames per pixel? if (pixels == 0) { const unsigned frames = speed / SEGLEN; // how many frames per 1 pixel if (SEGENV.step++ < frames) return; SEGENV.step = 0; index++; } if (index > SEGLEN) { SEGENV.aux0 = !SEGENV.aux0; // change direction SEGENV.aux1 = 0; // reset position // set delay if (SEGENV.aux0 || SEGMENT.check2) SEGENV.step = strip.now + SEGMENT.custom1 * 25; // multiply by 25ms else SEGENV.step = 0; } else { // paint as many pixels as needed for (unsigned i = SEGENV.aux1; i < index; i++) { unsigned j = (SEGENV.aux0) ? i : SEGLEN - 1 - i; uint32_t c = SEGMENT.color_from_palette(j, true, PALETTE_SOLID_WRAP, 0); SEGMENT.setPixelColor(j, c); if (SEGMENT.check1) { SEGMENT.setPixelColor(SEGLEN - 1 - j, SEGCOLOR(2) ? SEGCOLOR(2) : c); } } SEGENV.aux1 = index; } } static const char _data_FX_MODE_LARSON_SCANNER[] PROGMEM = "Scanner@!,Trail,Delay,,,Dual,Bi-delay;!,!,!;!;;m12=0,c1=0"; /* * Creates two Larson scanners moving in opposite directions * Custom mode by Keith Lord: https://github.com/kitesurfer1404/WS2812FX/blob/master/src/custom/DualLarson.h */ void mode_dual_larson_scanner(void){ SEGMENT.check1 = true; mode_larson_scanner(); } static const char _data_FX_MODE_DUAL_LARSON_SCANNER[] PROGMEM = "Scanner Dual@!,Trail,Delay,,,Dual,Bi-delay;!,!,!;!;;m12=0,c1=0"; /* * Firing comets from one end. "Lighthouse" */ void mode_comet(void) { if (SEGLEN <= 1) FX_FALLBACK_STATIC; unsigned counter = (strip.now * ((SEGMENT.speed >>2) +1)) & 0xFFFF; unsigned index = (counter * SEGLEN) >> 16; if (SEGENV.call == 0) SEGENV.aux0 = index; SEGMENT.fade_out(SEGMENT.intensity); SEGMENT.setPixelColor( index, SEGMENT.color_from_palette(index, true, PALETTE_SOLID_WRAP, 0)); if (index > SEGENV.aux0) { for (unsigned i = SEGENV.aux0; i < index ; i++) { SEGMENT.setPixelColor( i, SEGMENT.color_from_palette(i, true, PALETTE_SOLID_WRAP, 0)); } } else if (index < SEGENV.aux0 && index < 10) { for (unsigned i = 0; i < index ; i++) { SEGMENT.setPixelColor( i, SEGMENT.color_from_palette(i, true, PALETTE_SOLID_WRAP, 0)); } } SEGENV.aux0 = index++; } static const char _data_FX_MODE_COMET[] PROGMEM = "Lighthouse@!,Fade rate;!,!;!"; /* * Fireworks function. */ void mode_fireworks() { if (SEGLEN <= 1) FX_FALLBACK_STATIC; const uint16_t width = SEGMENT.is2D() ? SEG_W : SEGLEN; const uint16_t height = SEG_H; if (SEGENV.call == 0) { SEGENV.aux0 = UINT16_MAX; SEGENV.aux1 = UINT16_MAX; } SEGMENT.fade_out(128); uint8_t x = SEGENV.aux0%width, y = SEGENV.aux0/width; // 2D coordinates stored in upper and lower byte if (!SEGENV.step) { // fireworks mode (blur flares) bool valid1 = (SEGENV.aux0 < width*height); bool valid2 = (SEGENV.aux1 < width*height); uint32_t sv1 = 0, sv2 = 0; if (valid1) sv1 = SEGMENT.is2D() ? SEGMENT.getPixelColorXY(x, y) : SEGMENT.getPixelColor(SEGENV.aux0); // get spark color if (valid2) sv2 = SEGMENT.is2D() ? SEGMENT.getPixelColorXY(x, y) : SEGMENT.getPixelColor(SEGENV.aux1); SEGMENT.blur(16); // used in mode_rain() if (valid1) { if (SEGMENT.is2D()) SEGMENT.setPixelColorXY(x, y, sv1); else SEGMENT.setPixelColor(SEGENV.aux0, sv1); } // restore spark color after blur if (valid2) { if (SEGMENT.is2D()) SEGMENT.setPixelColorXY(x, y, sv2); else SEGMENT.setPixelColor(SEGENV.aux1, sv2); } // restore old spark color after blur } for (int i=0; i> 1)) == 0) { uint16_t index = hw_random16(width*height); x = index % width; y = index / width; uint32_t col = SEGMENT.color_from_palette(hw_random8(), false, false, 0); if (SEGMENT.is2D()) SEGMENT.setPixelColorXY(x, y, col); else SEGMENT.setPixelColor(index, col); SEGENV.aux1 = SEGENV.aux0; // old spark SEGENV.aux0 = index; // remember where spark occurred } } } static const char _data_FX_MODE_FIREWORKS[] PROGMEM = "Fireworks@,Frequency;!,!;!;12;ix=192,pal=11"; //Twinkling LEDs running. Inspired by https://github.com/kitesurfer1404/WS2812FX/blob/master/src/custom/Rain.h void mode_rain() { if (SEGLEN <= 1) FX_FALLBACK_STATIC; const unsigned width = SEG_W; const unsigned height = SEG_H; SEGENV.step += FRAMETIME; if (SEGENV.call && SEGENV.step > SPEED_FORMULA_L) { SEGENV.step = 1; if (SEGMENT.is2D()) { //uint32_t ctemp[width]; //for (int i = 0; i= width*height) SEGENV.aux0 = 0; // ignore if (SEGENV.aux1 >= width*height) SEGENV.aux1 = 0; } mode_fireworks(); } static const char _data_FX_MODE_RAIN[] PROGMEM = "Rain@!,Spawning rate;!,!;!;12;ix=128,pal=0"; /* * Fire flicker function */ void mode_fire_flicker(void) { uint32_t cycleTime = 40 + (255 - SEGMENT.speed); uint32_t it = strip.now / cycleTime; if (SEGENV.step == it) return; byte w = (SEGCOLOR(0) >> 24); byte r = (SEGCOLOR(0) >> 16); byte g = (SEGCOLOR(0) >> 8); byte b = (SEGCOLOR(0) ); byte lum = (SEGMENT.palette == 0) ? MAX(w, MAX(r, MAX(g, b))) : 255; lum /= (((256-SEGMENT.intensity)/16)+1); for (unsigned i = 0; i < SEGLEN; i++) { byte flicker = hw_random8(lum); if (SEGMENT.palette == 0) { SEGMENT.setPixelColor(i, MAX(r - flicker, 0), MAX(g - flicker, 0), MAX(b - flicker, 0), MAX(w - flicker, 0)); } else { SEGMENT.setPixelColor(i, SEGMENT.color_from_palette(i, true, PALETTE_SOLID_WRAP, 0, 255 - flicker)); } } SEGENV.step = it; } static const char _data_FX_MODE_FIRE_FLICKER[] PROGMEM = "Fire Flicker@!,!;!;!;01"; /* * Gradient run base function */ void gradient_base(bool loading) { if (SEGLEN <= 1) FX_FALLBACK_STATIC; uint16_t counter = strip.now * ((SEGMENT.speed >> 2) + 1); uint16_t pp = (counter * SEGLEN) >> 16; if (SEGENV.call == 0) pp = 0; int val; //0 = sec 1 = pri int brd = 1 + loading ? SEGMENT.intensity/2 : SEGMENT.intensity/4; //if (brd < 1) brd = 1; int p1 = pp-SEGLEN; int p2 = pp+SEGLEN; for (int i = 0; i < (int)SEGLEN; i++) { if (loading) { val = abs(((i>pp) ? p2:pp) - i); } else { val = min(abs(pp-i),min(abs(p1-i),abs(p2-i))); } val = (brd > val) ? (val * 255) / brd : 255; SEGMENT.setPixelColor(i, color_blend(SEGCOLOR(0), SEGMENT.color_from_palette(i, true, PALETTE_SOLID_WRAP, 1), uint8_t(val))); } } /* * Gradient run */ void mode_gradient(void) { gradient_base(false); } static const char _data_FX_MODE_GRADIENT[] PROGMEM = "Gradient@!,Spread;!,!;!;;ix=16"; /* * Gradient run with hard transition */ void mode_loading(void) { gradient_base(true); } static const char _data_FX_MODE_LOADING[] PROGMEM = "Loading@!,Fade;!,!;!;;ix=16"; /* * Two dots running */ void mode_two_dots() { if (SEGLEN <= 1) FX_FALLBACK_STATIC; unsigned delay = 1 + (FRAMETIME<<3) / SEGLEN; // longer segments should change faster uint32_t it = strip.now / map(SEGMENT.speed, 0, 255, delay<<4, delay); unsigned offset = it % SEGLEN; unsigned width = ((SEGLEN*(SEGMENT.intensity+1))>>9); //max width is half the strip if (!width) width = 1; if (!SEGMENT.check2) SEGMENT.fill(SEGCOLOR(2)); const uint32_t color1 = SEGCOLOR(0); const uint32_t color2 = (SEGCOLOR(1) == SEGCOLOR(2)) ? color1 : SEGCOLOR(1); for (unsigned i = 0; i < width; i++) { unsigned indexR = (offset + i) % SEGLEN; unsigned indexB = (offset + i + (SEGLEN>>1)) % SEGLEN; SEGMENT.setPixelColor(indexR, color1); SEGMENT.setPixelColor(indexB, color2); } } static const char _data_FX_MODE_TWO_DOTS[] PROGMEM = "Two Dots@!,Dot size,,,,,Overlay;1,2,Bg;!"; /* * Fairy, inspired by https://www.youtube.com/watch?v=zeOw5MZWq24 */ //4 bytes typedef struct Flasher { uint16_t stateStart; uint8_t stateDur; bool stateOn; } flasher; #define FLASHERS_PER_ZONE 6 #define MAX_SHIMMER 92 void mode_fairy() { //set every pixel to a 'random' color from palette (using seed so it doesn't change between frames) uint16_t PRNG16 = 5100 + strip.getCurrSegmentId(); for (unsigned i = 0; i < SEGLEN; i++) { PRNG16 = (uint16_t)(PRNG16 * 2053) + 1384; //next 'random' number SEGMENT.setPixelColor(i, SEGMENT.color_from_palette(PRNG16 >> 8, false, false, 0)); } //amount of flasher pixels depending on intensity (0: none, 255: every LED) if (SEGMENT.intensity == 0) return; unsigned flasherDistance = ((255 - SEGMENT.intensity) / 28) +1; //1-10 unsigned numFlashers = (SEGLEN / flasherDistance) +1; unsigned dataSize = sizeof(flasher) * numFlashers; if (!SEGENV.allocateData(dataSize)) return; //allocation failed Flasher* flashers = reinterpret_cast(SEGENV.data); unsigned now16 = strip.now & 0xFFFF; //Up to 11 flashers in one brightness zone, afterwards a new zone for every 6 flashers unsigned zones = numFlashers/FLASHERS_PER_ZONE; if (!zones) zones = 1; unsigned flashersInZone = numFlashers/zones; uint8_t flasherBri[FLASHERS_PER_ZONE*2 -1]; for (unsigned z = 0; z < zones; z++) { unsigned flasherBriSum = 0; unsigned firstFlasher = z*flashersInZone; if (z == zones-1) flashersInZone = numFlashers-(flashersInZone*(zones-1)); for (unsigned f = firstFlasher; f < firstFlasher + flashersInZone; f++) { unsigned stateTime = uint16_t(now16 - flashers[f].stateStart); //random on/off time reached, switch state if (stateTime > flashers[f].stateDur * 10) { flashers[f].stateOn = !flashers[f].stateOn; if (flashers[f].stateOn) { flashers[f].stateDur = 12 + hw_random8(12 + ((255 - SEGMENT.speed) >> 2)); //*10, 250ms to 1250ms } else { flashers[f].stateDur = 20 + hw_random8(6 + ((255 - SEGMENT.speed) >> 2)); //*10, 250ms to 1250ms } //flashers[f].stateDur = 51 + hw_random8(2 + ((255 - SEGMENT.speed) >> 1)); flashers[f].stateStart = now16; if (stateTime < 255) { flashers[f].stateStart -= 255 -stateTime; //start early to get correct bri flashers[f].stateDur += 26 - stateTime/10; stateTime = 255 - stateTime; } else { stateTime = 0; } } if (stateTime > 255) stateTime = 255; //for flasher brightness calculation, fades in first 255 ms of state //flasherBri[f - firstFlasher] = (flashers[f].stateOn) ? 255-SEGMENT.gamma8((510 - stateTime) >> 1) : SEGMENT.gamma8((510 - stateTime) >> 1); flasherBri[f - firstFlasher] = (flashers[f].stateOn) ? stateTime : 255 - (stateTime >> 0); flasherBriSum += flasherBri[f - firstFlasher]; } //dim factor, to create "shimmer" as other pixels get less voltage if a lot of flashers are on unsigned avgFlasherBri = flasherBriSum / flashersInZone; unsigned globalPeakBri = 255 - ((avgFlasherBri * MAX_SHIMMER) >> 8); //183-255, suitable for 1/5th of LEDs flashers for (unsigned f = firstFlasher; f < firstFlasher + flashersInZone; f++) { uint8_t bri = (flasherBri[f - firstFlasher] * globalPeakBri) / 255; PRNG16 = (uint16_t)(PRNG16 * 2053) + 1384; //next 'random' number unsigned flasherPos = f*flasherDistance; SEGMENT.setPixelColor(flasherPos, color_blend(SEGCOLOR(1), SEGMENT.color_from_palette(PRNG16 >> 8, false, false, 0), bri)); for (unsigned i = flasherPos+1; i < flasherPos+flasherDistance && i < SEGLEN; i++) { PRNG16 = (uint16_t)(PRNG16 * 2053) + 1384; //next 'random' number SEGMENT.setPixelColor(i, SEGMENT.color_from_palette(PRNG16 >> 8, false, false, 0, globalPeakBri)); } } } } static const char _data_FX_MODE_FAIRY[] PROGMEM = "Fairy@!,# of flashers;!,!;!"; /* * Fairytwinkle. Like Colortwinkle, but starting from all lit and not relying on strip.getPixelColor * Warning: Uses 4 bytes of segment data per pixel */ void mode_fairytwinkle() { unsigned dataSize = sizeof(flasher) * SEGLEN; if (!SEGENV.allocateData(dataSize)) FX_FALLBACK_STATIC; //allocation failed Flasher* flashers = reinterpret_cast(SEGENV.data); unsigned now16 = strip.now & 0xFFFF; uint16_t PRNG16 = 5100 + strip.getCurrSegmentId(); unsigned riseFallTime = 400 + (255-SEGMENT.speed)*3; unsigned maxDur = riseFallTime/100 + ((255 - SEGMENT.intensity) >> 2) + 13 + ((255 - SEGMENT.intensity) >> 1); for (unsigned f = 0; f < SEGLEN; f++) { uint16_t stateTime = now16 - flashers[f].stateStart; //random on/off time reached, switch state if (stateTime > flashers[f].stateDur * 100) { flashers[f].stateOn = !flashers[f].stateOn; bool init = !flashers[f].stateDur; if (flashers[f].stateOn) { flashers[f].stateDur = riseFallTime/100 + ((255 - SEGMENT.intensity) >> 2) + hw_random8(12 + ((255 - SEGMENT.intensity) >> 1)) +1; } else { flashers[f].stateDur = riseFallTime/100 + hw_random8(3 + ((255 - SEGMENT.speed) >> 6)) +1; } flashers[f].stateStart = now16; stateTime = 0; if (init) { flashers[f].stateStart -= riseFallTime; //start lit flashers[f].stateDur = riseFallTime/100 + hw_random8(12 + ((255 - SEGMENT.intensity) >> 1)) +5; //fire up a little quicker stateTime = riseFallTime; } } if (flashers[f].stateOn && flashers[f].stateDur > maxDur) flashers[f].stateDur = maxDur; //react more quickly on intensity change if (stateTime > riseFallTime) stateTime = riseFallTime; //for flasher brightness calculation, fades in first 255 ms of state unsigned fadeprog = 255 - ((stateTime * 255) / riseFallTime); uint8_t flasherBri = (flashers[f].stateOn) ? 255-gamma8(fadeprog) : gamma8(fadeprog); unsigned lastR = PRNG16; unsigned diff = 0; while (diff < 0x4000) { //make sure colors of two adjacent LEDs differ enough PRNG16 = (uint16_t)(PRNG16 * 2053) + 1384; //next 'random' number diff = (PRNG16 > lastR) ? PRNG16 - lastR : lastR - PRNG16; } SEGMENT.setPixelColor(f, color_blend(SEGCOLOR(1), SEGMENT.color_from_palette(PRNG16 >> 8, false, false, 0), flasherBri)); } } static const char _data_FX_MODE_FAIRYTWINKLE[] PROGMEM = "Fairytwinkle@!,!;!,!;!;;m12=0"; //pixels /* * Tricolor chase function */ void tricolor_chase(uint32_t color1, uint32_t color2) { uint32_t cycleTime = 50 + ((255 - SEGMENT.speed)<<1); uint32_t it = strip.now / cycleTime; // iterator unsigned width = (1 + (SEGMENT.intensity>>4)); // value of 1-16 for each colour unsigned index = it % (width*3); for (unsigned i = 0; i < SEGLEN; i++, index++) { if (index > (width*3)-1) index = 0; uint32_t color = color1; if (index > (width<<1)-1) color = SEGMENT.color_from_palette(i, true, PALETTE_SOLID_WRAP, 1); else if (index > width-1) color = color2; SEGMENT.setPixelColor(SEGLEN - i -1, color); } } /* * Tricolor chase mode */ void mode_tricolor_chase(void) { tricolor_chase(SEGCOLOR(2), SEGCOLOR(0)); } static const char _data_FX_MODE_TRICOLOR_CHASE[] PROGMEM = "Chase 3@!,Size;1,2,3;!"; /* * ICU mode */ void mode_icu(void) { // states: 0 = pause1, 1 = blink, 2 = pause2, 3 = move uint16_t now = strip.now; // save time for delay calculation, use low16 bits only unsigned dest = SEGENV.aux1; unsigned space = (SEGMENT.intensity >> 3) +2; uint16_t state = SEGENV.step >> 16; // upper bytes of step store current state uint16_t nextUpdate = SEGENV.step & 0xFFFF; // lower bytes store time for next update byte pindex = map(dest, 0, SEGLEN-SEGLEN/space, 0, 255); uint32_t col = SEGMENT.color_from_palette(pindex, false, false, 0); uint32_t bgcol = SEGMENT.check2 ? BLACK : SEGCOLOR(1); SEGMENT.fill(bgcol); // apply background color or clear // draw eyes if not blinking if (state != 1) { SEGMENT.setPixelColor(dest, col); SEGMENT.setPixelColor(dest + SEGLEN/space, col); // render next position if moving if (state == 3) { if(SEGENV.aux0 > SEGENV.aux1) { dest++; } else if (SEGENV.aux0 < SEGENV.aux1) { dest--; } SEGMENT.setPixelColor(dest, col); SEGMENT.setPixelColor(dest + SEGLEN/space, col); } } // update state if ((int16_t)(now - nextUpdate) >= 0) { // time to update, cast to int to handle wraparound properly switch (state) { case 0: // pause part 1 // first pause part finished, blink or pause some more state++; if(hw_random8(6) == 0) { // blink once in a while nextUpdate = uint16_t(now + 200); break; } // fall through if not blinking case 1: // blink // not blinking or finished blinking -> pause part 2 nextUpdate = uint16_t(now + 500 + hw_random16(1000)); state++; break; case 2: // pause part 2 // pause finished, move SEGENV.aux0 = hw_random16(SEGLEN-SEGLEN/space); // choose a new destination nextUpdate = now; state++; break; default: // move (state 3) SEGENV.aux1 = dest; // update destination to moved position nextUpdate = uint16_t(now + SPEED_FORMULA_L); if (SEGENV.aux0 == dest) { // reached destination nextUpdate = uint16_t(now + 500 + hw_random16(1000)); state = 0; } break; } } // use upper bits of SEGENV.step to store current state, lower bits for next update time SEGENV.step = (state << 16) | nextUpdate; } static const char _data_FX_MODE_ICU[] PROGMEM = "ICU@!,!,,,,,Overlay;!,!;!"; /* * Custom mode by Aircoookie. Color Wipe, but with 3 colors */ void mode_tricolor_wipe(void) { uint32_t cycleTime = 1000 + (255 - SEGMENT.speed)*200; uint32_t perc = strip.now % cycleTime; unsigned prog = (perc * 65535) / cycleTime; unsigned ledIndex = (prog * SEGLEN * 3) >> 16; unsigned ledOffset = ledIndex; for (unsigned i = 0; i < SEGLEN; i++) { SEGMENT.setPixelColor(i, SEGMENT.color_from_palette(i, true, PALETTE_SOLID_WRAP, 2)); } if(ledIndex < SEGLEN) { //wipe from 0 to 1 for (unsigned i = 0; i < SEGLEN; i++) { SEGMENT.setPixelColor(i, (i > ledOffset)? SEGCOLOR(0) : SEGCOLOR(1)); } } else if (ledIndex < SEGLEN*2) { //wipe from 1 to 2 ledOffset = ledIndex - SEGLEN; for (unsigned i = ledOffset +1; i < SEGLEN; i++) { SEGMENT.setPixelColor(i, SEGCOLOR(1)); } } else //wipe from 2 to 0 { ledOffset = ledIndex - SEGLEN*2; for (unsigned i = 0; i <= ledOffset; i++) { SEGMENT.setPixelColor(i, SEGCOLOR(0)); } } } static const char _data_FX_MODE_TRICOLOR_WIPE[] PROGMEM = "Tri Wipe@!;1,2,3;!"; /* * Fades between 3 colors * Custom mode by Keith Lord: https://github.com/kitesurfer1404/WS2812FX/blob/master/src/custom/TriFade.h * Modified by Aircoookie */ void mode_tricolor_fade(void) { uint16_t counter = strip.now * ((SEGMENT.speed >> 3) +1); uint32_t prog = (counter * 768) >> 16; uint32_t color1 = 0, color2 = 0; unsigned stage = 0; if(prog < 256) { color1 = SEGCOLOR(0); color2 = SEGCOLOR(1); stage = 0; } else if(prog < 512) { color1 = SEGCOLOR(1); color2 = SEGCOLOR(2); stage = 1; } else { color1 = SEGCOLOR(2); color2 = SEGCOLOR(0); stage = 2; } byte stp = prog; // % 256 for (unsigned i = 0; i < SEGLEN; i++) { uint32_t color; if (stage == 2) { color = color_blend(SEGMENT.color_from_palette(i, true, PALETTE_SOLID_WRAP, 2), color2, stp); } else if (stage == 1) { color = color_blend(color1, SEGMENT.color_from_palette(i, true, PALETTE_SOLID_WRAP, 2), stp); } else { color = color_blend(color1, color2, stp); } SEGMENT.setPixelColor(i, color); } } static const char _data_FX_MODE_TRICOLOR_FADE[] PROGMEM = "Tri Fade@!;1,2,3;!"; /* * Creates random comets * Custom mode by Keith Lord: https://github.com/kitesurfer1404/WS2812FX/blob/master/src/custom/MultiComet.h */ #define MAX_COMETS 8 void mode_multi_comet(void) { uint32_t cycleTime = 10 + (uint32_t)(255 - SEGMENT.speed); uint32_t it = strip.now / cycleTime; if (SEGENV.step == it) return; if (!SEGENV.allocateData(sizeof(uint16_t) * MAX_COMETS)) FX_FALLBACK_STATIC; //allocation failed SEGMENT.fade_out(SEGMENT.intensity/2 + 128); uint16_t* comets = reinterpret_cast(SEGENV.data); for (unsigned i=0; i < MAX_COMETS; i++) { if(comets[i] < SEGLEN) { unsigned index = comets[i]; if (SEGCOLOR(2) != 0) { SEGMENT.setPixelColor(index, i % 2 ? SEGMENT.color_from_palette(index, true, PALETTE_SOLID_WRAP, 0) : SEGCOLOR(2)); } else { SEGMENT.setPixelColor(index, SEGMENT.color_from_palette(index, true, PALETTE_SOLID_WRAP, 0)); } comets[i]++; } else { if(!hw_random16(SEGLEN)) { comets[i] = 0; } } } SEGENV.step = it; } static const char _data_FX_MODE_MULTI_COMET[] PROGMEM = "Multi Comet@!,Fade;!,!;!;1"; #undef MAX_COMETS /* * Running random pixels ("Stream 2") * Custom mode by Keith Lord: https://github.com/kitesurfer1404/WS2812FX/blob/master/src/custom/RandomChase.h */ void mode_random_chase(void) { if (SEGENV.call == 0) { SEGENV.step = RGBW32(random8(), random8(), random8(), 0); SEGENV.aux0 = random16(); } unsigned prevSeed = random16_get_seed(); // save seed so we can restore it at the end of the function uint32_t cycleTime = 25 + (3 * (uint32_t)(255 - SEGMENT.speed)); uint32_t it = strip.now / cycleTime; uint32_t color = SEGENV.step; random16_set_seed(SEGENV.aux0); for (int i = SEGLEN -1; i >= 0; i--) { uint8_t r = random8(6) != 0 ? (color >> 16 & 0xFF) : random8(); uint8_t g = random8(6) != 0 ? (color >> 8 & 0xFF) : random8(); uint8_t b = random8(6) != 0 ? (color & 0xFF) : random8(); color = RGBW32(r, g, b, 0); SEGMENT.setPixelColor(i, color); if (i == SEGLEN -1U && SEGENV.aux1 != (it & 0xFFFFU)) { //new first color in next frame SEGENV.step = color; SEGENV.aux0 = random16_get_seed(); } } SEGENV.aux1 = it & 0xFFFF; random16_set_seed(prevSeed); // restore original seed so other effects can use "random" PRNG } static const char _data_FX_MODE_RANDOM_CHASE[] PROGMEM = "Stream 2@!;;"; //7 bytes typedef struct Oscillator { uint16_t pos; uint8_t size; int8_t dir; uint8_t speed; } oscillator; /* / Oscillating bars of color, updated with standard framerate */ void mode_oscillate(void) { constexpr unsigned numOscillators = 3; constexpr unsigned dataSize = sizeof(oscillator) * numOscillators; if (!SEGENV.allocateData(dataSize)) FX_FALLBACK_STATIC; //allocation failed Oscillator* oscillators = reinterpret_cast(SEGENV.data); if (SEGENV.call == 0) { oscillators[0] = {(uint16_t)(SEGLEN/4), (uint8_t)(SEGLEN/8), 1, 1}; oscillators[1] = {(uint16_t)(SEGLEN/4*3), (uint8_t)(SEGLEN/8), 1, 2}; oscillators[2] = {(uint16_t)(SEGLEN/4*2), (uint8_t)(SEGLEN/8), -1, 1}; } uint32_t cycleTime = 20 + (2 * (uint32_t)(255 - SEGMENT.speed)); uint32_t it = strip.now / cycleTime; for (unsigned i = 0; i < numOscillators; i++) { // if the counter has increased, move the oscillator by the random step if (it != SEGENV.step) oscillators[i].pos += oscillators[i].dir * oscillators[i].speed; oscillators[i].size = SEGLEN/(3+SEGMENT.intensity/8); if((oscillators[i].dir == -1) && (oscillators[i].pos > SEGLEN << 1)) { // use integer overflow oscillators[i].pos = 0; oscillators[i].dir = 1; // make bigger steps for faster speeds oscillators[i].speed = SEGMENT.speed > 100 ? hw_random8(2, 4):hw_random8(1, 3); } if((oscillators[i].dir == 1) && (oscillators[i].pos >= (SEGLEN - 1))) { oscillators[i].pos = SEGLEN - 1; oscillators[i].dir = -1; oscillators[i].speed = SEGMENT.speed > 100 ? hw_random8(2, 4):hw_random8(1, 3); } } for (unsigned i = 0; i < SEGLEN; i++) { uint32_t color = BLACK; for (unsigned j = 0; j < numOscillators; j++) { if((int)i >= (int)oscillators[j].pos - oscillators[j].size && i <= oscillators[j].pos + oscillators[j].size) { color = (color == BLACK) ? SEGCOLOR(j) : color_blend(color, SEGCOLOR(j), uint8_t(128)); } } SEGMENT.setPixelColor(i, color); } SEGENV.step = it; } static const char _data_FX_MODE_OSCILLATE[] PROGMEM = "Oscillate"; void mode_lightning(void) { if (SEGLEN <= 1) FX_FALLBACK_STATIC; unsigned ledstart = hw_random16(SEGLEN); // Determine starting location of flash unsigned ledlen = 1 + hw_random16(SEGLEN -ledstart); // Determine length of flash (not to go beyond NUM_LEDS-1) uint8_t bri = 255/hw_random8(1, 3); if (SEGENV.aux1 == 0) //init, leader flash { SEGENV.aux1 = hw_random8(4, 4 + SEGMENT.intensity/20); //number of flashes SEGENV.aux1 *= 2; bri = 52; //leader has lower brightness SEGENV.aux0 = 200; //200ms delay after leader } if (!SEGMENT.check2) SEGMENT.fill(SEGCOLOR(1)); if (SEGENV.aux1 > 3 && !(SEGENV.aux1 & 0x01)) { //flash on even number >2 for (unsigned i = ledstart; i < ledstart + ledlen; i++) { SEGMENT.setPixelColor(i,SEGMENT.color_from_palette(i, true, PALETTE_SOLID_WRAP, 0, bri)); } SEGENV.aux1--; SEGENV.step = strip.now; //return hw_random8(4, 10); // each flash only lasts one frame/every 24ms... originally 4-10 milliseconds } else { if (strip.now - SEGENV.step > SEGENV.aux0) { SEGENV.aux1--; if (SEGENV.aux1 < 2) SEGENV.aux1 = 0; SEGENV.aux0 = (50 + hw_random8(100)); //delay between flashes if (SEGENV.aux1 == 2) { SEGENV.aux0 = (hw_random8(255 - SEGMENT.speed) * 100); // delay between strikes } SEGENV.step = strip.now; } } } static const char _data_FX_MODE_LIGHTNING[] PROGMEM = "Lightning@!,!,,,,,Overlay;!,!;!"; // combined function from original pride and colorwaves void mode_colorwaves_pride_base(bool isPride2015) { unsigned duration = 10 + SEGMENT.speed; unsigned sPseudotime = SEGENV.step; unsigned sHue16 = SEGENV.aux0; uint8_t sat8 = isPride2015 ? beatsin88_t(87, 220, 250) : 255; unsigned brightdepth = beatsin88_t(341, 96, 224); unsigned brightnessthetainc16 = beatsin88_t(203, (25 * 256), (40 * 256)); unsigned msmultiplier = beatsin88_t(147, 23, 60); unsigned hue16 = sHue16; unsigned hueinc16 = isPride2015 ? beatsin88_t(113, 1, 3000) : beatsin88_t(113, 60, 300) * SEGMENT.intensity * 10 / 255; sPseudotime += duration * msmultiplier; sHue16 += duration * beatsin88_t(400, 5, 9); unsigned brightnesstheta16 = sPseudotime; for (unsigned i = 0; i < SEGLEN; i++) { hue16 += hueinc16; uint8_t hue8; if (isPride2015) { hue8 = hue16 >> 8; } else { unsigned h16_128 = hue16 >> 7; hue8 = (h16_128 & 0x100) ? (255 - (h16_128 >> 1)) : (h16_128 >> 1); } brightnesstheta16 += brightnessthetainc16; unsigned b16 = sin16_t(brightnesstheta16) + 32768; unsigned bri16 = (uint32_t)((uint32_t)b16 * (uint32_t)b16) / 65536; uint8_t bri8 = (uint32_t)(((uint32_t)bri16) * brightdepth) / 65536; bri8 += (255 - brightdepth); if (isPride2015) { CRGBW newcolor = CRGB(CHSV(hue8, sat8, bri8)); newcolor.color32 = gamma32inv(newcolor.color32); SEGMENT.blendPixelColor(i, newcolor, 64); } else { SEGMENT.blendPixelColor(i, SEGMENT.color_from_palette(hue8, false, PALETTE_SOLID_WRAP, 0, bri8), 128); } } SEGENV.step = sPseudotime; SEGENV.aux0 = sHue16; } // Pride2015 // Animated, ever-changing rainbows. // by Mark Kriegsman: https://gist.github.com/kriegsman/964de772d64c502760e5 void mode_pride_2015(void) { mode_colorwaves_pride_base(true); } static const char _data_FX_MODE_PRIDE_2015[] PROGMEM = "Pride 2015@!;;"; // ColorWavesWithPalettes by Mark Kriegsman: https://gist.github.com/kriegsman/8281905786e8b2632aeb // This function draws color waves with an ever-changing, // widely-varying set of parameters, using a color palette. void mode_colorwaves() { mode_colorwaves_pride_base(false); } static const char _data_FX_MODE_COLORWAVES[] PROGMEM = "Colorwaves@!,Hue;!;!;;pal=26"; //eight colored dots, weaving in and out of sync with each other void mode_juggle(void) { if (SEGLEN <= 1) FX_FALLBACK_STATIC; SEGMENT.fadeToBlackBy(192 - (3*SEGMENT.intensity/4)); CRGB fastled_col; byte dothue = 0; for (int i = 0; i < 8; i++) { int index = 0 + beatsin88_t((16 + SEGMENT.speed)*(i + 7), 0, SEGLEN -1); fastled_col = CRGB(SEGMENT.getPixelColor(index)); fastled_col |= (SEGMENT.palette==0)?CHSV(dothue, 220, 255):CRGB(ColorFromPalette(SEGPALETTE, dothue, 255)); SEGMENT.setPixelColor(index, fastled_col); dothue += 32; } } static const char _data_FX_MODE_JUGGLE[] PROGMEM = "Juggle@!,Trail;;!;;sx=64,ix=128"; void mode_palette() { // Set up some compile time constants so that we can handle integer and float based modes using the same code base. #ifdef ESP8266 using mathType = int32_t; using wideMathType = int64_t; using angleType = unsigned; constexpr mathType sInt16Scale = 0x7FFF; constexpr mathType maxAngle = 0x8000; constexpr mathType staticRotationScale = 256; constexpr mathType animatedRotationScale = 1; constexpr int16_t (*sinFunction)(uint16_t) = &sin16_t; constexpr int16_t (*cosFunction)(uint16_t) = &cos16_t; #else using mathType = float; using wideMathType = float; using angleType = float; constexpr mathType sInt16Scale = 1.0f; constexpr mathType maxAngle = M_PI / 256.0; constexpr mathType staticRotationScale = 1.0f; constexpr mathType animatedRotationScale = M_TWOPI / double(0xFFFF); constexpr float (*sinFunction)(float) = &sin_t; constexpr float (*cosFunction)(float) = &cos_t; #endif const bool isMatrix = strip.isMatrix; const int cols = SEG_W; const int rows = isMatrix ? SEG_H : strip.getActiveSegmentsNum(); const int inputShift = SEGMENT.speed; const int inputSize = SEGMENT.intensity; const int inputRotation = SEGMENT.custom1; const bool inputAnimateShift = SEGMENT.check1; const bool inputAnimateRotation = SEGMENT.check2; const bool inputAssumeSquare = SEGMENT.check3; const angleType theta = (!inputAnimateRotation) ? ((inputRotation + 128) * maxAngle / staticRotationScale) : (((strip.now * ((inputRotation >> 4) +1)) & 0xFFFF) * animatedRotationScale); const mathType sinTheta = sinFunction(theta); const mathType cosTheta = cosFunction(theta); const mathType maxX = std::max(1, cols-1); const mathType maxY = std::max(1, rows-1); // Set up some parameters according to inputAssumeSquare, so that we can handle anamorphic mode using the same code base. const mathType maxXIn = inputAssumeSquare ? maxX : mathType(1); const mathType maxYIn = inputAssumeSquare ? maxY : mathType(1); const mathType maxXOut = !inputAssumeSquare ? maxX : mathType(1); const mathType maxYOut = !inputAssumeSquare ? maxY : mathType(1); const mathType centerX = sInt16Scale * maxXOut / mathType(2); const mathType centerY = sInt16Scale * maxYOut / mathType(2); // The basic idea for this effect is to rotate a rectangle that is filled with the palette along one axis, then map our // display to it, to find what color a pixel should have. // However, we want a) no areas of solid color (in front of or behind the palette), and b) we want to make use of the full palette. // So the rectangle needs to have exactly the right size. That size depends on the rotation. // This scale computation here only considers one dimension. You can think of it like the rectangle is always scaled so that // the left and right most points always match the left and right side of the display. const mathType scale = std::abs(sinTheta) + (std::abs(cosTheta) * maxYOut / maxXOut); // 2D simulation: // If we are dealing with a 1D setup, we assume that each segment represents one line on a 2-dimensional display. // The function is called once per segments, so we need to handle one line at a time. const int yFrom = isMatrix ? 0 : strip.getCurrSegmentId(); const int yTo = isMatrix ? maxY : yFrom; for (int y = yFrom; y <= yTo; ++y) { // translate, scale, rotate const mathType ytCosTheta = mathType((wideMathType(cosTheta) * wideMathType(y * sInt16Scale - centerY * maxYIn))/wideMathType(maxYIn * scale)); for (int x = 0; x < cols; ++x) { // translate, scale, rotate const mathType xtSinTheta = mathType((wideMathType(sinTheta) * wideMathType(x * sInt16Scale - centerX * maxXIn))/wideMathType(maxXIn * scale)); // Map the pixel coordinate to an imaginary-rectangle-coordinate. // The y coordinate doesn't actually matter, as our imaginary rectangle is filled with the palette from left to right, // so all points at a given x-coordinate have the same color. const mathType sourceX = xtSinTheta + ytCosTheta + centerX; // The computation was scaled just right so that the result should always be in range [0, maxXOut], but enforce this anyway // to account for imprecision. Then scale it so that the range is [0, 255], which we can use with the palette. int colorIndex = (std::min(std::max(sourceX, mathType(0)), maxXOut * sInt16Scale) * wideMathType(255)) / (sInt16Scale * maxXOut); // inputSize determines by how much we want to scale the palette: // values < 128 display a fraction of a palette, // values > 128 display multiple palettes. if (inputSize <= 128) { colorIndex = (colorIndex * inputSize) / 128; } else { // Linear function that maps colorIndex 128=>1, 256=>9. // With this function every full palette repetition is exactly 16 configuration steps wide. // That allows displaying exactly 2 repetitions for example. colorIndex = ((inputSize - 112) * colorIndex) / 16; } // Finally, shift the palette a bit. const int paletteOffset = (!inputAnimateShift) ? (inputShift) : (((strip.now * ((inputShift >> 3) +1)) & 0xFFFF) >> 8); colorIndex -= paletteOffset; const uint32_t color = SEGMENT.color_wheel((uint8_t)colorIndex); if (isMatrix) { SEGMENT.setPixelColorXY(x, y, color); } else { SEGMENT.setPixelColor(x, color); } } } } static const char _data_FX_MODE_PALETTE[] PROGMEM = "Palette@Shift,Size,Rotation,,,Animate Shift,Animate Rotation,Anamorphic;;!;12;ix=112,c1=0,o1=1,o2=0,o3=1"; #if defined(WLED_PS_DONT_REPLACE_1D_FX) || defined(WLED_PS_DONT_REPLACE_2D_FX) // WLED limitation: Analog Clock overlay will NOT work when Fire2012 is active // Fire2012 by Mark Kriegsman, July 2012 // as part of "Five Elements" shown here: http://youtu.be/knWiGsmgycY //// // This basic one-dimensional 'fire' simulation works roughly as follows: // There's a underlying array of 'heat' cells, that model the temperature // at each point along the line. Every cycle through the simulation, // four steps are performed: // 1) All cells cool down a little bit, losing heat to the air // 2) The heat from each cell drifts 'up' and diffuses a little // 3) Sometimes randomly new 'sparks' of heat are added at the bottom // 4) The heat from each cell is rendered as a color into the leds array // The heat-to-color mapping uses a black-body radiation approximation. // // Temperature is in arbitrary units from 0 (cold black) to 255 (white hot). // // This simulation scales it self a bit depending on SEGLEN; it should look // "OK" on anywhere from 20 to 100 LEDs without too much tweaking. // // I recommend running this simulation at anywhere from 30-100 frames per second, // meaning an interframe delay of about 10-35 milliseconds. // // Looks best on a high-density LED setup (60+ pixels/meter). // // // There are two main parameters you can play with to control the look and // feel of your fire: COOLING (used in step 1 above) (Speed = COOLING), and SPARKING (used // in step 3 above) (Effect Intensity = Sparking). void mode_fire_2012() { if (SEGLEN <= 1) FX_FALLBACK_STATIC; const unsigned strips = SEGMENT.nrOfVStrips(); if (!SEGENV.allocateData(strips * SEGLEN)) FX_FALLBACK_STATIC; //allocation failed byte* heat = SEGENV.data; const uint32_t it = strip.now >> 5; //div 32 struct virtualStrip { static void runStrip(uint16_t stripNr, byte* heat, uint32_t it) { const uint8_t ignition = MAX(3,SEGLEN/10); // ignition area: 10% of segment length or minimum 3 pixels // Step 1. Cool down every cell a little for (unsigned i = 0; i < SEGLEN; i++) { uint8_t cool = (it != SEGENV.step) ? hw_random8((((20 + SEGMENT.speed/3) * 16) / SEGLEN)+2) : hw_random8(4); uint8_t minTemp = (i 1; k--) { heat[k] = (heat[k - 1] + (heat[k - 2]<<1) ) / 3; // heat[k-2] multiplied by 2 } // Step 3. Randomly ignite new 'sparks' of heat near the bottom if (hw_random8() <= SEGMENT.intensity) { uint8_t y = hw_random8(ignition); uint8_t boost = (17+SEGMENT.custom3) * (ignition - y/2) / ignition; // integer math! heat[y] = qadd8(heat[y], hw_random8(96+2*boost,207+boost)); } } // Step 4. Map from heat cells to LED colors for (unsigned j = 0; j < SEGLEN; j++) { SEGMENT.setPixelColor(indexToVStrip(j, stripNr), ColorFromPalette(SEGPALETTE, min(heat[j], byte(240)), 255, NOBLEND)); } } }; for (unsigned stripNr=0; stripNr> 2; if (blurAmount > 48) blurAmount += blurAmount-48; // extra blur when slider > 192 (bush burn) if (blurAmount < 16) SEGMENT.blurCols(SEGMENT.custom2 >> 1); // no side-burn when slider < 64 (faster) else SEGMENT.blur(blurAmount); } if (it != SEGENV.step) SEGENV.step = it; } static const char _data_FX_MODE_FIRE_2012[] PROGMEM = "Fire 2012@Cooling,Spark rate,,2D Blur,Boost;;!;1;pal=35,sx=64,ix=160,m12=1,c2=128"; // bars #endif // WLED_PS_DONT_REPLACE_x_FX // colored stripes pulsing at a defined Beats-Per-Minute (BPM) void mode_bpm() { uint32_t stp = (strip.now / 20) & 0xFF; uint8_t beat = beatsin8_t(SEGMENT.speed, 64, 255); for (unsigned i = 0; i < SEGLEN; i++) { SEGMENT.setPixelColor(i, SEGMENT.color_from_palette(stp + (i * 2), false, PALETTE_SOLID_WRAP, 0, beat - stp + (i * 10))); } } static const char _data_FX_MODE_BPM[] PROGMEM = "Bpm@!;!;!;;sx=64"; void mode_fillnoise8() { if (SEGENV.call == 0) SEGENV.step = hw_random(); for (unsigned i = 0; i < SEGLEN; i++) { unsigned index = perlin8(i * SEGLEN, SEGENV.step + i * SEGLEN); SEGMENT.setPixelColor(i, SEGMENT.color_from_palette(index, false, PALETTE_SOLID_WRAP, 0)); } SEGENV.step += beatsin8_t(SEGMENT.speed, 1, 6); //10,1,4 } static const char _data_FX_MODE_FILLNOISE8[] PROGMEM = "Fill Noise@!;!;!"; void mode_noise16_1() { unsigned scale = 320; // the "zoom factor" for the noise SEGENV.step += (1 + SEGMENT.speed/16); for (unsigned i = 0; i < SEGLEN; i++) { unsigned shift_x = beatsin8_t(11); // the x position of the noise field swings @ 17 bpm unsigned shift_y = SEGENV.step/42; // the y position becomes slowly incremented unsigned real_x = (i + shift_x) * scale; // the x position of the noise field swings @ 17 bpm unsigned real_y = (i + shift_y) * scale; // the y position becomes slowly incremented uint32_t real_z = SEGENV.step; // the z position becomes quickly incremented unsigned noise = perlin16(real_x, real_y, real_z) >> 8; // get the noise data and scale it down unsigned index = sin8_t(noise * 3); // map LED color based on noise data SEGMENT.setPixelColor(i, SEGMENT.color_from_palette(index, false, PALETTE_SOLID_WRAP, 0)); } } static const char _data_FX_MODE_NOISE16_1[] PROGMEM = "Noise 1@!;!;!;;pal=20"; void mode_noise16_2() { unsigned scale = 1000; // the "zoom factor" for the noise SEGENV.step += (1 + (SEGMENT.speed >> 1)); for (unsigned i = 0; i < SEGLEN; i++) { unsigned shift_x = SEGENV.step >> 6; // x as a function of time uint32_t real_x = (i + shift_x) * scale; // calculate the coordinates within the noise field unsigned noise = perlin16(real_x, 0, 4223) >> 8; // get the noise data and scale it down unsigned index = sin8_t(noise * 3); // map led color based on noise data SEGMENT.setPixelColor(i, SEGMENT.color_from_palette(index, false, PALETTE_SOLID_WRAP, 0, noise)); } } static const char _data_FX_MODE_NOISE16_2[] PROGMEM = "Noise 2@!;!;!;;pal=43"; void mode_noise16_3() { unsigned scale = 800; // the "zoom factor" for the noise SEGENV.step += (1 + SEGMENT.speed); for (unsigned i = 0; i < SEGLEN; i++) { unsigned shift_x = 4223; // no movement along x and y unsigned shift_y = 1234; uint32_t real_x = (i + shift_x) * scale; // calculate the coordinates within the noise field uint32_t real_y = (i + shift_y) * scale; // based on the precalculated positions uint32_t real_z = SEGENV.step*8; unsigned noise = perlin16(real_x, real_y, real_z) >> 8; // get the noise data and scale it down unsigned index = sin8_t(noise * 3); // map led color based on noise data SEGMENT.setPixelColor(i, SEGMENT.color_from_palette(index, false, PALETTE_SOLID_WRAP, 0, noise)); } } static const char _data_FX_MODE_NOISE16_3[] PROGMEM = "Noise 3@!;!;!;;pal=35"; //https://github.com/aykevl/ledstrip-spark/blob/master/ledstrip.ino void mode_noise16_4() { uint32_t stp = (strip.now * SEGMENT.speed) >> 7; for (unsigned i = 0; i < SEGLEN; i++) { int index = perlin16(uint32_t(i) << 12, stp); SEGMENT.setPixelColor(i, SEGMENT.color_from_palette(index, false, PALETTE_SOLID_WRAP, 0)); } } static const char _data_FX_MODE_NOISE16_4[] PROGMEM = "Noise 4@!;!;!;;pal=26"; //based on https://gist.github.com/kriegsman/5408ecd397744ba0393e void mode_colortwinkle() { unsigned dataSize = (SEGLEN+7) >> 3; //1 bit per LED if (!SEGENV.allocateData(dataSize)) FX_FALLBACK_STATIC; //allocation failed //limit update rate if (strip.now - SEGENV.step < FRAMETIME_FIXED) return; SEGENV.step = strip.now; CRGBW col, prev; fract8 fadeUpAmount = strip.getBrightness()>28 ? 8 + (SEGMENT.speed>>2) : 68-strip.getBrightness(); fract8 fadeDownAmount = strip.getBrightness()>28 ? 8 + (SEGMENT.speed>>3) : 68-strip.getBrightness(); for (unsigned i = 0; i < SEGLEN; i++) { CRGBW cur = SEGMENT.getPixelColor(i); prev = cur; unsigned index = i >> 3; unsigned bitNum = i & 0x07; bool fadeUp = bitRead(SEGENV.data[index], bitNum); if (fadeUp) { CRGBW incrementalColor = color_fade(cur, fadeUpAmount, true); col = color_add(cur, incrementalColor); if (col.r == 255 || col.g == 255 || col.b == 255) { bitWrite(SEGENV.data[index], bitNum, false); } if (cur == prev) { //fix "stuck" pixels col = color_add(col, col); SEGMENT.setPixelColor(i, col); } else SEGMENT.setPixelColor(i, col); } else { col = color_fade(cur, 255 - fadeDownAmount); SEGMENT.setPixelColor(i, col); } } for (unsigned j = 0; j <= SEGLEN / 50; j++) { if (hw_random8() <= SEGMENT.intensity) { for (unsigned times = 0; times < 5; times++) { //attempt to spawn a new pixel 5 times int i = hw_random16(SEGLEN); if (SEGMENT.getPixelColor(i) == 0) { unsigned index = i >> 3; unsigned bitNum = i & 0x07; bitWrite(SEGENV.data[index], bitNum, true); SEGMENT.setPixelColor(i, ColorFromPalette(SEGPALETTE, hw_random8(), 64, NOBLEND)); break; //only spawn 1 new pixel per frame per 50 LEDs } } } } } static const char _data_FX_MODE_COLORTWINKLE[] PROGMEM = "Colortwinkles@Fade speed,Spawn speed;;!;;m12=0"; //pixels //Calm effect, like a lake at night void mode_lake() { unsigned sp = SEGMENT.speed/10; int wave1 = beatsin8_t(sp +2, -64,64); int wave2 = beatsin8_t(sp +1, -64,64); int wave3 = beatsin8_t(sp +2, 0,80); for (unsigned i = 0; i < SEGLEN; i++) { int index = cos8_t((i*15)+ wave1)/2 + cubicwave8((i*23)+ wave2)/2; uint8_t lum = (index > wave3) ? index - wave3 : 0; SEGMENT.setPixelColor(i, SEGMENT.color_from_palette(index, false, false, 0, lum)); } } static const char _data_FX_MODE_LAKE[] PROGMEM = "Lake@!;Fx;!"; // meteor effect & meteor smooth (merged by @dedehai) // send a meteor from begining to to the end of the strip with a trail that randomly decays. // adapted from https://www.tweaking4all.com/hardware/arduino/adruino-led-strip-effects/#LEDStripEffectMeteorRain void mode_meteor() { if (SEGLEN <= 1) FX_FALLBACK_STATIC; if (!SEGENV.allocateData(SEGLEN)) FX_FALLBACK_STATIC; //allocation failed const bool meteorSmooth = SEGMENT.check3; byte* trail = SEGENV.data; const unsigned meteorSize = 1 + SEGLEN / 20; // 5% uint16_t meteorstart; if(meteorSmooth) meteorstart = map((SEGENV.step >> 6 & 0xFF), 0, 255, 0, SEGLEN -1); else { unsigned counter = strip.now * ((SEGMENT.speed >> 2) + 8); meteorstart = (counter * SEGLEN) >> 16; } const int max = SEGMENT.palette==5 || !SEGMENT.check1 ? 240 : 255; // fade all leds to colors[1] in LEDs one step for (unsigned i = 0; i < SEGLEN; i++) { uint32_t col; if (hw_random8() <= 255 - SEGMENT.intensity) { if(meteorSmooth) { if (trail[i] > 0) { int change = trail[i] + 4 - hw_random8(24); //change each time between -20 and +4 trail[i] = constrain(change, 0, max); } col = SEGMENT.check1 ? SEGMENT.color_from_palette(i, true, false, 0, trail[i]) : SEGMENT.color_from_palette(trail[i], false, true, 255); } else { trail[i] = scale8(trail[i], 128 + hw_random8(127)); int index = trail[i]; int idx = 255; int bri = SEGMENT.palette==35 || SEGMENT.palette==36 ? 255 : trail[i]; if (!SEGMENT.check1) { idx = 0; index = map(i,0,SEGLEN,0,max); bri = trail[i]; } col = SEGMENT.color_from_palette(index, false, false, idx, bri); // full brightness for Fire } SEGMENT.setPixelColor(i, col); } } // draw meteor for (unsigned j = 0; j < meteorSize; j++) { unsigned index = (meteorstart + j) % SEGLEN; if(meteorSmooth) { trail[index] = max; uint32_t col = SEGMENT.check1 ? SEGMENT.color_from_palette(index, true, false, 0, trail[index]) : SEGMENT.color_from_palette(trail[index], false, true, 255); SEGMENT.setPixelColor(index, col); } else{ int idx = 255; int i = trail[index] = max; if (!SEGMENT.check1) { i = map(index,0,SEGLEN,0,max); idx = 0; } uint32_t col = SEGMENT.color_from_palette(i, false, false, idx, 255); // full brightness SEGMENT.setPixelColor(index, col); } } SEGENV.step += SEGMENT.speed +1; } static const char _data_FX_MODE_METEOR[] PROGMEM = "Meteor@!,Trail,,,,Gradient,,Smooth;;!;1"; //Railway Crossing / Christmas Fairy lights void mode_railway() { if (SEGLEN <= 1) FX_FALLBACK_STATIC; unsigned dur = (256 - SEGMENT.speed) * 40; uint16_t rampdur = (dur * SEGMENT.intensity) >> 8; if (SEGENV.step > dur) { //reverse direction SEGENV.step = 0; SEGENV.aux0 = !SEGENV.aux0; } unsigned pos = 255; if (rampdur != 0) { unsigned p0 = (SEGENV.step * 255) / rampdur; if (p0 < 255) pos = p0; } if (SEGENV.aux0) pos = 255 - pos; for (unsigned i = 0; i < SEGLEN; i += 2) { SEGMENT.setPixelColor(i, SEGMENT.color_from_palette(255 - pos, false, false, 255)); // do not use color 1 or 2, always use palette if (i < SEGLEN -1) { SEGMENT.setPixelColor(i + 1, SEGMENT.color_from_palette(pos, false, false, 255)); // do not use color 1 or 2, always use palette } } SEGENV.step += FRAMETIME; } static const char _data_FX_MODE_RAILWAY[] PROGMEM = "Railway@!,Smoothness;1,2;!;;pal=3"; //Water ripple //propagation velocity from speed //drop rate from intensity //4 bytes typedef struct Ripple { uint8_t state; uint8_t color; uint16_t pos; } ripple; #ifdef ESP8266 #define MAX_RIPPLES 56 #else #define MAX_RIPPLES 100 #endif static void ripple_base(uint8_t blurAmount = 0) { unsigned maxRipples = min(1 + (int)(SEGLEN >> 2), MAX_RIPPLES); // 56 max for 16 segment ESP8266 unsigned dataSize = sizeof(ripple) * maxRipples; if (!SEGENV.allocateData(dataSize)) FX_FALLBACK_STATIC; //allocation failed Ripple* ripples = reinterpret_cast(SEGENV.data); //draw wave for (unsigned i = 0; i < maxRipples; i++) { unsigned ripplestate = ripples[i].state; if (ripplestate) { unsigned rippledecay = (SEGMENT.speed >> 4) +1; //faster decay if faster propagation unsigned rippleorigin = ripples[i].pos; uint32_t col = SEGMENT.color_from_palette(ripples[i].color, false, false, 255); unsigned propagation = ((ripplestate/rippledecay - 1) * (SEGMENT.speed + 1)); int propI = propagation >> 8; unsigned propF = propagation & 0xFF; unsigned amp = (ripplestate < 17) ? triwave8((ripplestate-1)*8) : map(ripplestate,17,255,255,2); #ifndef WLED_DISABLE_2D if (SEGMENT.is2D()) { propI /= 2; unsigned cx = rippleorigin >> 8; unsigned cy = rippleorigin & 0xFF; unsigned mag = scale8(sin8_t((propF>>2)), amp); if (propI > 0) SEGMENT.drawCircle(cx, cy, propI, color_blend(SEGMENT.getPixelColorXY(cx + propI, cy), col, mag), true); } else #endif { int left = rippleorigin - propI -1; int right = rippleorigin + propI +2; for (int v = 0; v < 4; v++) { uint8_t mag = scale8(cubicwave8((propF>>2) + v * 64), amp); SEGMENT.setPixelColor(left + v, color_blend(SEGMENT.getPixelColor(left + v), col, mag)); // TODO SEGMENT.setPixelColor(right - v, color_blend(SEGMENT.getPixelColor(right - v), col, mag)); // TODO } } ripplestate += rippledecay; ripples[i].state = (ripplestate > 254) ? 0 : ripplestate; } else {//randomly create new wave if (hw_random16(IBN + 10000) <= (SEGMENT.intensity >> (SEGMENT.is2D()*3))) { ripples[i].state = 1; ripples[i].pos = SEGMENT.is2D() ? ((hw_random8(SEG_W)<<8) | (hw_random8(SEG_H))) : hw_random16(SEGLEN); ripples[i].color = hw_random8(); //color } } } SEGMENT.blur(blurAmount); } #undef MAX_RIPPLES void mode_ripple(void) { if (SEGLEN <= 1) FX_FALLBACK_STATIC; if(SEGMENT.custom1 || SEGMENT.check2) // blur or overlay SEGMENT.fade_out(250); else SEGMENT.fill(SEGCOLOR(1)); ripple_base(SEGMENT.custom1>>1); } static const char _data_FX_MODE_RIPPLE[] PROGMEM = "Ripple@!,Wave #,Blur,,,,Overlay;,!;!;12;c1=0"; void mode_ripple_rainbow(void) { if (SEGLEN <= 1) FX_FALLBACK_STATIC; if (SEGENV.call ==0) { SEGENV.aux0 = hw_random8(); SEGENV.aux1 = hw_random8(); } if (SEGENV.aux0 == SEGENV.aux1) { SEGENV.aux1 = hw_random8(); } else if (SEGENV.aux1 > SEGENV.aux0) { SEGENV.aux0++; } else { SEGENV.aux0--; } SEGMENT.fill(color_blend(SEGMENT.color_wheel(SEGENV.aux0),BLACK,uint8_t(235))); ripple_base(); } static const char _data_FX_MODE_RIPPLE_RAINBOW[] PROGMEM = "Ripple Rainbow@!,Wave #;;!;12"; // TwinkleFOX by Mark Kriegsman: https://gist.github.com/kriegsman/756ea6dcae8e30845b5a // // TwinkleFOX: Twinkling 'holiday' lights that fade in and out. // Colors are chosen from a palette. Read more about this effect using the link above! static CRGB twinklefox_one_twinkle(uint32_t ms, uint8_t salt, bool cat) { // Overall twinkle speed (changed) unsigned ticks = ms / SEGENV.aux0; unsigned fastcycle8 = uint8_t(ticks); uint16_t slowcycle16 = (ticks >> 8) + salt; slowcycle16 += sin8_t(slowcycle16); slowcycle16 = (slowcycle16 * 2053) + 1384; uint8_t slowcycle8 = (slowcycle16 & 0xFF) + (slowcycle16 >> 8); // Overall twinkle density. // 0 (NONE lit) to 8 (ALL lit at once). // Default is 5. unsigned twinkleDensity = (SEGMENT.intensity >> 5) +1; unsigned bright = 0; if (((slowcycle8 & 0x0E)/2) < twinkleDensity) { unsigned ph = fastcycle8; // This is like 'triwave8', which produces a // symmetrical up-and-down triangle sawtooth waveform, except that this // function produces a triangle wave with a faster attack and a slower decay if (cat) { //twinklecat, variant where the leds instantly turn on and fade off bright = 255 - ph; if (SEGMENT.check2) { //reverse checkbox, reverses the leds to fade on and instantly turn off bright = ph; } } else { //vanilla twinklefox if (ph < 86) { bright = ph * 3; } else { ph -= 86; bright = 255 - (ph + (ph/2)); } } } unsigned hue = slowcycle8 - salt; CRGB c; if (bright > 0) { c = ColorFromPalette(SEGPALETTE, hue, bright, NOBLEND); if (!SEGMENT.check1) { // This code takes a pixel, and if its in the 'fading down' // part of the cycle, it adjusts the color a little bit like the // way that incandescent bulbs fade toward 'red' as they dim. if (fastcycle8 >= 128) { unsigned cooling = (fastcycle8 - 128) >> 4; c.g = qsub8(c.g, cooling); c.b = qsub8(c.b, cooling * 2); } } } else { c = CRGB::Black; } return c; } // This function loops over each pixel, calculates the // adjusted 'clock' that this pixel should use, and calls // "CalculateOneTwinkle" on each pixel. It then displays // either the twinkle color of the background color, // whichever is brighter. static void twinklefox_base(bool cat) { // "PRNG16" is the pseudorandom number generator // It MUST be reset to the same starting value each time // this function is called, so that the sequence of 'random' // numbers that it generates is (paradoxically) stable. uint16_t PRNG16 = 11337; // Calculate speed if (SEGMENT.speed > 100) SEGENV.aux0 = 3 + ((255 - SEGMENT.speed) >> 3); else SEGENV.aux0 = 22 + ((100 - SEGMENT.speed) >> 1); // Set up the background color, "bg". CRGB bg = CRGB(SEGCOLOR(1)); unsigned bglight = bg.getAverageLight(); if (bglight > 64) { bg.nscale8_video(16); // very bright, so scale to 1/16th } else if (bglight > 16) { bg.nscale8_video(64); // not that bright, so scale to 1/4th } else { bg.nscale8_video(86); // dim, scale to 1/3rd. } unsigned backgroundBrightness = bg.getAverageLight(); for (unsigned i = 0; i < SEGLEN; i++) { PRNG16 = (uint16_t)(PRNG16 * 2053) + 1384; // next 'random' number unsigned myclockoffset16= PRNG16; // use that number as clock offset PRNG16 = (uint16_t)(PRNG16 * 2053) + 1384; // next 'random' number // use that number as clock speed adjustment factor (in 8ths, from 8/8ths to 23/8ths) unsigned myspeedmultiplierQ5_3 = ((((PRNG16 & 0xFF)>>4) + (PRNG16 & 0x0F)) & 0x0F) + 0x08; uint32_t myclock30 = (uint32_t)((strip.now * myspeedmultiplierQ5_3) >> 3) + myclockoffset16; unsigned myunique8 = PRNG16 >> 8; // get 'salt' value for this pixel // We now have the adjusted 'clock' for this pixel, now we call // the function that computes what color the pixel should be based // on the "brightness = f( time )" idea. CRGB c = twinklefox_one_twinkle(myclock30, myunique8, cat); unsigned cbright = c.getAverageLight(); int deltabright = cbright - backgroundBrightness; if (deltabright >= 32 || (!bg)) { // If the new pixel is significantly brighter than the background color, // use the new color. SEGMENT.setPixelColor(i, c); } else if (deltabright > 0) { // If the new pixel is just slightly brighter than the background color, // mix a blend of the new color and the background color SEGMENT.setPixelColor(i, color_blend(RGBW32(bg.r,bg.g,bg.b,0), RGBW32(c.r,c.g,c.b,0), uint8_t(deltabright * 8))); } else { // if the new pixel is not at all brighter than the background color, // just use the background color. SEGMENT.setPixelColor(i, bg); } } } void mode_twinklefox() { twinklefox_base(false); } static const char _data_FX_MODE_TWINKLEFOX[] PROGMEM = "Twinklefox@!,Twinkle rate,,,,Cool;!,!;!"; void mode_twinklecat() { twinklefox_base(true); } static const char _data_FX_MODE_TWINKLECAT[] PROGMEM = "Twinklecat@!,Twinkle rate,,,,Cool,Reverse;!,!;!"; void mode_halloween_eyes() { enum eyeState : uint8_t { initializeOn = 0, on, blink, initializeOff, off, count }; struct EyeData { eyeState state; uint8_t color; uint16_t startPos; // duration + endTime could theoretically be replaced by a single endTime, however we would lose // the ability to end the animation early when the user reduces the animation time. uint16_t duration; uint32_t startTime; uint32_t blinkEndTime; }; if (SEGLEN <= 1) FX_FALLBACK_STATIC; const unsigned maxWidth = strip.isMatrix ? SEG_W : SEGLEN; const unsigned HALLOWEEN_EYE_SPACE = MAX(2, strip.isMatrix ? SEG_W>>4: SEGLEN>>5); const unsigned HALLOWEEN_EYE_WIDTH = HALLOWEEN_EYE_SPACE/2; unsigned eyeLength = (2*HALLOWEEN_EYE_WIDTH) + HALLOWEEN_EYE_SPACE; if (eyeLength >= maxWidth) FX_FALLBACK_STATIC; //bail if segment too short if (!SEGENV.allocateData(sizeof(EyeData))) FX_FALLBACK_STATIC; //allocation failed EyeData& data = *reinterpret_cast(SEGENV.data); if (!SEGMENT.check2) SEGMENT.fill(SEGCOLOR(1)); //fill background data.state = static_cast(data.state % eyeState::count); unsigned duration = max(uint16_t{1u}, data.duration); const uint32_t elapsedTime = strip.now - data.startTime; switch (data.state) { case eyeState::initializeOn: { // initialize the eyes-on state: // - select eye position and color // - select a duration // - immediately switch to eyes on state. data.startPos = hw_random16(0, maxWidth - eyeLength - 1); data.color = hw_random8(); if (strip.isMatrix) SEGMENT.offset = hw_random16(SEG_H-1); // a hack: reuse offset since it is not used in matrices duration = 128u + hw_random16(SEGMENT.intensity*64u); data.duration = duration; data.state = eyeState::on; [[fallthrough]]; } case eyeState::on: { // eyes-on steate: // - fade eyes in for some time // - keep eyes on until the pre-selected duration is over // - randomly switch to the blink (sub-)state, and initialize it with a blink duration (more precisely, a blink end time stamp) // - never switch to the blink state if the animation just started or is about to end unsigned start2ndEye = data.startPos + HALLOWEEN_EYE_WIDTH + HALLOWEEN_EYE_SPACE; // If the user reduces the input while in this state, limit the duration. duration = min(duration, (128u + (SEGMENT.intensity * 64u))); constexpr uint32_t minimumOnTimeBegin = 1024u; constexpr uint32_t minimumOnTimeEnd = 1024u; const uint32_t fadeInAnimationState = elapsedTime * uint32_t{256u * 8u} / duration; const uint32_t backgroundColor = SEGCOLOR(1); const uint32_t eyeColor = SEGMENT.color_from_palette(data.color, false, false, 0); uint32_t c = eyeColor; if (fadeInAnimationState < 256u) { c = color_blend(backgroundColor, eyeColor, uint8_t(fadeInAnimationState)); } else if (elapsedTime > minimumOnTimeBegin) { const uint32_t remainingTime = (elapsedTime >= duration) ? 0u : (duration - elapsedTime); if (remainingTime > minimumOnTimeEnd) { if (hw_random8() < 4u) { c = backgroundColor; data.state = eyeState::blink; data.blinkEndTime = strip.now + hw_random8(8, 128); } } } if (c != backgroundColor) { // render eyes for (unsigned i = 0; i < HALLOWEEN_EYE_WIDTH; i++) { if (strip.isMatrix) { SEGMENT.setPixelColorXY(data.startPos + i, (unsigned)SEGMENT.offset, c); SEGMENT.setPixelColorXY(start2ndEye + i, (unsigned)SEGMENT.offset, c); } else { SEGMENT.setPixelColor(data.startPos + i, c); SEGMENT.setPixelColor(start2ndEye + i, c); } } } break; } case eyeState::blink: { // eyes-on but currently blinking state: // - wait until the blink time is over, then switch back to eyes-on if (strip.now >= data.blinkEndTime) { data.state = eyeState::on; } break; } case eyeState::initializeOff: { // initialize eyes-off state: // - select a duration // - immediately switch to eyes-off state const unsigned eyeOffTimeBase = SEGMENT.speed*128u; duration = eyeOffTimeBase + hw_random16(eyeOffTimeBase); data.duration = duration; data.state = eyeState::off; [[fallthrough]]; } case eyeState::off: { // eyes-off state: // - not much to do here // If the user reduces the input while in this state, limit the duration. const unsigned eyeOffTimeBase = SEGMENT.speed*128u; duration = min(duration, (2u * eyeOffTimeBase)); break; } case eyeState::count: { // Can't happen, not an actual state. data.state = eyeState::initializeOn; break; } } if (elapsedTime > duration) { // The current state duration is over, switch to the next state. switch (data.state) { case eyeState::initializeOn: case eyeState::on: case eyeState::blink: data.state = eyeState::initializeOff; break; case eyeState::initializeOff: case eyeState::off: case eyeState::count: default: data.state = eyeState::initializeOn; break; } data.startTime = strip.now; } } static const char _data_FX_MODE_HALLOWEEN_EYES[] PROGMEM = "Halloween Eyes@Eye off time,Eye on time,,,,,Overlay;!,!;!;12"; //Speed slider sets amount of LEDs lit, intensity sets unlit void mode_static_pattern() { unsigned lit = 1 + SEGMENT.speed; unsigned unlit = 1 + SEGMENT.intensity; bool drawingLit = true; unsigned cnt = 0; for (unsigned i = 0; i < SEGLEN; i++) { SEGMENT.setPixelColor(i, (drawingLit) ? SEGMENT.color_from_palette(i, true, PALETTE_SOLID_WRAP, 0) : SEGCOLOR(1)); cnt++; if (cnt >= ((drawingLit) ? lit : unlit)) { cnt = 0; drawingLit = !drawingLit; } } } static const char _data_FX_MODE_STATIC_PATTERN[] PROGMEM = "Solid Pattern@Fg size,Bg size;Fg,!;!;;pal=0"; void mode_tri_static_pattern() { unsigned segSize = (SEGMENT.intensity >> 5) +1; unsigned currSeg = 0; unsigned currSegCount = 0; for (unsigned i = 0; i < SEGLEN; i++) { if ( currSeg % 3 == 0 ) { SEGMENT.setPixelColor(i, SEGCOLOR(0)); } else if( currSeg % 3 == 1) { SEGMENT.setPixelColor(i, SEGCOLOR(1)); } else { SEGMENT.setPixelColor(i, SEGCOLOR(2)); } currSegCount += 1; if (currSegCount >= segSize) { currSeg +=1; currSegCount = 0; } } } static const char _data_FX_MODE_TRI_STATIC_PATTERN[] PROGMEM = "Solid Pattern Tri@,Size;1,2,3;;;pal=0"; static void spots_base(uint16_t threshold) { if (SEGLEN <= 1) FX_FALLBACK_STATIC; if (!SEGMENT.check2) SEGMENT.fill(SEGCOLOR(1)); unsigned maxZones = SEGLEN >> 2; unsigned zones = 1 + ((SEGMENT.intensity * maxZones) >> 8); unsigned zoneLen = SEGLEN / zones; unsigned offset = (SEGLEN - zones * zoneLen) >> 1; for (unsigned z = 0; z < zones; z++) { unsigned pos = offset + z * zoneLen; for (unsigned i = 0; i < zoneLen; i++) { unsigned wave = triwave16((i * 0xFFFF) / zoneLen); if (wave > threshold) { unsigned index = 0 + pos + i; unsigned s = (wave - threshold)*255 / (0xFFFF - threshold); SEGMENT.setPixelColor(index, color_blend(SEGMENT.color_from_palette(index, true, PALETTE_SOLID_WRAP, 0), SEGCOLOR(1), uint8_t(255-s))); } } } } //Intensity slider sets number of "lights", speed sets LEDs per light void mode_spots() { spots_base((255 - SEGMENT.speed) << 8); } static const char _data_FX_MODE_SPOTS[] PROGMEM = "Spots@Spread,Width,,,,,Overlay;!,!;!"; //Intensity slider sets number of "lights", LEDs per light fade in and out void mode_spots_fade() { unsigned counter = strip.now * ((SEGMENT.speed >> 2) +8); unsigned t = triwave16(counter); unsigned tr = (t >> 1) + (t >> 2); spots_base(tr); } static const char _data_FX_MODE_SPOTS_FADE[] PROGMEM = "Spots Fade@Spread,Width,,,,,Overlay;!,!;!"; //each needs 12 bytes typedef struct Ball { unsigned long lastBounceTime; float impactVelocity; float height; } ball; /* * Bouncing Balls Effect */ void mode_bouncing_balls(void) { if (SEGLEN <= 1) FX_FALLBACK_STATIC; //allocate segment data const unsigned strips = SEGMENT.nrOfVStrips(); // adapt for 2D const size_t maxNumBalls = 16; unsigned dataSize = sizeof(ball) * maxNumBalls; if (!SEGENV.allocateData(dataSize * strips)) FX_FALLBACK_STATIC; //allocation failed Ball* balls = reinterpret_cast(SEGENV.data); if (!SEGMENT.check2) SEGMENT.fill(SEGCOLOR(2) ? BLACK : SEGCOLOR(1)); // virtualStrip idea by @ewowi (Ewoud Wijma) // requires virtual strip # to be embedded into upper 16 bits of index in setPixelColor() // the following functions will not work on virtual strips: fill(), fade_out(), fadeToBlack(), blur() struct virtualStrip { static void runStrip(size_t stripNr, Ball* balls) { // number of balls based on intensity setting to max of 7 (cycles colors) // non-chosen color is a random color unsigned numBalls = (SEGMENT.intensity * (maxNumBalls - 1)) / 255 + 1; // minimum 1 ball const float gravity = -9.81f; // standard value of gravity const bool hasCol2 = SEGCOLOR(2); const unsigned long time = strip.now; if (SEGENV.call == 0) { for (size_t i = 0; i < maxNumBalls; i++) balls[i].lastBounceTime = time; } for (size_t i = 0; i < numBalls; i++) { float timeSinceLastBounce = (time - balls[i].lastBounceTime)/((255-SEGMENT.speed)/64 +1); float timeSec = timeSinceLastBounce/1000.0f; balls[i].height = (0.5f * gravity * timeSec + balls[i].impactVelocity) * timeSec; // avoid use pow(x, 2) - its extremely slow ! if (balls[i].height <= 0.0f) { balls[i].height = 0.0f; //damping for better effect using multiple balls float dampening = 0.9f - float(i)/float(numBalls * numBalls); // avoid use pow(x, 2) - its extremely slow ! balls[i].impactVelocity = dampening * balls[i].impactVelocity; balls[i].lastBounceTime = time; if (balls[i].impactVelocity < 0.015f) { float impactVelocityStart = sqrtf(-2.0f * gravity) * hw_random8(5,11)/10.0f; // randomize impact velocity balls[i].impactVelocity = impactVelocityStart; } } else if (balls[i].height > 1.0f) { continue; // do not draw OOB ball } uint32_t color = SEGCOLOR(0); if (SEGMENT.palette) { color = SEGMENT.color_wheel(i*(256/MAX(numBalls, 8))); } else if (hasCol2) { color = SEGCOLOR(i % NUM_COLORS); } int pos = roundf(balls[i].height * (SEGLEN - 1)); #ifdef WLED_USE_AA_PIXELS if (SEGLEN<32) SEGMENT.setPixelColor(indexToVStrip(pos, stripNr), color); // encode virtual strip into index else SEGMENT.setPixelColor(balls[i].height + (stripNr+1)*10.0f, color); #else SEGMENT.setPixelColor(indexToVStrip(pos, stripNr), color); // encode virtual strip into index #endif } } }; for (unsigned stripNr=0; stripNr(SEGENV.data); // number of balls based on intensity setting to max of 16 (cycles colors) // non-chosen color is a random color unsigned numBalls = SEGMENT.intensity/16 + 1; bool hasCol2 = SEGCOLOR(2); if (SEGENV.call == 0) { SEGMENT.fill(hasCol2 ? BLACK : SEGCOLOR(1)); // start clean for (unsigned i = 0; i < maxNumBalls; i++) { balls[i].lastBounceUpdate = strip.now; balls[i].velocity = 20.0f * float(hw_random16(1000, 10000))/10000.0f; // number from 1 to 10 if (hw_random8()<128) balls[i].velocity = -balls[i].velocity; // 50% chance of reverse direction balls[i].height = (float(hw_random16(0, 10000)) / 10000.0f); // from 0. to 1. balls[i].mass = (float(hw_random16(1000, 10000)) / 10000.0f); // from .1 to 1. } } float cfac = float(scale8(8, 255-SEGMENT.speed) +1)*20000.0f; // this uses the Aircoookie conversion factor for scaling time using speed slider if (SEGMENT.check3) SEGMENT.fade_out(250); // 2-8 pixel trails (optional) else { if (!SEGMENT.check2) SEGMENT.fill(hasCol2 ? BLACK : SEGCOLOR(1)); // don't fill with background color if user wants to see trails } for (unsigned i = 0; i < numBalls; i++) { float timeSinceLastUpdate = float((strip.now - balls[i].lastBounceUpdate))/cfac; float thisHeight = balls[i].height + balls[i].velocity * timeSinceLastUpdate; // this method keeps higher resolution // test if intensity level was increased and some balls are way off the track then put them back if (thisHeight < -0.5f || thisHeight > 1.5f) { thisHeight = balls[i].height = (float(hw_random16(0, 10000)) / 10000.0f); // from 0. to 1. balls[i].lastBounceUpdate = strip.now; } // check if reached ends of the strip if ((thisHeight <= 0.0f && balls[i].velocity < 0.0f) || (thisHeight >= 1.0f && balls[i].velocity > 0.0f)) { balls[i].velocity = -balls[i].velocity; // reverse velocity balls[i].lastBounceUpdate = strip.now; balls[i].height = thisHeight; } // check for collisions if (SEGMENT.check1) { for (unsigned j = i+1; j < numBalls; j++) { if (balls[j].velocity != balls[i].velocity) { // tcollided + balls[j].lastBounceUpdate is acutal time of collision (this keeps precision with long to float conversions) float tcollided = (cfac*(balls[i].height - balls[j].height) + balls[i].velocity*float(balls[j].lastBounceUpdate - balls[i].lastBounceUpdate))/(balls[j].velocity - balls[i].velocity); if ((tcollided > 2.0f) && (tcollided < float(strip.now - balls[j].lastBounceUpdate))) { // 2ms minimum to avoid duplicate bounces balls[i].height = balls[i].height + balls[i].velocity*(tcollided + float(balls[j].lastBounceUpdate - balls[i].lastBounceUpdate))/cfac; balls[j].height = balls[i].height; balls[i].lastBounceUpdate = (unsigned long)(tcollided + 0.5f) + balls[j].lastBounceUpdate; balls[j].lastBounceUpdate = balls[i].lastBounceUpdate; float vtmp = balls[i].velocity; balls[i].velocity = ((balls[i].mass - balls[j].mass)*vtmp + 2.0f*balls[j].mass*balls[j].velocity)/(balls[i].mass + balls[j].mass); balls[j].velocity = ((balls[j].mass - balls[i].mass)*balls[j].velocity + 2.0f*balls[i].mass*vtmp) /(balls[i].mass + balls[j].mass); thisHeight = balls[i].height + balls[i].velocity*(strip.now - balls[i].lastBounceUpdate)/cfac; } } } } uint32_t color = SEGCOLOR(0); if (SEGMENT.palette) { //color = SEGMENT.color_wheel(i*(256/MAX(numBalls, 8))); color = SEGMENT.color_from_palette(i*255/numBalls, false, PALETTE_SOLID_WRAP, 0); } else if (hasCol2) { color = SEGCOLOR(i % NUM_COLORS); } if (thisHeight < 0.0f) thisHeight = 0.0f; if (thisHeight > 1.0f) thisHeight = 1.0f; unsigned pos = round(thisHeight * (SEGLEN - 1)); SEGMENT.setPixelColor(pos, color); balls[i].lastBounceUpdate = strip.now; balls[i].height = thisHeight; } } static const char _data_FX_MODE_ROLLINGBALLS[] PROGMEM = "Rolling Balls@!,# of balls,,,,Collide,Overlay,Trails;!,!,!;!;1;m12=1"; //bar #endif // WLED_PS_DONT_REPLACE_1D_FX /* / Pac-Man by Bob Loeffler with help from @dedehai and @blazoncek * speed slider is for speed. * intensity slider is for selecting the number of power dots. * custom1 slider is for selecting the LED where the ghosts will start blinking blue. * custom2 slider is for blurring the LEDs in the segment. * custom3 slider is for selecting the # of ghosts (between 2 and 8). * check1 is for displaying White Dots that PacMan eats. Enabled will show white dots. Disabled will not show any white dots (all leds will be black). * check2 is for Smear mode (enabled will smear/persist the LED colors, disabled will not). * check3 is for the Compact Dots mode of displaying white dots. Enabled will show white dots in every LED. Disabled will show black LEDs between the white dots. * aux0 is used to keep track of the previous number of power dots in case the user selects a different number with the intensity slider. * aux1 is the main counter for timing. */ typedef struct PacManChars { signed pos; signed topPos; // LED position of farthest PacMan has moved uint32_t color; bool direction; // true = moving away from first LED bool blue; // used for ghosts only bool eaten; // used for power dots only } pacmancharacters_t; static void mode_pacman(void) { constexpr unsigned ORANGEYELLOW = 0xFFCC00; constexpr unsigned PURPLEISH = 0xB000B0; constexpr unsigned ORANGEISH = 0xFF8800; constexpr unsigned WHITEISH = 0x999999; constexpr unsigned PACMAN = 0; // PacMan is character[0] constexpr uint32_t ghostColors[] = {RED, PURPLEISH, CYAN, ORANGEISH}; unsigned maxPowerDots = min(SEGLEN / 10U, 255U); // cap the max so packed state fits in 8 bits unsigned numPowerDots = map(SEGMENT.intensity, 0, 255, 1, maxPowerDots); unsigned numGhosts = map(SEGMENT.custom3, 0, 31, 2, 8); bool smearMode = SEGMENT.check2; // Pack two 8-bit values into one 16-bit field (stored in SEGENV.aux0) uint16_t combined_value = uint16_t(((numPowerDots & 0xFF) << 8) | (numGhosts & 0xFF)); if (combined_value != SEGENV.aux0) SEGENV.call = 0; // Reinitialize on setting change SEGENV.aux0 = combined_value; // Allocate segment data unsigned dataSize = sizeof(pacmancharacters_t) * (numGhosts + maxPowerDots + 1); // +1 is the PacMan character if (SEGLEN <= 16 + (2*numGhosts) || !SEGENV.allocateData(dataSize)) FX_FALLBACK_STATIC; pacmancharacters_t *character = reinterpret_cast(SEGENV.data); // Calculate when blue ghosts start blinking. // On first call (or after settings change), `topPos` is not known yet, so fall back to the full segment length in that case. int maxBlinkPos = (SEGENV.call == 0) ? (int)SEGLEN - 1 : character[PACMAN].topPos; if (maxBlinkPos < 20) maxBlinkPos = 20; int startBlinkingGhostsLED = (SEGLEN < 64) ? (int)SEGLEN / 3 : map(SEGMENT.custom1, 0, 255, 20, maxBlinkPos); // Initialize characters on first call if (SEGENV.call == 0) { // Initialize PacMan character[PACMAN].color = YELLOW; character[PACMAN].pos = 0; character[PACMAN].topPos = 0; character[PACMAN].direction = true; character[PACMAN].blue = false; // Initialize ghosts with alternating colors for (int i = 1; i <= numGhosts; i++) { character[i].color = ghostColors[(i-1) % 4]; character[i].pos = -2 * (i + 1); character[i].direction = true; character[i].blue = false; } // Initialize power dots for (int i = 0; i < numPowerDots; i++) { character[i + numGhosts + 1].color = ORANGEYELLOW; character[i + numGhosts + 1].eaten = false; } character[numGhosts + 1].pos = SEGLEN - 1; // Last power dot at end } if (strip.now > SEGENV.step) { SEGENV.step = strip.now; SEGENV.aux1++; } // Clear background if not in smear mode if (!smearMode) SEGMENT.fill(BLACK); // Draw white dots in front of PacMan if option selected if (SEGMENT.check1) { int step = SEGMENT.check3 ? 1 : 2; // Compact or spaced dots for (int i = SEGLEN - 1; i > character[PACMAN].topPos; i -= step) { SEGMENT.setPixelColor(i, WHITEISH); } } // Update power dot positions dynamically uint32_t everyXLeds = (((uint32_t)SEGLEN - 10U) << 8) / numPowerDots; // Fixed-point spacing for power dots: use 32-bit math to avoid overflow on long segments. for (int i = 1; i < numPowerDots; i++) { character[i + numGhosts + 1].pos = 10 + ((i * everyXLeds) >> 8); } // Blink power dots every 10 ticks if (SEGENV.aux1 % 10 == 0) { uint32_t dotColor = (character[numGhosts + 1].color == ORANGEYELLOW) ? BLACK : ORANGEYELLOW; for (int i = 0; i < numPowerDots; i++) { character[i + numGhosts + 1].color = dotColor; } } // Blink blue ghosts when nearing start if (SEGENV.aux1 % 15 == 0 && character[1].blue && character[PACMAN].pos <= startBlinkingGhostsLED) { uint32_t ghostColor = (character[1].color == BLUE) ? WHITEISH : BLUE; for (int i = 1; i <= numGhosts; i++) { character[i].color = ghostColor; } } // Draw uneaten power dots for (int i = 0; i < numPowerDots; i++) { if (!character[i + numGhosts + 1].eaten && (unsigned)character[i + numGhosts + 1].pos < SEGLEN) { SEGMENT.setPixelColor(character[i + numGhosts + 1].pos, character[i + numGhosts + 1].color); } } // Check if PacMan ate a power dot for (int j = 0; j < numPowerDots; j++) { auto &dot = character[j + numGhosts + 1]; if (character[PACMAN].pos == dot.pos && !dot.eaten) { // Reverse all characters - PacMan now chases ghosts for (int i = 0; i <= numGhosts; i++) { character[i].direction = false; } // Turn ghosts blue for (int i = 1; i <= numGhosts; i++) { character[i].color = BLUE; character[i].blue = true; } dot.eaten = true; break; // only one power dot per frame } } // Reset when PacMan reaches start with blue ghosts if (character[1].blue && character[PACMAN].pos <= 0) { // Reverse direction back for (int i = 0; i <= numGhosts; i++) { character[i].direction = true; } // Reset ghost colors for (int i = 1; i <= numGhosts; i++) { character[i].color = ghostColors[(i-1) % 4]; character[i].blue = false; } // Reset power dots if last one was eaten if (character[numGhosts + 1].eaten) { for (int i = 0; i < numPowerDots; i++) { character[i + numGhosts + 1].eaten = false; } character[PACMAN].topPos = 0; // set the top position of PacMan to LED 0 (beginning of the segment) } } // Update and draw characters based on speed setting bool updatePositions = (SEGENV.aux1 % map(SEGMENT.speed, 0, 255, 15, 1) == 0); // update positions of characters if it's time to do so if (updatePositions) { character[PACMAN].pos += character[PACMAN].direction ? 1 : -1; for (int i = 1; i <= numGhosts; i++) { character[i].pos += character[i].direction ? 1 : -1; } } // Draw PacMan if ((unsigned)character[PACMAN].pos < SEGLEN) { SEGMENT.setPixelColor(character[PACMAN].pos, character[PACMAN].color); } // Draw ghosts for (int i = 1; i <= numGhosts; i++) { if ((unsigned)character[i].pos < SEGLEN) { SEGMENT.setPixelColor(character[i].pos, character[i].color); } } // Track farthest position of PacMan if (character[PACMAN].topPos < character[PACMAN].pos) { character[PACMAN].topPos = character[PACMAN].pos; } SEGMENT.blur(SEGMENT.custom2>>1); } static const char _data_FX_MODE_PACMAN[] PROGMEM = "PacMan@Speed,# of PowerDots,Blink distance,Blur,# of Ghosts,Dots,Smear,Compact;;!;1;m12=0,sx=192,ix=64,c1=64,c2=0,c3=12,o1=1,o2=0"; /* * Sinelon stolen from FASTLED examples */ static void sinelon_base(bool dual, bool rainbow=false) { if (SEGLEN <= 1) FX_FALLBACK_STATIC; SEGMENT.fade_out(SEGMENT.intensity); unsigned pos = beatsin16_t(SEGMENT.speed/10,0,SEGLEN-1); if (SEGENV.call == 0) SEGENV.aux0 = pos; uint32_t color1 = SEGMENT.color_from_palette(pos, true, false, 0); uint32_t color2 = SEGCOLOR(2); if (rainbow) { color1 = SEGMENT.color_wheel((pos & 0x07) * 32); } SEGMENT.setPixelColor(pos, color1); if (dual) { if (!color2) color2 = SEGMENT.color_from_palette(pos, true, false, 0); if (rainbow) color2 = color1; //rainbow SEGMENT.setPixelColor(SEGLEN-1-pos, color2); } if (SEGENV.aux0 != pos) { if (SEGENV.aux0 < pos) { for (unsigned i = SEGENV.aux0; i < pos ; i++) { SEGMENT.setPixelColor(i, color1); if (dual) SEGMENT.setPixelColor(SEGLEN-1-i, color2); } } else { for (unsigned i = SEGENV.aux0; i > pos ; i--) { SEGMENT.setPixelColor(i, color1); if (dual) SEGMENT.setPixelColor(SEGLEN-1-i, color2); } } SEGENV.aux0 = pos; } } void mode_sinelon(void) { sinelon_base(false); } static const char _data_FX_MODE_SINELON[] PROGMEM = "Sinelon@!,Trail;!,!,!;!"; void mode_sinelon_dual(void) { sinelon_base(true); } static const char _data_FX_MODE_SINELON_DUAL[] PROGMEM = "Sinelon Dual@!,Trail;!,!,!;!"; void mode_sinelon_rainbow(void) { sinelon_base(false, true); } static const char _data_FX_MODE_SINELON_RAINBOW[] PROGMEM = "Sinelon Rainbow@!,Trail;,,!;!"; // utility function that will add random glitter to SEGMENT void glitter_base(uint8_t intensity, uint32_t col = ULTRAWHITE) { if (intensity > hw_random8()) SEGMENT.setPixelColor(hw_random16(SEGLEN), col); } //Glitter with palette background, inspired by https://gist.github.com/kriegsman/062e10f7f07ba8518af6 void mode_glitter() { if (!SEGMENT.check2) { // use "* Color 1" palette for solid background (replacing "Solid glitter") unsigned counter = 0; if (SEGMENT.speed != 0) { counter = (strip.now * ((SEGMENT.speed >> 3) +1)) & 0xFFFF; counter = counter >> 8; } bool noWrap = (paletteBlend == 2 || (paletteBlend == 0 && SEGMENT.speed == 0)); for (unsigned i = 0; i < SEGLEN; i++) { unsigned colorIndex = (i * 255 / SEGLEN) - counter; if (noWrap) colorIndex = map(colorIndex, 0, 255, 0, 240); //cut off blend at palette "end" SEGMENT.setPixelColor(i, SEGMENT.color_from_palette(colorIndex, false, true, 255)); } } glitter_base(SEGMENT.intensity, SEGCOLOR(2) ? SEGCOLOR(2) : ULTRAWHITE); } static const char _data_FX_MODE_GLITTER[] PROGMEM = "Glitter@!,!,,,,,Overlay;,,Glitter color;!;;pal=11,m12=0"; //pixels //Solid colour background with glitter (can be replaced by Glitter) void mode_solid_glitter() { SEGMENT.fill(SEGCOLOR(0)); glitter_base(SEGMENT.intensity, SEGCOLOR(2) ? SEGCOLOR(2) : ULTRAWHITE); } static const char _data_FX_MODE_SOLID_GLITTER[] PROGMEM = "Solid Glitter@,!;Bg,,Glitter color;;;m12=0"; //each needs 20 bytes //Spark type is used for popcorn, 1D fireworks, and drip typedef struct Spark { float pos, posX; float vel, velX; uint16_t col; uint8_t colIndex; } spark; #define maxNumPopcorn 21 // max 21 on 16 segment ESP8266 /* * POPCORN * modified from https://github.com/kitesurfer1404/WS2812FX/blob/master/src/custom/Popcorn.h */ void mode_popcorn(void) { if (SEGLEN <= 1) FX_FALLBACK_STATIC; //allocate segment data unsigned strips = SEGMENT.nrOfVStrips(); unsigned usablePopcorns = maxNumPopcorn; if (usablePopcorns * strips * sizeof(spark) > FAIR_DATA_PER_SEG) usablePopcorns = FAIR_DATA_PER_SEG / (strips * sizeof(spark)) + 1; // at least 1 popcorn per vstrip unsigned dataSize = sizeof(spark) * usablePopcorns; // on a matrix 64x64 this could consume a little less than 27kB when Bar expansion is used if (!SEGENV.allocateData(dataSize * strips)) FX_FALLBACK_STATIC; //allocation failed Spark* popcorn = reinterpret_cast(SEGENV.data); bool hasCol2 = SEGCOLOR(2); if (!SEGMENT.check2) SEGMENT.fill(hasCol2 ? BLACK : SEGCOLOR(1)); struct virtualStrip { static void runStrip(uint16_t stripNr, Spark* popcorn, unsigned usablePopcorns) { float gravity = -0.0001f - (SEGMENT.speed/200000.0f); // m/s/s gravity *= SEGLEN; unsigned numPopcorn = SEGMENT.intensity * usablePopcorns / 255; if (numPopcorn == 0) numPopcorn = 1; for (unsigned i = 0; i < numPopcorn; i++) { if (popcorn[i].pos >= 0.0f) { // if kernel is active, update its position popcorn[i].pos += popcorn[i].vel; popcorn[i].vel += gravity; } else { // if kernel is inactive, randomly pop it if (hw_random8() < 2) { // POP!!! popcorn[i].pos = 0.01f; unsigned peakHeight = 128 + hw_random8(128); //0-255 peakHeight = (peakHeight * (SEGLEN -1)) >> 8; popcorn[i].vel = sqrtf(-2.0f * gravity * peakHeight); if (SEGMENT.palette) { popcorn[i].colIndex = hw_random8(); } else { byte col = hw_random8(0, NUM_COLORS); if (!SEGCOLOR(2) || !SEGCOLOR(col)) col = 0; popcorn[i].colIndex = col; } } } if (popcorn[i].pos >= 0.0f) { // draw now active popcorn (either active before or just popped) uint32_t col = SEGMENT.color_wheel(popcorn[i].colIndex); if (!SEGMENT.palette && popcorn[i].colIndex < NUM_COLORS) col = SEGCOLOR(popcorn[i].colIndex); unsigned ledIndex = popcorn[i].pos; if (ledIndex < SEGLEN) SEGMENT.setPixelColor(indexToVStrip(ledIndex, stripNr), col); } } } }; for (unsigned stripNr=0; stripNr 1) { //allocate segment data unsigned dataSize = sizeof(uint32_t) + max(1, (int)SEGLEN -1) *3; //max. 1365 pixels (ESP8266) if (!SEGENV.allocateData(dataSize)) candle(false); //allocation failed } uint32_t* lastcall = reinterpret_cast(SEGENV.data); uint8_t* candleData = reinterpret_cast(SEGENV.data + sizeof(uint32_t)); //limit update rate if (strip.now - *lastcall < FRAMETIME_FIXED) return; *lastcall = strip.now; //max. flicker range controlled by intensity unsigned valrange = SEGMENT.intensity; unsigned rndval = valrange >> 1; //max 127 //step (how much to move closer to target per frame) coarsely set by speed unsigned speedFactor = 4; if (SEGMENT.speed > 252) { //epilepsy speedFactor = 1; } else if (SEGMENT.speed > 99) { //regular candle (mode called every ~25 ms, so 4 frames to have a new target every 100ms) speedFactor = 2; } else if (SEGMENT.speed > 49) { //slower fade speedFactor = 3; } //else 4 (slowest) unsigned numCandles = (multi) ? SEGLEN : 1; for (unsigned i = 0; i < numCandles; i++) { unsigned d = 0; //data location unsigned s = SEGENV.aux0, s_target = SEGENV.aux1, fadeStep = SEGENV.step; if (i > 0) { d = (i-1) *3; s = candleData[d]; s_target = candleData[d+1]; fadeStep = candleData[d+2]; } if (fadeStep == 0) { //init vals s = 128; s_target = 130 + hw_random8(4); fadeStep = 1; } bool newTarget = false; if (s_target > s) { //fade up s = qadd8(s, fadeStep); if (s >= s_target) newTarget = true; } else { s = qsub8(s, fadeStep); if (s <= s_target) newTarget = true; } if (newTarget) { s_target = hw_random8(rndval) + hw_random8(rndval); //between 0 and rndval*2 -2 = 252 if (s_target < (rndval >> 1)) s_target = (rndval >> 1) + hw_random8(rndval); unsigned offset = (255 - valrange); s_target += offset; unsigned dif = (s_target > s) ? s_target - s : s - s_target; fadeStep = dif >> speedFactor; if (fadeStep == 0) fadeStep = 1; } if (i > 0) { SEGMENT.setPixelColor(i, color_blend(SEGCOLOR(1), SEGMENT.color_from_palette(i, true, PALETTE_SOLID_WRAP, 0), uint8_t(s))); candleData[d] = s; candleData[d+1] = s_target; candleData[d+2] = fadeStep; } else { for (unsigned j = 0; j < SEGLEN; j++) { SEGMENT.setPixelColor(j, color_blend(SEGCOLOR(1), SEGMENT.color_from_palette(j, true, PALETTE_SOLID_WRAP, 0), uint8_t(s))); } SEGENV.aux0 = s; SEGENV.aux1 = s_target; SEGENV.step = fadeStep; } } } void mode_candle() { candle(false); } static const char _data_FX_MODE_CANDLE[] PROGMEM = "Candle@!,!;!,!;!;01;sx=96,ix=224,pal=0"; void mode_candle_multi() { candle(true); } static const char _data_FX_MODE_CANDLE_MULTI[] PROGMEM = "Candle Multi@!,!;!,!;!;;sx=96,ix=224,pal=0"; #ifdef WLED_PS_DONT_REPLACE_1D_FX /* / Fireworks in starburst effect / based on the video: https://www.reddit.com/r/arduino/comments/c3sd46/i_made_this_fireworks_effect_for_my_led_strips/ / Speed sets frequency of new starbursts, intensity is the intensity of the burst */ #ifdef ESP8266 #define STARBURST_MAX_FRAG 8 //52 bytes / star #else #define STARBURST_MAX_FRAG 10 //60 bytes / star #endif //each needs 20+STARBURST_MAX_FRAG*4 bytes typedef struct particle { CRGB color; uint32_t birth =0; uint32_t last =0; float vel =0; uint16_t pos =-1; float fragment[STARBURST_MAX_FRAG]; } star; void mode_starburst(void) { if (SEGLEN <= 1) FX_FALLBACK_STATIC; unsigned maxData = FAIR_DATA_PER_SEG; //ESP8266: 256 ESP32: 640 unsigned segs = strip.getActiveSegmentsNum(); if (segs <= (strip.getMaxSegments() /2)) maxData *= 2; //ESP8266: 512 if <= 8 segs ESP32: 1280 if <= 16 segs if (segs <= (strip.getMaxSegments() /4)) maxData *= 2; //ESP8266: 1024 if <= 4 segs ESP32: 2560 if <= 8 segs unsigned maxStars = maxData / sizeof(star); //ESP8266: max. 4/9/19 stars/seg, ESP32: max. 10/21/42 stars/seg unsigned numStars = 1 + (SEGLEN >> 3); if (numStars > maxStars) numStars = maxStars; unsigned dataSize = sizeof(star) * numStars; if (!SEGENV.allocateData(dataSize)) FX_FALLBACK_STATIC; //allocation failed uint32_t it = strip.now; star* stars = reinterpret_cast(SEGENV.data); float maxSpeed = 375.0f; // Max velocity float particleIgnition = 250.0f; // How long to "flash" float particleFadeTime = 1500.0f; // Fade out time for (unsigned j = 0; j < numStars; j++) { // speed to adjust chance of a burst, max is nearly always. if (hw_random8((144-(SEGMENT.speed >> 1))) == 0 && stars[j].birth == 0) { // Pick a random color and location. unsigned startPos = hw_random16(SEGLEN-1); float multiplier = (float)(hw_random8())/255.0f * 1.0f; stars[j].color = CRGB(SEGMENT.color_wheel(hw_random8())); stars[j].pos = startPos; stars[j].vel = maxSpeed * (float)(hw_random8())/255.0f * multiplier; stars[j].birth = it; stars[j].last = it; // more fragments means larger burst effect int num = hw_random8(3,6 + (SEGMENT.intensity >> 5)); for (int i=0; i < STARBURST_MAX_FRAG; i++) { if (i < num) stars[j].fragment[i] = startPos; else stars[j].fragment[i] = -1; } } } if (!SEGMENT.check2) SEGMENT.fill(SEGCOLOR(1)); for (unsigned j=0; j> 1; if (stars[j].fragment[i] > 0) { //all fragments travel right, will be mirrored on other side stars[j].fragment[i] += stars[j].vel * dt * (float)var/3.0; } } stars[j].last = it; stars[j].vel -= 3*stars[j].vel*dt; } CRGB c = stars[j].color; // If the star is brand new, it flashes white briefly. // Otherwise it just fades over time. float fade = 0.0f; float age = it-stars[j].birth; if (age < particleIgnition) { c = CRGB(color_blend(WHITE, RGBW32(c.r,c.g,c.b,0), uint8_t(254.5f*((age / particleIgnition))))); } else { // Figure out how much to fade and shrink the star based on // its age relative to its lifetime if (age > particleIgnition + particleFadeTime) { fade = 1.0f; // Black hole, all faded out stars[j].birth = 0; c = CRGB(SEGCOLOR(1)); } else { age -= particleIgnition; fade = (age / particleFadeTime); // Fading star c = CRGB(color_blend(RGBW32(c.r,c.g,c.b,0), SEGCOLOR(1), uint8_t(254.5f*fade))); } } float particleSize = (1.0f - fade) * 2.0f; for (size_t index=0; index < STARBURST_MAX_FRAG*2; index++) { bool mirrored = index & 0x1; unsigned i = index >> 1; if (stars[j].fragment[i] > 0) { float loc = stars[j].fragment[i]; if (mirrored) loc -= (loc-stars[j].pos)*2; unsigned start = loc - particleSize; unsigned end = loc + particleSize; if (start < 0) start = 0; if (start == end) end++; if (end > SEGLEN) end = SEGLEN; for (unsigned p = start; p < end; p++) { SEGMENT.setPixelColor(p, c); } } } } } #undef STARBURST_MAX_FRAG static const char _data_FX_MODE_STARBURST[] PROGMEM = "Fireworks Starburst@Chance,Fragments,,,,,Overlay;,!;!;;pal=11,m12=0"; #endif // WLED_PS_DONT_REPLACE_1DFX #if defined(WLED_PS_DONT_REPLACE_1D_FX) || defined(WLED_PS_DONT_REPLACE_2D_FX) /* * Exploding fireworks effect * adapted from: http://www.anirama.com/1000leds/1d-fireworks/ * adapted for 2D WLED by blazoncek (Blaz Kristan (AKA blazoncek)) */ void mode_exploding_fireworks(void) { if (SEGLEN <= 1) FX_FALLBACK_STATIC; const int cols = SEGMENT.is2D() ? SEG_W : 1; const int rows = SEGMENT.is2D() ? SEG_H : SEGLEN; //allocate segment data unsigned maxData = FAIR_DATA_PER_SEG; //ESP8266: 256 ESP32: 640 unsigned segs = strip.getActiveSegmentsNum(); if (segs <= (strip.getMaxSegments() /2)) maxData *= 2; //ESP8266: 512 if <= 8 segs ESP32: 1280 if <= 16 segs if (segs <= (strip.getMaxSegments() /4)) maxData *= 2; //ESP8266: 1024 if <= 4 segs ESP32: 2560 if <= 8 segs int maxSparks = maxData / sizeof(spark); //ESP8266: max. 21/42/85 sparks/seg, ESP32: max. 53/106/213 sparks/seg unsigned numSparks = min(5 + ((rows*cols) >> 1), maxSparks); unsigned dataSize = sizeof(spark) * numSparks; if (!SEGENV.allocateData(dataSize + sizeof(float))) FX_FALLBACK_STATIC; //allocation failed float *dying_gravity = reinterpret_cast(SEGENV.data + dataSize); if (dataSize != SEGENV.aux1) { //reset to flare if sparks were reallocated (it may be good idea to reset segment if bounds change) *dying_gravity = 0.0f; SEGENV.aux0 = 0; SEGENV.aux1 = dataSize; } SEGMENT.fade_out(252); Spark* sparks = reinterpret_cast(SEGENV.data); Spark* flare = sparks; //first spark is flare data float gravity = -0.0004f - (SEGMENT.speed/800000.0f); // m/s/s gravity *= rows; if (SEGENV.aux0 < 2) { //FLARE if (SEGENV.aux0 == 0) { //init flare flare->pos = 0; flare->posX = SEGMENT.is2D() ? hw_random16(2,cols-3) : (SEGMENT.intensity > hw_random8()); // will enable random firing side on 1D unsigned peakHeight = 75 + hw_random8(180); //0-255 peakHeight = (peakHeight * (rows -1)) >> 8; flare->vel = sqrtf(-2.0f * gravity * peakHeight); flare->velX = SEGMENT.is2D() ? (hw_random8(9)-4)/64.0f : 0; // no X velocity on 1D flare->col = 255; //brightness SEGENV.aux0 = 1; } // launch if (flare->vel > 12 * gravity) { // flare if (SEGMENT.is2D()) SEGMENT.setPixelColorXY(unsigned(flare->posX), rows - uint16_t(flare->pos) - 1, flare->col, flare->col, flare->col); else SEGMENT.setPixelColor((flare->posX > 0.0f) ? rows - int(flare->pos) - 1 : int(flare->pos), flare->col, flare->col, flare->col); flare->pos += flare->vel; flare->pos = constrain(flare->pos, 0, rows-1); if (SEGMENT.is2D()) { flare->posX += flare->velX; flare->posX = constrain(flare->posX, 0, cols-1); } flare->vel += gravity; flare->col -= 2; } else { SEGENV.aux0 = 2; // ready to explode } } else if (SEGENV.aux0 < 4) { /* * Explode! * * Explosion happens where the flare ended. * Size is proportional to the height. */ unsigned nSparks = flare->pos + hw_random8(4); nSparks = std::max(nSparks, 4U); // This is not a standard constrain; numSparks is not guaranteed to be at least 4 nSparks = std::min(nSparks, numSparks); // initialize sparks if (SEGENV.aux0 == 2) { for (unsigned i = 1; i < nSparks; i++) { sparks[i].pos = flare->pos; sparks[i].posX = flare->posX; sparks[i].vel = (float(hw_random16(20001)) / 10000.0f) - 0.9f; // from -0.9 to 1.1 sparks[i].vel *= rows<32 ? 0.5f : 1; // reduce velocity for smaller strips sparks[i].velX = SEGMENT.is2D() ? (float(hw_random16(20001)) / 10000.0f) - 1.0f : 0; // from -1 to 1 sparks[i].col = 345;//abs(sparks[i].vel * 750.0); // set colors before scaling velocity to keep them bright //sparks[i].col = constrain(sparks[i].col, 0, 345); sparks[i].colIndex = hw_random8(); sparks[i].vel *= flare->pos/rows; // proportional to height sparks[i].velX *= SEGMENT.is2D() ? flare->posX/cols : 0; // proportional to width sparks[i].vel *= -gravity *50; } //sparks[1].col = 345; // this will be our known spark *dying_gravity = gravity/2; SEGENV.aux0 = 3; } if (sparks[1].col > 4) {//&& sparks[1].pos > 0) { // as long as our known spark is lit, work with all the sparks for (unsigned i = 1; i < nSparks; i++) { sparks[i].pos += sparks[i].vel; sparks[i].posX += sparks[i].velX; sparks[i].vel += *dying_gravity; sparks[i].velX += SEGMENT.is2D() ? *dying_gravity : 0; if (sparks[i].col > 3) sparks[i].col -= 4; if (sparks[i].pos > 0 && sparks[i].pos < rows) { if (SEGMENT.is2D() && !(sparks[i].posX >= 0 && sparks[i].posX < cols)) continue; unsigned prog = sparks[i].col; uint32_t spColor = (SEGMENT.palette) ? SEGMENT.color_wheel(sparks[i].colIndex) : SEGCOLOR(0); CRGBW c = BLACK; //HeatColor(sparks[i].col); if (prog > 300) { //fade from white to spark color c = color_blend(spColor, WHITE, uint8_t((prog - 300)*5)); } else if (prog > 45) { //fade from spark color to black c = color_blend(BLACK, spColor, uint8_t(prog - 45)); unsigned cooling = (300 - prog) >> 5; c.g = qsub8(c.g, cooling); c.b = qsub8(c.b, cooling * 2); } if (SEGMENT.is2D()) SEGMENT.setPixelColorXY(int(sparks[i].posX), rows - int(sparks[i].pos) - 1, c); else SEGMENT.setPixelColor(int(sparks[i].posX) ? rows - int(sparks[i].pos) - 1 : int(sparks[i].pos), c); } } if (SEGMENT.check3) SEGMENT.blur(16); *dying_gravity *= .8f; // as sparks burn out they fall slower } else { SEGENV.aux0 = 6 + hw_random8(10); //wait for this many frames } } else { SEGENV.aux0--; if (SEGENV.aux0 < 4) { SEGENV.aux0 = 0; //back to flare } } } #undef MAX_SPARKS static const char _data_FX_MODE_EXPLODING_FIREWORKS[] PROGMEM = "Fireworks 1D@Gravity,Firing side;!,!;!;12;pal=11,ix=128"; #endif // WLED_PS_DONT_REPLACE_x_FX /* * Drip Effect * ported of: https://www.youtube.com/watch?v=sru2fXh4r7k */ void mode_drip(void) { if (SEGLEN <= 1) FX_FALLBACK_STATIC; //allocate segment data unsigned strips = SEGMENT.nrOfVStrips(); const int maxNumDrops = 4; unsigned dataSize = sizeof(spark) * maxNumDrops; if (!SEGENV.allocateData(dataSize * strips)) FX_FALLBACK_STATIC; //allocation failed Spark* drops = reinterpret_cast(SEGENV.data); if (!SEGMENT.check2) SEGMENT.fill(SEGCOLOR(1)); struct virtualStrip { static void runStrip(uint16_t stripNr, Spark* drops) { unsigned numDrops = 1 + (SEGMENT.intensity >> 6); // 255>>6 = 3 float gravity = -0.0005f - (SEGMENT.speed/50000.0f); gravity *= max(1, (int)SEGLEN-1); int sourcedrop = 12; for (unsigned j=0;j255) drops[j].col=255; SEGMENT.setPixelColor(indexToVStrip(uint16_t(drops[j].pos), stripNr), color_blend(BLACK,SEGCOLOR(0),uint8_t(drops[j].col))); drops[j].col += map(SEGMENT.speed, 0, 255, 1, 6); // swelling if (hw_random8() < drops[j].col/10) { // random drop drops[j].colIndex=2; //fall drops[j].col=255; } } if (drops[j].colIndex > 1) { // falling if (drops[j].pos > 0) { // fall until end of segment drops[j].pos += drops[j].vel; if (drops[j].pos < 0) drops[j].pos = 0; drops[j].vel += gravity; // gravity is negative for (int i=1;i<7-drops[j].colIndex;i++) { // some minor math so we don't expand bouncing droplets unsigned pos = constrain(unsigned(drops[j].pos) +i, 0, SEGLEN-1); //this is BAD, returns a pos >= SEGLEN occasionally SEGMENT.setPixelColor(indexToVStrip(pos, stripNr), color_blend(BLACK,SEGCOLOR(0),uint8_t(drops[j].col/i))); //spread pixel with fade while falling } if (drops[j].colIndex > 2) { // during bounce, some water is on the floor SEGMENT.setPixelColor(indexToVStrip(0, stripNr), color_blend(SEGCOLOR(0),BLACK,uint8_t(drops[j].col))); } } else { // we hit bottom if (drops[j].colIndex > 2) { // already hit once, so back to forming drops[j].colIndex = 0; drops[j].col = sourcedrop; } else { if (drops[j].colIndex==2) { // init bounce drops[j].vel = -drops[j].vel/4;// reverse velocity with damping drops[j].pos += drops[j].vel; } drops[j].col = sourcedrop*2; drops[j].colIndex = 5; // bouncing } } } } } }; for (unsigned stripNr=0; stripNr(SEGENV.data); //if (SEGENV.call == 0) SEGMENT.fill(SEGCOLOR(1)); // will fill entire segment (1D or 2D), then use drop->step = 0 below // virtualStrip idea by @ewowi (Ewoud Wijma) // requires virtual strip # to be embedded into upper 16 bits of index in setPixelcolor() // the following functions will not work on virtual strips: fill(), fade_out(), fadeToBlack(), blur() struct virtualStrip { static void runStrip(size_t stripNr, Tetris *drop) { // initialize dropping on first call or segment full if (SEGENV.call == 0) { drop->stack = 0; // reset brick stack size drop->step = strip.now + 2000; // start by fading out strip if (SEGMENT.check1) drop->col = 0;// use only one color from palette } if (drop->step == 0) { // init brick // speed calculation: a single brick should reach bottom of strip in X seconds // if the speed is set to 1 this should take 5s and at 255 it should take 0.25s // as this is dependant on SEGLEN it should be taken into account and the fact that effect runs every FRAMETIME s int speed = SEGMENT.speed ? SEGMENT.speed : hw_random8(1,255); speed = map(speed, 1, 255, 5000, 250); // time taken for full (SEGLEN) drop drop->speed = float(SEGLEN * FRAMETIME) / float(speed); // set speed drop->pos = SEGLEN; // start at end of segment (no need to subtract 1) if (!SEGMENT.check1) drop->col = hw_random8(0,15)<<4; // limit color choices so there is enough HUE gap drop->step = 1; // drop state (0 init, 1 forming, 2 falling) drop->brick = (SEGMENT.intensity ? (SEGMENT.intensity>>5)+1 : hw_random8(1,5)) * (1+(SEGLEN>>6)); // size of brick } if (drop->step == 1) { // forming if (hw_random8()>>6) { // random drop drop->step = 2; // fall } } if (drop->step == 2) { // falling if (drop->pos > drop->stack) { // fall until top of stack drop->pos -= drop->speed; // may add gravity as: speed += gravity if (int(drop->pos) < int(drop->stack)) drop->pos = drop->stack; for (unsigned i = unsigned(drop->pos); i < SEGLEN; i++) { uint32_t col = i < unsigned(drop->pos)+drop->brick ? SEGMENT.color_from_palette(drop->col, false, false, 0) : SEGCOLOR(1); SEGMENT.setPixelColor(indexToVStrip(i, stripNr), col); } } else { // we hit bottom drop->step = 0; // proceed with next brick, go back to init drop->stack += drop->brick; // increase the stack size if (drop->stack >= SEGLEN) drop->step = strip.now + 2000; // fade out stack } } if (drop->step > 2) { // fade strip drop->brick = 0; // reset brick size (no more growing) if (drop->step > strip.now) { // allow fading of virtual strip for (unsigned i = 0; i < SEGLEN; i++) SEGMENT.blendPixelColor(indexToVStrip(i, stripNr), SEGCOLOR(1), 25); // 10% blend } else { drop->stack = 0; // reset brick stack size drop->step = 0; // proceed with next brick if (SEGMENT.check1) drop->col += 8; // gradually increase palette index } } } }; for (unsigned stripNr=0; stripNr> 5))+thisPhase) & 0xFF)/2 // factor=23 // Create a wave and add a phase change and add another wave with its own phase change. + cos8_t((i*(1+ 2*(SEGMENT.speed >> 5))+thatPhase) & 0xFF)/2; // factor=15 // Hey, you can even change the frequencies if you wish. unsigned thisBright = qsub8(colorIndex, beatsin8_t(7,0, (128 - (SEGMENT.intensity>>1)))); SEGMENT.setPixelColor(i, SEGMENT.color_from_palette(colorIndex, false, PALETTE_SOLID_WRAP, 0, thisBright)); } } static const char _data_FX_MODE_PLASMA[] PROGMEM = "Plasma@Phase,!;!;!"; /* * Percentage display * Intensity values from 0-100 turn on the leds. */ void mode_percent(void) { unsigned percent = SEGMENT.intensity; percent = constrain(percent, 0, 200); unsigned active_leds = (percent < 100) ? roundf(SEGLEN * percent / 100.0f) : roundf(SEGLEN * (200 - percent) / 100.0f); unsigned size = (1 + ((SEGMENT.speed * SEGLEN) >> 11)); if (SEGMENT.speed == 255) size = 255; if (percent <= 100) { for (unsigned i = 0; i < SEGLEN; i++) { if (i < SEGENV.aux1) { if (SEGMENT.check1) SEGMENT.setPixelColor(i, SEGMENT.color_from_palette(map(percent,0,100,0,255), false, false, 0)); else SEGMENT.setPixelColor(i, SEGMENT.color_from_palette(i, true, PALETTE_SOLID_WRAP, 0)); } else { SEGMENT.setPixelColor(i, SEGCOLOR(1)); } } } else { for (unsigned i = 0; i < SEGLEN; i++) { if (i < (SEGLEN - SEGENV.aux1)) { SEGMENT.setPixelColor(i, SEGCOLOR(1)); } else { if (SEGMENT.check1) SEGMENT.setPixelColor(i, SEGMENT.color_from_palette(map(percent,100,200,255,0), false, false, 0)); else SEGMENT.setPixelColor(i, SEGMENT.color_from_palette(i, true, PALETTE_SOLID_WRAP, 0)); } } } if(active_leds > SEGENV.aux1) { // smooth transition to the target value SEGENV.aux1 += size; if (SEGENV.aux1 > active_leds) SEGENV.aux1 = active_leds; } else if (active_leds < SEGENV.aux1) { if (SEGENV.aux1 > size) SEGENV.aux1 -= size; else SEGENV.aux1 = 0; if (SEGENV.aux1 < active_leds) SEGENV.aux1 = active_leds; } } static const char _data_FX_MODE_PERCENT[] PROGMEM = "Percent@!,% of fill,,,,One color;!,!;!"; /* * Modulates the brightness similar to a heartbeat * (unimplemented?) tries to draw an ECG approximation on a 2D matrix */ void mode_heartbeat(void) { unsigned bpm = 40 + (SEGMENT.speed >> 3); uint32_t msPerBeat = (60000L / bpm); uint32_t secondBeat = (msPerBeat / 3); uint32_t bri_lower = SEGENV.aux1; unsigned long beatTimer = strip.now - SEGENV.step; bri_lower = bri_lower * 2042 / (2048 + SEGMENT.intensity); SEGENV.aux1 = bri_lower; if ((beatTimer > secondBeat) && !SEGENV.aux0) { // time for the second beat? SEGENV.aux1 = UINT16_MAX; //3/4 bri SEGENV.aux0 = 1; } if (beatTimer > msPerBeat) { // time to reset the beat timer? SEGENV.aux1 = UINT16_MAX; //full bri SEGENV.aux0 = 0; SEGENV.step = strip.now; } for (unsigned i = 0; i < SEGLEN; i++) { SEGMENT.setPixelColor(i, color_blend(SEGMENT.color_from_palette(i, true, PALETTE_SOLID_WRAP, 0), SEGCOLOR(1), uint8_t(255 - (SEGENV.aux1 >> 8)))); } } static const char _data_FX_MODE_HEARTBEAT[] PROGMEM = "Heartbeat@!,!;!,!;!;01;m12=1"; // "Pacifica" // Gentle, blue-green ocean waves. // December 2019, Mark Kriegsman and Mary Corey March. // For Dan. // // // In this animation, there are four "layers" of waves of light. // // Each layer moves independently, and each is scaled separately. // // All four wave layers are added together on top of each other, and then // another filter is applied that adds "whitecaps" of brightness where the // waves line up with each other more. Finally, another pass is taken // over the led array to 'deepen' (dim) the blues and greens. // // The speed and scale and motion each layer varies slowly within independent // hand-chosen ranges, which is why the code has a lot of low-speed 'beatsin8' functions // with a lot of oddly specific numeric ranges. // // These three custom blue-green color palettes were inspired by the colors found in // the waters off the southern coast of California, https://goo.gl/maps/QQgd97jjHesHZVxQ7 // // Modified for WLED, based on https://github.com/FastLED/FastLED/blob/master/examples/Pacifica/Pacifica.ino // // Add one layer of waves into the led array static CRGB pacifica_one_layer(uint16_t i, const CRGBPalette16& p, uint16_t cistart, uint16_t wavescale, uint8_t bri, uint16_t ioff) { unsigned ci = cistart; unsigned waveangle = ioff; unsigned wavescale_half = (wavescale >> 1) + 20; waveangle += ((120 + SEGMENT.intensity) * i); //original 250 * i unsigned s16 = sin16_t(waveangle) + 32768; unsigned cs = scale16(s16, wavescale_half) + wavescale_half; ci += (cs * i); unsigned sindex16 = sin16_t(ci) + 32768; unsigned sindex8 = scale16(sindex16, 240); return CRGB(ColorFromPalette(p, sindex8, bri, LINEARBLEND)); } void mode_pacifica() { uint32_t nowOld = strip.now; CRGBPalette16 pacifica_palette_1 = { 0x000507, 0x000409, 0x00030B, 0x00030D, 0x000210, 0x000212, 0x000114, 0x000117, 0x000019, 0x00001C, 0x000026, 0x000031, 0x00003B, 0x000046, 0x14554B, 0x28AA50 }; CRGBPalette16 pacifica_palette_2 = { 0x000507, 0x000409, 0x00030B, 0x00030D, 0x000210, 0x000212, 0x000114, 0x000117, 0x000019, 0x00001C, 0x000026, 0x000031, 0x00003B, 0x000046, 0x0C5F52, 0x19BE5F }; CRGBPalette16 pacifica_palette_3 = { 0x000208, 0x00030E, 0x000514, 0x00061A, 0x000820, 0x000927, 0x000B2D, 0x000C33, 0x000E39, 0x001040, 0x001450, 0x001860, 0x001C70, 0x002080, 0x1040BF, 0x2060FF }; if (SEGMENT.palette) { pacifica_palette_1 = SEGPALETTE; pacifica_palette_2 = SEGPALETTE; pacifica_palette_3 = SEGPALETTE; } // Increment the four "color index start" counters, one for each wave layer. // Each is incremented at a different speed, and the speeds vary over time. unsigned sCIStart1 = SEGENV.aux0, sCIStart2 = SEGENV.aux1, sCIStart3 = SEGENV.step & 0xFFFF, sCIStart4 = (SEGENV.step >> 16); uint32_t deltams = (FRAMETIME >> 2) + ((FRAMETIME * SEGMENT.speed) >> 7); uint64_t deltat = (strip.now >> 2) + ((strip.now * SEGMENT.speed) >> 7); strip.now = deltat; unsigned speedfactor1 = beatsin16_t(3, 179, 269); unsigned speedfactor2 = beatsin16_t(4, 179, 269); uint32_t deltams1 = (deltams * speedfactor1) / 256; uint32_t deltams2 = (deltams * speedfactor2) / 256; uint32_t deltams21 = (deltams1 + deltams2) / 2; sCIStart1 += (deltams1 * beatsin88_t(1011,10,13)); sCIStart2 -= (deltams21 * beatsin88_t(777,8,11)); sCIStart3 -= (deltams1 * beatsin88_t(501,5,7)); sCIStart4 -= (deltams2 * beatsin88_t(257,4,6)); SEGENV.aux0 = sCIStart1; SEGENV.aux1 = sCIStart2; SEGENV.step = (sCIStart4 << 16) | (sCIStart3 & 0xFFFF); // Clear out the LED array to a dim background blue-green //SEGMENT.fill(132618); unsigned basethreshold = beatsin8_t( 9, 55, 65); unsigned wave = beat8( 7 ); for (unsigned i = 0; i < SEGLEN; i++) { CRGB c = CRGB(2, 6, 10); // Render each of four layers, with different scales and speeds, that vary over time c += pacifica_one_layer(i, pacifica_palette_1, sCIStart1, beatsin16_t(3, 11 * 256, 14 * 256), beatsin8_t(10, 70, 130), 0-beat16(301)); c += pacifica_one_layer(i, pacifica_palette_2, sCIStart2, beatsin16_t(4, 6 * 256, 9 * 256), beatsin8_t(17, 40, 80), beat16(401)); c += pacifica_one_layer(i, pacifica_palette_3, sCIStart3, 6 * 256 , beatsin8_t(9, 10,38) , 0-beat16(503)); c += pacifica_one_layer(i, pacifica_palette_3, sCIStart4, 5 * 256 , beatsin8_t(8, 10,28) , beat16(601)); // Add extra 'white' to areas where the four layers of light have lined up brightly unsigned threshold = scale8( sin8_t( wave), 20) + basethreshold; wave += 7; unsigned l = c.getAverageLight(); if (l > threshold) { unsigned overage = l - threshold; unsigned overage2 = qadd8(overage, overage); c += CRGB(overage, overage2, qadd8(overage2, overage2)); } //deepen the blues and greens c.blue = scale8(c.blue, 145); c.green = scale8(c.green, 200); c |= CRGB( 2, 5, 7); SEGMENT.setPixelColor(i, c); } strip.now = nowOld; } static const char _data_FX_MODE_PACIFICA[] PROGMEM = "Pacifica@!,Angle;;!;;pal=51"; /* * Mode simulates a gradual sunrise */ void mode_sunrise() { if (SEGLEN <= 1) FX_FALLBACK_STATIC; //speed 0 - static sun //speed 1 - 60: sunrise time in minutes //speed 60 - 120 : sunset time in minutes - 60; //speed above: "breathing" rise and set if (SEGENV.call == 0 || SEGMENT.speed != SEGENV.aux0) { SEGENV.step = millis(); //save starting time, millis() because strip.now can change from sync SEGENV.aux0 = SEGMENT.speed; } SEGMENT.fill(BLACK); unsigned stage = 0xFFFF; uint32_t s10SinceStart = (millis() - SEGENV.step) /100; //tenths of seconds if (SEGMENT.speed > 120) { //quick sunrise and sunset unsigned counter = (strip.now >> 1) * (((SEGMENT.speed -120) >> 1) +1); stage = triwave16(counter); } else if (SEGMENT.speed) { //sunrise unsigned durMins = SEGMENT.speed; if (durMins > 60) durMins -= 60; uint32_t s10Target = durMins * 600; if (s10SinceStart > s10Target) s10SinceStart = s10Target; stage = map(s10SinceStart, 0, s10Target, 0, 0xFFFF); if (SEGMENT.speed > 60) stage = 0xFFFF - stage; //sunset } for (unsigned i = 0; i <= SEGLEN/2; i++) { //default palette is Fire unsigned wave = triwave16((i * stage) / SEGLEN); wave = (wave >> 8) + ((wave * SEGMENT.intensity) >> 15); uint32_t c; if (wave > 240) { //clipped, full white sun c = SEGMENT.color_from_palette( 240, false, true, 255); } else { //transition c = SEGMENT.color_from_palette(wave, false, true, 255); } SEGMENT.setPixelColor(i, c); SEGMENT.setPixelColor(SEGLEN - i - 1, c); } } static const char _data_FX_MODE_SUNRISE[] PROGMEM = "Sunrise@Time [min],Width;;!;;pal=35,sx=60"; /* * Effects by Andrew Tuline */ static void phased_base(uint8_t moder) { // We're making sine waves here. By Andrew Tuline. unsigned allfreq = 16; // Base frequency. float *phase = reinterpret_cast(&SEGENV.step); // Phase change value gets calculated (float fits into unsigned long). unsigned cutOff = (255-SEGMENT.intensity); // You can change the number of pixels. AKA INTENSITY (was 192). unsigned modVal = 5;//SEGMENT.fft1/8+1; // You can change the modulus. AKA FFT1 (was 5). unsigned index = strip.now/64; // Set color rotation speed *phase += SEGMENT.speed/32.0; // You can change the speed of the wave. AKA SPEED (was .4) for (unsigned i = 0; i < SEGLEN; i++) { if (moder == 1) modVal = (perlin8(i*10 + i*10) /16); // Let's randomize our mod length with some Perlin noise. unsigned val = (i+1) * allfreq; // This sets the frequency of the waves. The +1 makes sure that led 0 is used. if (modVal == 0) modVal = 1; val += *phase * (i % modVal +1) /2; // This sets the varying phase change of the waves. By Andrew Tuline. unsigned b = cubicwave8(val); // Now we make an 8 bit sinewave. b = (b > cutOff) ? (b - cutOff) : 0; // A ternary operator to cutoff the light. SEGMENT.setPixelColor(i, color_blend(SEGCOLOR(1), SEGMENT.color_from_palette(index, false, false, 0), uint8_t(b))); index += 256 / SEGLEN; if (SEGLEN > 256) index ++; // Correction for segments longer than 256 LEDs } } void mode_phased(void) { phased_base(0); } static const char _data_FX_MODE_PHASED[] PROGMEM = "Phased@!,!;!,!;!"; void mode_phased_noise(void) { phased_base(1); } static const char _data_FX_MODE_PHASEDNOISE[] PROGMEM = "Phased Noise@!,!;!,!;!"; void mode_twinkleup(void) { // A very short twinkle routine with fade-in and dual controls. By Andrew Tuline. unsigned prevSeed = random16_get_seed(); // save seed so we can restore it at the end of the function random16_set_seed(535); // The randomizer needs to be re-set each time through the loop in order for the same 'random' numbers to be the same each time through. for (unsigned i = 0; i < SEGLEN; i++) { unsigned ranstart = random8(); // The starting value (aka brightness) for each pixel. Must be consistent each time through the loop for this to work. unsigned pixBri = sin8_t(ranstart + 16 * strip.now/(256-SEGMENT.speed)); if (random8() > SEGMENT.intensity) pixBri = 0; SEGMENT.setPixelColor(i, color_blend(SEGCOLOR(1), SEGMENT.color_from_palette(random8()+strip.now/100, false, PALETTE_SOLID_WRAP, 0), pixBri)); } random16_set_seed(prevSeed); // restore original seed so other effects can use "random" PRNG } static const char _data_FX_MODE_TWINKLEUP[] PROGMEM = "Twinkleup@!,Intensity;!,!;!;;m12=0"; // Peaceful noise that's slow and with gradually changing palettes. Does not support WLED palettes or default colours or controls. void mode_noisepal(void) { // Slow noise palette by Andrew Tuline. unsigned scale = 15 + (SEGMENT.intensity >> 2); //default was 30 //#define scale 30 unsigned dataSize = sizeof(CRGBPalette16) * 2; //allocate space for 2 Palettes (2 * 16 * 3 = 96 bytes) if (!SEGENV.allocateData(dataSize)) FX_FALLBACK_STATIC; //allocation failed CRGBPalette16* palettes = reinterpret_cast(SEGENV.data); unsigned changePaletteMs = 4000 + SEGMENT.speed *10; //between 4 - 6.5sec if (strip.now - SEGENV.step > changePaletteMs) { SEGENV.step = strip.now; unsigned baseI = hw_random8(); palettes[1] = CRGBPalette16(CHSV(baseI+hw_random8(64), 255, hw_random8(128,255)), CHSV(baseI+128, 255, hw_random8(128,255)), CHSV(baseI+hw_random8(92), 192, hw_random8(128,255)), CHSV(baseI+hw_random8(92), 255, hw_random8(128,255))); } //EVERY_N_MILLIS(10) { //(don't have to time this, effect function is only called every 24ms) nblendPaletteTowardPalette(palettes[0], palettes[1], 48); // Blend towards the target palette over 48 iterations. if (SEGMENT.palette > 0) palettes[0] = SEGPALETTE; for (unsigned i = 0; i < SEGLEN; i++) { unsigned index = perlin8(i*scale, SEGENV.aux0+i*scale); // Get a value from the noise function. I'm using both x and y axis. SEGMENT.setPixelColor(i, ColorFromPalette(palettes[0], index, 255, LINEARBLEND)); // Use my own palette. } SEGENV.aux0 += beatsin8_t(10,1,4); // Moving along the distance. Vary it a bit with a sine wave. } static const char _data_FX_MODE_NOISEPAL[] PROGMEM = "Noise Pal@!,Scale;;!"; // Sine waves that have controllable phase change speed, frequency and cutoff. By Andrew Tuline. // SEGMENT.speed ->Speed, SEGMENT.intensity -> Frequency (SEGMENT.fft1 -> Color change, SEGMENT.fft2 -> PWM cutoff) // void mode_sinewave(void) { // Adjustable sinewave. By Andrew Tuline //#define qsuba(x, b) ((x>b)?x-b:0) // Analog Unsigned subtraction macro. if result <0, then => 0 unsigned colorIndex = strip.now /32;//(256 - SEGMENT.fft1); // Amount of colour change. SEGENV.step += SEGMENT.speed/16; // Speed of animation. unsigned freq = SEGMENT.intensity/4;//SEGMENT.fft2/8; // Frequency of the signal. for (unsigned i = 0; i < SEGLEN; i++) { // For each of the LED's in the strand, set a brightness based on a wave as follows: uint8_t pixBri = cubicwave8((i*freq)+SEGENV.step);//qsuba(cubicwave8((i*freq)+SEGENV.step), (255-SEGMENT.intensity)); // qsub sets a minimum value called thiscutoff. If < thiscutoff, then bright = 0. Otherwise, bright = 128 (as defined in qsub).. //setPixCol(i, i*colorIndex/255, pixBri); SEGMENT.setPixelColor(i, color_blend(SEGCOLOR(1), SEGMENT.color_from_palette(i*colorIndex/255, false, PALETTE_SOLID_WRAP, 0), pixBri)); } } static const char _data_FX_MODE_SINEWAVE[] PROGMEM = "Sine@!,Scale;;!"; /* * Best of both worlds from Palette and Spot effects. By Aircoookie */ void mode_flow(void) { unsigned counter = 0; if (SEGMENT.speed != 0) { counter = strip.now * ((SEGMENT.speed >> 2) +1); counter = counter >> 8; } unsigned maxZones = SEGLEN / 6; //only looks good if each zone has at least 6 LEDs int zones = (SEGMENT.intensity * maxZones) >> 8; if (zones & 0x01) zones++; //zones must be even if (zones < 2) zones = 2; int zoneLen = SEGLEN / zones; zones += 2; //add two extra zones to cover beginning and end of segment (compensate integer truncation) int offset = ((int)SEGLEN - (zones * zoneLen)) / 2; // center the zones on the segment (can not use bit shift on negative number) for (int z = 0; z < zones; z++) { int pos = offset + z * zoneLen; for (int i = 0; i < zoneLen; i++) { unsigned colorIndex = (i * 255 / zoneLen) - counter; int led = (z & 0x01) ? i : (zoneLen -1) -i; if (SEGMENT.reverse) led = (zoneLen -1) -led; SEGMENT.setPixelColor(pos + led, SEGMENT.color_from_palette(colorIndex, false, true, 255)); } } } static const char _data_FX_MODE_FLOW[] PROGMEM = "Flow@!,Zones;;!;;m12=1"; //vertical /* * Dots waving around in a sine/pendulum motion. * Little pixel birds flying in a circle. By Aircoookie */ void mode_chunchun(void) { if (SEGLEN <= 1) FX_FALLBACK_STATIC; SEGMENT.fade_out(254); // add a bit of trail unsigned counter = strip.now * (6 + (SEGMENT.speed >> 4)); unsigned numBirds = 2 + (SEGLEN >> 3); // 2 + 1/8 of a segment unsigned span = (SEGMENT.intensity << 8) / numBirds; for (unsigned i = 0; i < numBirds; i++) { counter -= span; unsigned megumin = sin16_t(counter) + 0x8000; unsigned bird = uint32_t(megumin * SEGLEN) >> 16; bird = constrain(bird, 0U, SEGLEN-1U); SEGMENT.setPixelColor(bird, SEGMENT.color_from_palette((i * 255)/ numBirds, false, false, 0)); // no palette wrapping } } static const char _data_FX_MODE_CHUNCHUN[] PROGMEM = "Chunchun@!,Gap size;!,!;!"; #define SPOT_TYPE_SOLID 0 #define SPOT_TYPE_GRADIENT 1 #define SPOT_TYPE_2X_GRADIENT 2 #define SPOT_TYPE_2X_DOT 3 #define SPOT_TYPE_3X_DOT 4 #define SPOT_TYPE_4X_DOT 5 #define SPOT_TYPES_COUNT 6 #ifdef ESP8266 #define SPOT_MAX_COUNT 17 //Number of simultaneous waves #else #define SPOT_MAX_COUNT 49 //Number of simultaneous waves #endif #ifdef WLED_PS_DONT_REPLACE_1D_FX //13 bytes typedef struct Spotlight { float speed; uint8_t colorIdx; int16_t position; unsigned long lastUpdateTime; uint8_t width; uint8_t type; } spotlight; /* * Spotlights moving back and forth that cast dancing shadows. * Shine this through tree branches/leaves or other close-up objects that cast * interesting shadows onto a ceiling or tarp. * * By Steve Pomeroy @xxv */ void mode_dancing_shadows(void) { if (SEGLEN <= 1) FX_FALLBACK_STATIC; unsigned numSpotlights = map(SEGMENT.intensity, 0, 255, 2, SPOT_MAX_COUNT); // 49 on 32 segment ESP32, 17 on 16 segment ESP8266 bool initialize = SEGENV.aux0 != numSpotlights; SEGENV.aux0 = numSpotlights; unsigned dataSize = sizeof(spotlight) * numSpotlights; if (!SEGENV.allocateData(dataSize)) FX_FALLBACK_STATIC; //allocation failed Spotlight* spotlights = reinterpret_cast(SEGENV.data); SEGMENT.fill(BLACK); unsigned long time = strip.now; bool respawn = false; for (size_t i = 0; i < numSpotlights; i++) { if (!initialize) { // advance the position of the spotlight int delta = (float)(time - spotlights[i].lastUpdateTime) * (spotlights[i].speed * ((1.0 + SEGMENT.speed)/100.0)); if (abs(delta) >= 1) { spotlights[i].position += delta; spotlights[i].lastUpdateTime = time; } respawn = (spotlights[i].speed > 0.0 && spotlights[i].position > (int)(SEGLEN + 2)) || (spotlights[i].speed < 0.0 && spotlights[i].position < -(spotlights[i].width + 2)); } if (initialize || respawn) { spotlights[i].colorIdx = hw_random8(); spotlights[i].width = hw_random8(1, 10); spotlights[i].speed = 1.0/hw_random8(4, 50); if (initialize) { spotlights[i].position = hw_random16(SEGLEN); spotlights[i].speed *= hw_random8(2) ? 1.0 : -1.0; } else { if (hw_random8(2)) { spotlights[i].position = SEGLEN + spotlights[i].width; spotlights[i].speed *= -1.0; }else { spotlights[i].position = -spotlights[i].width; } } spotlights[i].lastUpdateTime = time; spotlights[i].type = hw_random8(SPOT_TYPES_COUNT); } uint32_t color = SEGMENT.color_from_palette(spotlights[i].colorIdx, false, false, 255); int start = spotlights[i].position; if (spotlights[i].width <= 1) { if (start >= 0 && start < (int)SEGLEN) { SEGMENT.blendPixelColor(start, color, 128); } } else { switch (spotlights[i].type) { case SPOT_TYPE_SOLID: for (size_t j = 0; j < spotlights[i].width; j++) { if ((start + j) >= 0 && (start + j) < SEGLEN) { SEGMENT.blendPixelColor(start + j, color, 128); } } break; case SPOT_TYPE_GRADIENT: for (size_t j = 0; j < spotlights[i].width; j++) { if ((start + j) >= 0 && (start + j) < SEGLEN) { SEGMENT.blendPixelColor(start + j, color, cubicwave8(map(j, 0, spotlights[i].width - 1, 0, 255))); } } break; case SPOT_TYPE_2X_GRADIENT: for (size_t j = 0; j < spotlights[i].width; j++) { if ((start + j) >= 0 && (start + j) < SEGLEN) { SEGMENT.blendPixelColor(start + j, color, cubicwave8(2 * map(j, 0, spotlights[i].width - 1, 0, 255))); } } break; case SPOT_TYPE_2X_DOT: for (size_t j = 0; j < spotlights[i].width; j += 2) { if ((start + j) >= 0 && (start + j) < SEGLEN) { SEGMENT.blendPixelColor(start + j, color, 128); } } break; case SPOT_TYPE_3X_DOT: for (size_t j = 0; j < spotlights[i].width; j += 3) { if ((start + j) >= 0 && (start + j) < SEGLEN) { SEGMENT.blendPixelColor(start + j, color, 128); } } break; case SPOT_TYPE_4X_DOT: for (size_t j = 0; j < spotlights[i].width; j += 4) { if ((start + j) >= 0 && (start + j) < SEGLEN) { SEGMENT.blendPixelColor(start + j, color, 128); } } break; } } } } static const char _data_FX_MODE_DANCING_SHADOWS[] PROGMEM = "Dancing Shadows@!,# of shadows;!;!"; #endif // WLED_PS_DONT_REPLACE_1D_FX /* Imitates a washing machine, rotating same waves forward, then pause, then backward. By Stefan Seegel */ void mode_washing_machine(void) { int speed = tristate_square8(strip.now >> 7, 90, 15); SEGENV.step += (speed * 2048) / (512 - SEGMENT.speed); for (unsigned i = 0; i < SEGLEN; i++) { uint8_t col = sin8_t(((SEGMENT.intensity / 25 + 1) * 255 * i / SEGLEN) + (SEGENV.step >> 7)); SEGMENT.setPixelColor(i, SEGMENT.color_from_palette(col, false, PALETTE_SOLID_WRAP, 3)); } } static const char _data_FX_MODE_WASHING_MACHINE[] PROGMEM = "Washing Machine@!,!;;!"; /* Image effect Draws a .gif image from filesystem on the matrix/strip */ void mode_image(void) { #ifndef WLED_ENABLE_GIF FX_FALLBACK_STATIC; #else renderImageToSegment(SEGMENT); #endif // if (status != 0 && status != 254 && status != 255) { // Serial.print("GIF renderer return: "); // Serial.println(status); // } } static const char _data_FX_MODE_IMAGE[] PROGMEM = "Image@!,Blur,;;;12;sx=128,ix=0"; /* Blends random colors across palette Modified, originally by Mark Kriegsman https://gist.github.com/kriegsman/1f7ccbbfa492a73c015e */ void mode_blends(void) { unsigned pixelLen = SEGLEN > UINT8_MAX ? UINT8_MAX : SEGLEN; unsigned dataSize = sizeof(uint32_t) * (pixelLen + 1); // max segment length of 56 pixels on 16 segment ESP8266 if (!SEGENV.allocateData(dataSize)) FX_FALLBACK_STATIC; //allocation failed uint32_t* pixels = reinterpret_cast(SEGENV.data); uint8_t blendSpeed = map(SEGMENT.intensity, 0, UINT8_MAX, 10, 128); unsigned shift = (strip.now * ((SEGMENT.speed >> 3) +1)) >> 8; for (unsigned i = 0; i < pixelLen; i++) { pixels[i] = color_blend(pixels[i], SEGMENT.color_from_palette(shift + quadwave8((i + 1) * 16), false, PALETTE_SOLID_WRAP, 255), blendSpeed); shift += 3; } unsigned offset = 0; for (unsigned i = 0; i < SEGLEN; i++) { SEGMENT.setPixelColor(i, pixels[offset++]); if (offset >= pixelLen) offset = 0; } } static const char _data_FX_MODE_BLENDS[] PROGMEM = "Blends@Shift speed,Blend speed;;!"; /* TV Simulator Modified and adapted to WLED by Def3nder, based on "Fake TV Light for Engineers" by Phillip Burgess https://learn.adafruit.com/fake-tv-light-for-engineers/arduino-sketch */ //43 bytes typedef struct TvSim { uint32_t totalTime = 0; uint32_t fadeTime = 0; uint32_t startTime = 0; uint32_t elapsed = 0; uint32_t pixelNum = 0; uint16_t sliderValues = 0; uint32_t sceeneStart = 0; uint32_t sceeneDuration = 0; uint16_t sceeneColorHue = 0; uint8_t sceeneColorSat = 0; uint8_t sceeneColorBri = 0; uint8_t actualColorR = 0; uint8_t actualColorG = 0; uint8_t actualColorB = 0; uint16_t pr = 0; // Prev R, G, B uint16_t pg = 0; uint16_t pb = 0; } tvSim; void mode_tv_simulator(void) { int nr, ng, nb, r, g, b, i, hue; uint8_t sat, bri, j; if (!SEGENV.allocateData(sizeof(tvSim))) FX_FALLBACK_STATIC; //allocation failed TvSim* tvSimulator = reinterpret_cast(SEGENV.data); uint8_t colorSpeed = map(SEGMENT.speed, 0, UINT8_MAX, 1, 20); uint8_t colorIntensity = map(SEGMENT.intensity, 0, UINT8_MAX, 10, 30); i = SEGMENT.speed << 8 | SEGMENT.intensity; if (i != tvSimulator->sliderValues) { tvSimulator->sliderValues = i; SEGENV.aux1 = 0; } // create a new sceene if (((strip.now - tvSimulator->sceeneStart) >= tvSimulator->sceeneDuration) || SEGENV.aux1 == 0) { tvSimulator->sceeneStart = strip.now; // remember the start of the new sceene tvSimulator->sceeneDuration = hw_random16(60* 250* colorSpeed, 60* 750 * colorSpeed); // duration of a "movie sceene" which has similar colors (5 to 15 minutes with max speed slider) tvSimulator->sceeneColorHue = hw_random16( 0, 768); // random start color-tone for the sceene tvSimulator->sceeneColorSat = hw_random8 ( 100, 130 + colorIntensity); // random start color-saturation for the sceene tvSimulator->sceeneColorBri = hw_random8 ( 200, 240); // random start color-brightness for the sceene SEGENV.aux1 = 1; SEGENV.aux0 = 0; } // slightly change the color-tone in this sceene if (SEGENV.aux0 == 0) { // hue change in both directions j = hw_random8(4 * colorIntensity); hue = (hw_random8() < 128) ? ((j < tvSimulator->sceeneColorHue) ? tvSimulator->sceeneColorHue - j : 767 - tvSimulator->sceeneColorHue - j) : // negative ((j + tvSimulator->sceeneColorHue) < 767 ? tvSimulator->sceeneColorHue + j : tvSimulator->sceeneColorHue + j - 767) ; // positive // saturation j = hw_random8(2 * colorIntensity); sat = (tvSimulator->sceeneColorSat - j) < 0 ? 0 : tvSimulator->sceeneColorSat - j; // brightness j = hw_random8(100); bri = (tvSimulator->sceeneColorBri - j) < 0 ? 0 : tvSimulator->sceeneColorBri - j; // calculate R,G,B from HSV // Source: https://blog.adafruit.com/2012/03/14/constant-brightness-hsb-to-rgb-algorithm/ { // just to create a local scope for the variables uint8_t temp[5], n = (hue >> 8) % 3; uint8_t x = ((((hue & 255) * sat) >> 8) * bri) >> 8; uint8_t s = ( (256 - sat) * bri) >> 8; temp[0] = temp[3] = s; temp[1] = temp[4] = x + s; temp[2] = bri - x; tvSimulator->actualColorR = temp[n + 2]; tvSimulator->actualColorG = temp[n + 1]; tvSimulator->actualColorB = temp[n ]; } } // Apply gamma correction, further expand to 16/16/16 nr = (uint8_t)gamma8(tvSimulator->actualColorR) * 257; // New R/G/B ng = (uint8_t)gamma8(tvSimulator->actualColorG) * 257; nb = (uint8_t)gamma8(tvSimulator->actualColorB) * 257; if (SEGENV.aux0 == 0) { // initialize next iteration SEGENV.aux0 = 1; // randomize total duration and fade duration for the actual color tvSimulator->totalTime = hw_random16(250, 2500); // Semi-random pixel-to-pixel time tvSimulator->fadeTime = hw_random16(0, tvSimulator->totalTime); // Pixel-to-pixel transition time if (hw_random8(10) < 3) tvSimulator->fadeTime = 0; // Force scene cut 30% of time tvSimulator->startTime = strip.now; } // end of initialization // how much time is elapsed ? tvSimulator->elapsed = strip.now - tvSimulator->startTime; // fade from prev color to next color if (tvSimulator->elapsed < tvSimulator->fadeTime) { r = map(tvSimulator->elapsed, 0, tvSimulator->fadeTime, tvSimulator->pr, nr); g = map(tvSimulator->elapsed, 0, tvSimulator->fadeTime, tvSimulator->pg, ng); b = map(tvSimulator->elapsed, 0, tvSimulator->fadeTime, tvSimulator->pb, nb); } else { // Avoid divide-by-zero in map() r = nr; g = ng; b = nb; } // set strip color for (i = 0; i < (int)SEGLEN; i++) { SEGMENT.setPixelColor(i, r >> 8, g >> 8, b >> 8); // Quantize to 8-bit } // if total duration has passed, remember last color and restart the loop if ( tvSimulator->elapsed >= tvSimulator->totalTime) { tvSimulator->pr = nr; // Prev RGB = new RGB tvSimulator->pg = ng; tvSimulator->pb = nb; SEGENV.aux0 = 0; } } static const char _data_FX_MODE_TV_SIMULATOR[] PROGMEM = "TV Simulator@!,!;;!;01"; /* Aurora effect by @Mazen improved and converted to integer math by @dedehai */ //CONFIG #ifdef ESP8266 #define W_MAX_COUNT 9 //Number of simultaneous waves #else #define W_MAX_COUNT 20 //Number of simultaneous waves #endif #define W_MAX_SPEED 6 //Higher number, higher speed #define W_WIDTH_FACTOR 6 //Higher number, smaller waves // fixed-point math scaling #define AW_SHIFT 16 #define AW_SCALE (1 << AW_SHIFT) // 65536 representing 1.0 // 32 bytes class AuroraWave { private: int32_t center; // scaled by AW_SCALE uint32_t ageFactor_cached; // cached age factor scaled by AW_SCALE uint16_t ttl; uint16_t age; uint16_t width; uint16_t basealpha; // scaled by AW_SCALE uint16_t speed_factor; // scaled by AW_SCALE int16_t wave_start; // wave start LED index int16_t wave_end; // wave end LED index bool goingleft; bool alive = true; CRGBW basecolor; public: void init(uint32_t segment_length, CRGBW color) { ttl = hw_random16(500, 1501); basecolor = color; basealpha = hw_random8(60, 100) * AW_SCALE / 100; // 0-99% note: if using 100% there is risk of integer overflow age = 0; width = hw_random16(segment_length / 20, segment_length / W_WIDTH_FACTOR) + 1; center = (((uint32_t)hw_random8(101) << AW_SHIFT) / 100) * segment_length; // 0-100% goingleft = hw_random8() & 0x01; // 50/50 chance speed_factor = (((uint32_t)hw_random8(10, 31) * W_MAX_SPEED) << AW_SHIFT) / (100 * 255); alive = true; } void updateCachedValues() { uint32_t half_ttl = ttl >> 1; if (age < half_ttl) { ageFactor_cached = ((uint32_t)age << AW_SHIFT) / half_ttl; } else { ageFactor_cached = ((uint32_t)(ttl - age) << AW_SHIFT) / half_ttl; } if (ageFactor_cached >= AW_SCALE) ageFactor_cached = AW_SCALE - 1; // prevent overflow uint32_t center_led = center >> AW_SHIFT; wave_start = (int16_t)center_led - (int16_t)width; wave_end = (int16_t)center_led + (int16_t)width; } CRGBW getColorForLED(int ledIndex) { // linear brightness falloff from center to edge of wave if (ledIndex < wave_start || ledIndex > wave_end) return 0; int32_t ledIndex_scaled = (int32_t)ledIndex << AW_SHIFT; int32_t offset = ledIndex_scaled - center; if (offset < 0) offset = -offset; uint32_t offsetFactor = offset / width; // scaled by AW_SCALE if (offsetFactor > AW_SCALE) return 0; // outside of wave uint32_t brightness_factor = (AW_SCALE - offsetFactor); brightness_factor = (brightness_factor * ageFactor_cached) >> AW_SHIFT; brightness_factor = (brightness_factor * basealpha) >> AW_SHIFT; CRGBW rgb; rgb.r = (basecolor.r * brightness_factor) >> AW_SHIFT; rgb.g = (basecolor.g * brightness_factor) >> AW_SHIFT; rgb.b = (basecolor.b * brightness_factor) >> AW_SHIFT; rgb.w = (basecolor.w * brightness_factor) >> AW_SHIFT; return rgb; }; //Change position and age of wave //Determine if its still "alive" void update(uint32_t segment_length, uint32_t speed) { int32_t step = speed_factor * speed; center += goingleft ? -step : step; age++; if (age > ttl) { alive = false; } else { uint32_t width_scaled = (uint32_t)width << AW_SHIFT; uint32_t segment_length_scaled = segment_length << AW_SHIFT; if (goingleft) { if (center < - (int32_t)width_scaled) { alive = false; } } else { if (center > (int32_t)segment_length_scaled + (int32_t)width_scaled) { alive = false; } } } }; bool stillAlive() { return alive; } }; void mode_aurora(void) { AuroraWave* waves; SEGENV.aux1 = map(SEGMENT.intensity, 0, 255, 2, W_MAX_COUNT); // aux1 = Wavecount if (!SEGENV.allocateData(sizeof(AuroraWave) * SEGENV.aux1)) { FX_FALLBACK_STATIC; } waves = reinterpret_cast(SEGENV.data); // note: on first call, SEGENV.data is zero -> all waves are dead and will be initialized for (int i = 0; i < SEGENV.aux1; i++) { waves[i].update(SEGLEN, SEGMENT.speed); if (!(waves[i].stillAlive())) { waves[i].init(SEGLEN, SEGMENT.color_from_palette(hw_random8(), false, false, hw_random8(0, 3))); } waves[i].updateCachedValues(); } uint8_t backlight = 0; // note: original code used 1, with inverse gamma applied background would never be black if (SEGCOLOR(0)) backlight++; if (SEGCOLOR(1)) backlight++; if (SEGCOLOR(2)) backlight++; backlight = gamma8inv(backlight); // preserve backlight when using gamma correction for (unsigned i = 0; i < SEGLEN; i++) { CRGBW mixedRgb = CRGBW(backlight, backlight, backlight); for (int j = 0; j < SEGENV.aux1; j++) { CRGBW rgb = waves[j].getColorForLED(i); mixedRgb = color_add(mixedRgb, rgb); // sum all waves influencing this pixel } SEGMENT.setPixelColor(i, mixedRgb); } } static const char _data_FX_MODE_AURORA[] PROGMEM = "Aurora@!,!;1,2,3;!;;sx=24,pal=50"; /** Softly floating colorful clouds. * This is a very smooth effect that moves colorful clouds randomly around the LED strip. * It was initially intended for rather unobtrusive ambient lights (with very slow speed settings). * Nevertheless, it appears completely different and quite vibrant when the sliders are moved near * to their limits. No matter in which direction or in which combination... * Ported to WLED from https://github.com/JoaDick/EyeCandy/blob/master/ColorClouds.h */ void mode_ColorClouds() { // Set random start points for clouds and color. if (SEGENV.call == 0) { SEGENV.aux0 = hw_random16(); SEGENV.aux1 = hw_random16(); } const uint32_t volX0 = SEGENV.aux0; const uint32_t hueX0 = SEGENV.aux1; const uint8_t hueOffset0 = volX0 + hueX0; // derive a 3rd random number // Makes a very soft wraparound of the color palette by putting more emphasis on the begin & end // of the palette (or on the red'ish colors in case of a rainbow spectrum). // This gives the effect oftentimes an even more calm perception. const bool cozy = SEGMENT.check3; // Higher values make the clouds move faster. const uint32_t volSpeed = 1 + SEGMENT.speed; // Higher values make the color change faster. const uint32_t hueSpeed = 1 + SEGMENT.intensity; // Higher values make more clouds (but smaller ones). const uint32_t volSqueeze = 8 + SEGMENT.custom1; // Higher values make the clouds more colorful. const uint32_t hueSqueeze = SEGMENT.custom2; // Higher values make larger gaps between the clouds. const int32_t volCutoff = 12500 + SEGMENT.custom3 * 900; const int32_t volSaturate = 52000; // Note: When adjusting these calculations, ensure that volCutoff is always smaller than volSaturate. const uint32_t now = strip.now; const uint32_t volT = now * volSpeed / 8; const uint32_t hueT = now * hueSpeed / 8; const uint8_t hueOffset = beat88(64) >> 8; for (int i = 0; i < SEGLEN; i++) { const uint32_t volX = i * volSqueeze * 64; int32_t vol = perlin16(volX0 + volX, volT); vol = map(vol, volCutoff, volSaturate, 0, 255); vol = constrain(vol, 0, 255); const uint32_t hueX = i * hueSqueeze * 8; uint8_t hue = perlin16(hueX0 + hueX, hueT) >> 7; hue += hueOffset0; hue += hueOffset; if (cozy) { hue = cos8_t(128 + hue / 2); } uint32_t pixel; if (SEGMENT.palette) { pixel = SEGMENT.color_from_palette(hue, false, true, 0, vol); } else { hsv2rgb(CHSV32(hue, 255, vol), pixel); } // Suppress extremely dark pixels to avoid flickering of plain r/g/b. if (int(R(pixel)) + G(pixel) + B(pixel) <= 2) { pixel = 0; } SEGMENT.setPixelColor(i, pixel); } } static const char _data_FX_MODE_COLORCLOUDS[] PROGMEM = "Color Clouds@!,!,Clouds,Colors,Distance,,,Cozy;;!;;sx=24,ix=32,c1=48,c2=64,c3=12,pal=0"; // WLED-SR effects ///////////////////////// // Perlin Move // ///////////////////////// // 16 bit perlinmove. Use Perlin Noise instead of sinewaves for movement. By Andrew Tuline. // Controls are speed, # of pixels, faderate. void mode_perlinmove(void) { if (SEGLEN <= 1) FX_FALLBACK_STATIC; SEGMENT.fade_out(255-SEGMENT.custom1); for (int i = 0; i < SEGMENT.intensity/16 + 1; i++) { unsigned locn = perlin16(strip.now*128/(260-SEGMENT.speed)+i*15000, strip.now*128/(260-SEGMENT.speed)); // Get a new pixel location from moving noise. unsigned pixloc = map(locn, 50*256, 192*256, 0, SEGLEN-1); // Map that to the length of the strand, and ensure we don't go over. SEGMENT.setPixelColor(pixloc, SEGMENT.color_from_palette(pixloc%255, false, PALETTE_SOLID_WRAP, 0)); } } // mode_perlinmove() static const char _data_FX_MODE_PERLINMOVE[] PROGMEM = "Perlin Move@!,# of pixels,Fade rate;!,!;!"; ///////////////////////// // Waveins // ///////////////////////// // Uses beatsin8() + phase shifting. By: Andrew Tuline void mode_wavesins(void) { for (unsigned i = 0; i < SEGLEN; i++) { uint8_t bri = sin8_t(strip.now/4 + i * SEGMENT.intensity); uint8_t index = beatsin8_t(SEGMENT.speed, SEGMENT.custom1, SEGMENT.custom1+SEGMENT.custom2, 0, i * (SEGMENT.custom3<<3)); // custom3 is reduced resolution slider //SEGMENT.setPixelColor(i, ColorFromPalette(SEGPALETTE, index, bri, LINEARBLEND)); SEGMENT.setPixelColor(i, SEGMENT.color_from_palette(index, false, PALETTE_SOLID_WRAP, 0, bri)); } } // mode_waveins() static const char _data_FX_MODE_WAVESINS[] PROGMEM = "Wavesins@!,Brightness variation,Starting color,Range of colors,Color variation;!;!"; ////////////////////////////// // Flow Stripe // ////////////////////////////// // By: ldirko https://editor.soulmatelights.com/gallery/392-flow-led-stripe , modifed by: Andrew Tuline, fixed by @DedeHai void mode_FlowStripe(void) { if (SEGLEN <= 1) FX_FALLBACK_STATIC; const int hl = SEGLEN * 10 / 13; uint8_t hue = strip.now / (SEGMENT.speed+1); uint32_t t = strip.now / (SEGMENT.intensity/8+1); for (unsigned i = 0; i < SEGLEN; i++) { int c = ((abs((int)i - hl) * 127) / hl); c = sin8_t(c); c = sin8_t(c / 2 + t); byte b = sin8_t(c + t/8); SEGMENT.setPixelColor(i, SEGMENT.color_from_palette(b + hue, false, true, 3)); } } // mode_FlowStripe() static const char _data_FX_MODE_FLOWSTRIPE[] PROGMEM = "Flow Stripe@Hue speed,Effect speed;;!;pal=11"; /* Shimmer effect: moves a gradient with optional modulators across the strip at a given interval, up to 60 seconds It can be used as an overlay to other effects or standalone by DedeHai (Damian Schneider), based on idea from @Charming-Lime (#4905) */ void mode_shimmer() { if(!SEGENV.allocateData(sizeof(uint32_t))) { FX_FALLBACK_STATIC; } uint32_t* lastTime = reinterpret_cast(SEGENV.data); uint32_t radius = (SEGMENT.custom1 * SEGLEN >> 7) + 1; // [1, 2*SEGLEN+1] pixels uint32_t traversalDistance = (SEGLEN + 2 * radius) << 8; // total subpixels to cross, 1 pixel = 256 subpixels uint32_t traversalTime = 200 + (255 - SEGMENT.speed) * 80; // [200, 20600] ms uint32_t speed = ((traversalDistance << 5) / traversalTime); // subpixels/512ms int32_t position = static_cast(SEGENV.step); // current position in subpixels uint16_t inputstate = (uint16_t(SEGMENT.intensity) << 8) | uint16_t(SEGMENT.custom1); // current user input state // init if (SEGENV.call == 0 || inputstate != SEGENV.aux1) { position = -(radius << 8); SEGENV.aux0 = 0; // aux0 is pause timer *lastTime = strip.now; SEGENV.aux1 = inputstate; // save user input state } if(SEGMENT.speed) { uint32_t deltaTime = (strip.now - *lastTime) & 0x7F; // clamp to 127ms to avoid overflows. note: speed*deltaTime can still overflow for segments > ~10k pixels *lastTime = strip.now; if (SEGENV.aux0 > 0) { SEGENV.aux0 = (SEGENV.aux0 > deltaTime) ? SEGENV.aux0 - deltaTime : 0; } else { // calculate movement step and update position int32_t step = 1 + ((speed * deltaTime) >> 5); // subpixels moved this frame. note >>5 as speed is in subpixels/512ms position += step; int endposition = (SEGLEN + radius) << 8; if (position > endposition) { SEGENV.aux0 = SEGMENT.intensity * 236; // [0, 60180] ms pause if(SEGMENT.check3) SEGENV.aux0 = hw_random(SEGENV.aux0 + 1000); // randomise interval, +1 second to affect low intensity values position = -(radius << 8); // reset to start position (out of frame) } SEGENV.step = (uint32_t)position; // save back } if (SEGMENT.check2) position = (SEGLEN << 8) - position; // invert position (and direction) } else { position = (SEGLEN << 7); // at speed=0, make it static in the center (this enables to use modulators only) } for (int i = 0; i < SEGLEN; i++) { uint32_t dist = abs(position - (i << 8)); if (dist < (radius << 8)) { uint32_t color = SEGMENT.color_from_palette(i * 255 / SEGLEN, false, false, 0); uint8_t blend = dist / radius; // linear gradient note: dist is in subpixels, radius in pixels, result is [0, 255] since dist < radius*256 if (SEGMENT.custom2) { uint8_t modVal; // modulation value if (SEGMENT.check1) { modVal = (sin16_t((i * SEGMENT.custom2 << 6) + (strip.now * SEGMENT.custom3 << 5)) >> 8) + 128; // sine modulation: regular "Zebra" stripes } else { modVal = perlin16((i * SEGMENT.custom2 << 7), strip.now * SEGMENT.custom3 << 5) >> 8; // perlin noise modulation } color = color_fade(color, modVal, true); // dim by modulator value } SEGMENT.setPixelColor(i, color_blend(color, SEGCOLOR(1), blend)); // blend to background color } else { SEGMENT.setPixelColor(i, SEGCOLOR(1)); } } } static const char _data_FX_MODE_SHIMMER[] PROGMEM = "Shimmer@Speed,Interval,Size,Granular,Flow,Zebra,Reverse,Sporadic;Fx,Bg,Cx;!;1;pal=15,sx=220,ix=10,c2=0,c3=0"; #ifndef WLED_DISABLE_2D /////////////////////////////////////////////////////////////////////////////// //*************************** 2D routines *********************************** // Black hole void mode_2DBlackHole(void) { // By: Stepko https://editor.soulmatelights.com/gallery/1012 , Modified by: Andrew Tuline if (!strip.isMatrix || !SEGMENT.is2D()) FX_FALLBACK_STATIC; // not a 2D set-up const int cols = SEG_W; const int rows = SEG_H; int x, y; SEGMENT.fadeToBlackBy(16 + (SEGMENT.speed>>3)); // create fading trails unsigned long t = strip.now/128; // timebase // outer stars for (size_t i = 0; i < 8; i++) { x = beatsin8_t(SEGMENT.custom1>>3, 0, cols - 1, 0, ((i % 2) ? 128 : 0) + t * i); y = beatsin8_t(SEGMENT.intensity>>3, 0, rows - 1, 0, ((i % 2) ? 192 : 64) + t * i); SEGMENT.addPixelColorXY(x, y, SEGMENT.color_from_palette(i*32, false, PALETTE_SOLID_WRAP, SEGMENT.check1?0:255)); } // inner stars for (size_t i = 0; i < 4; i++) { x = beatsin8_t(SEGMENT.custom2>>3, cols/4, cols - 1 - cols/4, 0, ((i % 2) ? 128 : 0) + t * i); y = beatsin8_t(SEGMENT.custom3 , rows/4, rows - 1 - rows/4, 0, ((i % 2) ? 192 : 64) + t * i); SEGMENT.addPixelColorXY(x, y, SEGMENT.color_from_palette(255-i*64, false, PALETTE_SOLID_WRAP, SEGMENT.check1?0:255)); } // central white dot SEGMENT.setPixelColorXY(cols/2, rows/2, WHITE); // blur everything a bit if (SEGMENT.check3) SEGMENT.blur(16, cols*rows < 100); } // mode_2DBlackHole() static const char _data_FX_MODE_2DBLACKHOLE[] PROGMEM = "Black Hole@Fade rate,Outer Y freq.,Outer X freq.,Inner X freq.,Inner Y freq.,Solid,,Blur;!;!;2;pal=11"; //////////////////////////// // 2D Colored Bursts // //////////////////////////// void mode_2DColoredBursts() { // By: ldirko https://editor.soulmatelights.com/gallery/819-colored-bursts , modified by: Andrew Tuline if (!strip.isMatrix || !SEGMENT.is2D()) FX_FALLBACK_STATIC; // not a 2D set-up const int cols = SEG_W; const int rows = SEG_H; if (SEGENV.call == 0) { SEGENV.aux0 = 0; // start with red hue } bool dot = SEGMENT.check3; bool grad = SEGMENT.check1; byte numLines = SEGMENT.intensity/16 + 1; SEGENV.aux0++; // hue SEGMENT.fadeToBlackBy(40 - SEGMENT.check2 * 8); for (size_t i = 0; i < numLines; i++) { byte x1 = beatsin8_t(2 + SEGMENT.speed/16, 0, (cols - 1)); byte x2 = beatsin8_t(1 + SEGMENT.speed/16, 0, (rows - 1)); byte y1 = beatsin8_t(5 + SEGMENT.speed/16, 0, (cols - 1), 0, i * 24); byte y2 = beatsin8_t(3 + SEGMENT.speed/16, 0, (rows - 1), 0, i * 48 + 64); uint32_t color = ColorFromPalette(SEGPALETTE, i * 255 / numLines + (SEGENV.aux0&0xFF), 255, LINEARBLEND); byte xsteps = abs8(x1 - y1) + 1; byte ysteps = abs8(x2 - y2) + 1; byte steps = xsteps >= ysteps ? xsteps : ysteps; //Draw gradient line for (size_t j = 1; j <= steps; j++) { uint8_t rate = j * 255 / steps; byte dx = lerp8by8(x1, y1, rate); byte dy = lerp8by8(x2, y2, rate); //SEGMENT.setPixelColorXY(dx, dy, grad ? color.nscale8_video(255-rate) : color); // use addPixelColorXY for different look SEGMENT.addPixelColorXY(dx, dy, color); // use setPixelColorXY for different look if (grad) SEGMENT.fadePixelColorXY(dx, dy, rate); } if (dot) { //add white point at the ends of line SEGMENT.setPixelColorXY(x1, x2, WHITE); SEGMENT.setPixelColorXY(y1, y2, DARKSLATEGRAY); } } SEGMENT.blur(SEGMENT.custom3>>1, SEGMENT.check2); } // mode_2DColoredBursts() static const char _data_FX_MODE_2DCOLOREDBURSTS[] PROGMEM = "Colored Bursts@Speed,# of lines,,,Blur,Gradient,Smear,Dots;;!;2;c3=16"; ///////////////////// // 2D DNA // ///////////////////// void mode_2Ddna(void) { // dna originally by by ldirko at https://pastebin.com/pCkkkzcs. Updated by Preyy. WLED conversion by Andrew Tuline. if (!strip.isMatrix || !SEGMENT.is2D()) FX_FALLBACK_STATIC; // not a 2D set-up const int cols = SEG_W; const int rows = SEG_H; SEGMENT.fadeToBlackBy(64); for (int i = 0; i < cols; i++) { SEGMENT.setPixelColorXY(i, beatsin8_t(SEGMENT.speed/8, 0, rows-1, 0, i*4 ), ColorFromPalette(SEGPALETTE, i*5+strip.now/17, beatsin8_t(5, 55, 255, 0, i*10), LINEARBLEND)); SEGMENT.setPixelColorXY(i, beatsin8_t(SEGMENT.speed/8, 0, rows-1, 0, i*4+128), ColorFromPalette(SEGPALETTE, i*5+128+strip.now/17, beatsin8_t(5, 55, 255, 0, i*10+128), LINEARBLEND)); } SEGMENT.blur(SEGMENT.intensity / (8 - (SEGMENT.check1 * 2)), SEGMENT.check1); } // mode_2Ddna() static const char _data_FX_MODE_2DDNA[] PROGMEM = "DNA@Scroll speed,Blur,,,,Smear;;!;2;ix=0"; ///////////////////////// // 2D DNA Spiral // ///////////////////////// void mode_2DDNASpiral() { // By: ldirko https://editor.soulmatelights.com/gallery/512-dna-spiral-variation , modified by: Andrew Tuline if (!strip.isMatrix || !SEGMENT.is2D()) FX_FALLBACK_STATIC; // not a 2D set-up const int cols = SEG_W; const int rows = SEG_H; if (SEGENV.call == 0) { SEGMENT.fill(BLACK); } unsigned speeds = SEGMENT.speed/2 + 7; unsigned freq = SEGMENT.intensity/8; uint32_t ms = strip.now / 20; SEGMENT.fadeToBlackBy(135); for (int i = 0; i < rows; i++) { int x = beatsin8_t(speeds, 0, cols - 1, 0, i * freq) + beatsin8_t(speeds - 7, 0, cols - 1, 0, i * freq + 128); int x1 = beatsin8_t(speeds, 0, cols - 1, 0, 128 + i * freq) + beatsin8_t(speeds - 7, 0, cols - 1, 0, 128 + 64 + i * freq); unsigned hue = (i * 128 / rows) + ms; // skip every 4th row every now and then (fade it more) if ((i + ms / 8) & 3) { // draw a gradient line between x and x1 x = x / 2; x1 = x1 / 2; unsigned steps = abs8(x - x1) + 1; bool positive = (x1 >= x); // direction of drawing for (size_t k = 1; k <= steps; k++) { unsigned rate = k * 255 / steps; //unsigned dx = lerp8by8(x, x1, rate); unsigned dx = positive? (x + k-1) : (x - k+1); // behaves the same as "lerp8by8" but does not create holes //SEGMENT.setPixelColorXY(dx, i, ColorFromPalette(SEGPALETTE, hue, 255, LINEARBLEND).nscale8_video(rate)); SEGMENT.addPixelColorXY(dx, i, ColorFromPalette(SEGPALETTE, hue, 255, LINEARBLEND)); // use setPixelColorXY for different look SEGMENT.fadePixelColorXY(dx, i, rate); } SEGMENT.setPixelColorXY(x, i, DARKSLATEGRAY); SEGMENT.setPixelColorXY(x1, i, WHITE); } } SEGMENT.blur(((uint16_t)SEGMENT.custom1 * 3) / (6 + SEGMENT.check1), SEGMENT.check1); } // mode_2DDNASpiral() static const char _data_FX_MODE_2DDNASPIRAL[] PROGMEM = "DNA Spiral@Scroll speed,Y frequency,Blur,,,Smear;;!;2;c1=0"; ///////////////////////// // 2D Drift // ///////////////////////// void mode_2DDrift() { // By: Stepko https://editor.soulmatelights.com/gallery/884-drift , Modified by: Andrew Tuline if (!strip.isMatrix || !SEGMENT.is2D()) FX_FALLBACK_STATIC; // not a 2D set-up const int cols = SEG_W; const int rows = SEG_H; const int colsCenter = (cols>>1) + (cols%2); const int rowsCenter = (rows>>1) + (rows%2); SEGMENT.fadeToBlackBy(128); const float maxDim = MAX(cols, rows)/2; unsigned long t = strip.now / (32 - (SEGMENT.speed>>3)); unsigned long t_20 = t/20; // softhack007: pre-calculating this gives about 10% speedup for (float i = 1.0f; i < maxDim; i += 0.25f) { float angle = radians(t * (maxDim - i)); int mySin = sin_t(angle) * i; int myCos = cos_t(angle) * i; SEGMENT.setPixelColorXY(colsCenter + mySin, rowsCenter + myCos, ColorFromPalette(SEGPALETTE, (i * 20) + t_20, 255, LINEARBLEND)); if (SEGMENT.check1) SEGMENT.setPixelColorXY(colsCenter + myCos, rowsCenter + mySin, ColorFromPalette(SEGPALETTE, (i * 20) + t_20, 255, LINEARBLEND)); } SEGMENT.blur(SEGMENT.intensity>>(3 - SEGMENT.check2), SEGMENT.check2); } // mode_2DDrift() static const char _data_FX_MODE_2DDRIFT[] PROGMEM = "Drift@Rotation speed,Blur,,,,Twin,Smear;;!;2;ix=0"; ////////////////////////// // 2D Firenoise // ////////////////////////// void mode_2Dfirenoise(void) { // firenoise2d. By Andrew Tuline. Yet another short routine. if (!strip.isMatrix || !SEGMENT.is2D()) FX_FALLBACK_STATIC; // not a 2D set-up const int cols = SEG_W; const int rows = SEG_H; if (SEGENV.call == 0) { SEGMENT.fill(BLACK); } unsigned xscale = SEGMENT.intensity*4; unsigned yscale = SEGMENT.speed*8; unsigned indexx = 0; //CRGBPalette16 pal = SEGMENT.check1 ? SEGPALETTE : SEGMENT.loadPalette(pal, 35); CRGBPalette16 pal = SEGMENT.check1 ? SEGPALETTE : CRGBPalette16(CRGB::Black, CRGB::Black, CRGB::Black, CRGB::Black, CRGB::Red, CRGB::Red, CRGB::Red, CRGB::DarkOrange, CRGB::DarkOrange,CRGB::DarkOrange, CRGB::Orange, CRGB::Orange, CRGB::Yellow, CRGB::Orange, CRGB::Yellow, CRGB::Yellow); for (int j=0; j < cols; j++) { for (int i=0; i < rows; i++) { indexx = perlin8(j*yscale*rows/255, i*xscale+strip.now/4); // We're moving along our Perlin map. SEGMENT.setPixelColorXY(j, i, ColorFromPalette(pal, min(i*indexx/11, 225U), i*255/rows, LINEARBLEND)); // With that value, look up the 8 bit colour palette value and assign it to the current LED. } // for i } // for j } // mode_2Dfirenoise() static const char _data_FX_MODE_2DFIRENOISE[] PROGMEM = "Firenoise@X scale,Y scale,,,,Palette;;!;2;pal=66"; ////////////////////////////// // 2D Frizzles // ////////////////////////////// void mode_2DFrizzles(void) { // By: Stepko https://editor.soulmatelights.com/gallery/640-color-frizzles , Modified by: Andrew Tuline if (!strip.isMatrix || !SEGMENT.is2D()) FX_FALLBACK_STATIC; // not a 2D set-up const int cols = SEG_W; const int rows = SEG_H; SEGMENT.fadeToBlackBy(16 + SEGMENT.check1 * 10); for (size_t i = 8; i > 0; i--) { SEGMENT.addPixelColorXY(beatsin8_t(SEGMENT.speed/8 + i, 0, cols - 1), beatsin8_t(SEGMENT.intensity/8 - i, 0, rows - 1), ColorFromPalette(SEGPALETTE, beatsin8_t(12, 0, 255), 255, LINEARBLEND)); } SEGMENT.blur(SEGMENT.custom1 >> (3 + SEGMENT.check1), SEGMENT.check1); } // mode_2DFrizzles() static const char _data_FX_MODE_2DFRIZZLES[] PROGMEM = "Frizzles@X frequency,Y frequency,Blur,,,Smear;;!;2"; /////////////////////////////////////////// // 2D Cellular Automata Game of life // /////////////////////////////////////////// typedef struct Cell { uint8_t alive : 1, faded : 1, toggleStatus : 1, edgeCell: 1, oscillatorCheck : 1, spaceshipCheck : 1, unused : 2; } Cell; void mode_2Dgameoflife(void) { // Written by Ewoud Wijma, inspired by https://natureofcode.com/book/chapter-7-cellular-automata/ // and https://github.com/DougHaber/nlife-color , Modified By: Brandon Butler if (!strip.isMatrix || !SEGMENT.is2D()) FX_FALLBACK_STATIC; // not a 2D set-up const int cols = SEG_W, rows = SEG_H; const unsigned maxIndex = cols * rows; if (!SEGENV.allocateData(SEGMENT.length() * sizeof(Cell))) FX_FALLBACK_STATIC; // allocation failed Cell *cells = reinterpret_cast (SEGENV.data); uint16_t& generation = SEGENV.aux0, &gliderLength = SEGENV.aux1; // rename aux variables for clarity bool mutate = SEGMENT.check3; uint8_t blur = map(SEGMENT.custom1, 0, 255, 255, 4); uint32_t bgColor = SEGCOLOR(1); uint32_t birthColor = SEGMENT.color_from_palette(128, false, PALETTE_SOLID_WRAP, 255); bool setup = SEGENV.call == 0; if (setup) { // Calculate glider length LCM(rows,cols)*4 once unsigned a = rows, b = cols; while (b) { unsigned t = b; b = a % b; a = t; } gliderLength = (cols * rows / a) << 2; } if (abs(long(strip.now) - long(SEGENV.step)) > 2000) SEGENV.step = 0; // Timebase jump fix bool paused = SEGENV.step > strip.now; // Setup New Game of Life if ((!paused && generation == 0) || setup) { SEGENV.step = strip.now + 1280; // show initial state for 1.28 seconds generation = 1; paused = true; //Setup Grid memset(cells, 0, maxIndex * sizeof(Cell)); for (unsigned i = 0; i < maxIndex; i++) { bool isAlive = !hw_random8(3); // ~33% cells[i].alive = isAlive; cells[i].faded = !isAlive; unsigned x = i % cols, y = i / cols; cells[i].edgeCell = (x == 0 || x == cols-1 || y == 0 || y == rows-1); SEGMENT.setPixelColor(i, isAlive ? SEGMENT.color_from_palette(hw_random8(), false, PALETTE_SOLID_WRAP, 0) : bgColor); } } if (paused || (strip.now - SEGENV.step < 1000 / map(SEGMENT.speed,0,255,1,42))) { // Redraw if paused or between updates to remove blur for (unsigned i = maxIndex; i--; ) { if (!cells[i].alive) { uint32_t cellColor = SEGMENT.getPixelColor(i); if (cellColor != bgColor) { uint32_t newColor; bool needsColor = false; if (cells[i].faded) { newColor = bgColor; needsColor = true; } else { uint32_t blended = color_blend(cellColor, bgColor, 2); if (blended == cellColor) { blended = bgColor; cells[i].faded = 1; } newColor = blended; needsColor = true; } if (needsColor) SEGMENT.setPixelColor(i, newColor); } } } } // Repeat detection bool updateOscillator = generation % 16 == 0; bool updateSpaceship = gliderLength && generation % gliderLength == 0; bool repeatingOscillator = true, repeatingSpaceship = true, emptyGrid = true; unsigned cIndex = maxIndex-1; for (unsigned y = rows; y--; ) for (unsigned x = cols; x--; cIndex--) { Cell& cell = cells[cIndex]; if (cell.alive) emptyGrid = false; if (cell.oscillatorCheck != cell.alive) repeatingOscillator = false; if (cell.spaceshipCheck != cell.alive) repeatingSpaceship = false; if (updateOscillator) cell.oscillatorCheck = cell.alive; if (updateSpaceship) cell.spaceshipCheck = cell.alive; unsigned neighbors = 0, aliveParents = 0, parentIdx[3]; // Count alive neighbors for (int i = -1; i <= 1; i++) for (int j = -1; j <= 1; j++) if (i || j) { int nX = x + j, nY = y + i; if (cell.edgeCell) { nX = (nX + cols) % cols; nY = (nY + rows) % rows; } unsigned nIndex = nX + nY * cols; Cell& neighbor = cells[nIndex]; if (neighbor.alive) { neighbors++; if (!neighbor.toggleStatus && neighbors < 4) { // Alive and not dying parentIdx[aliveParents++] = nIndex; } } } uint32_t newColor; bool needsColor = false; if (cell.alive && (neighbors < 2 || neighbors > 3)) { // Loneliness or Overpopulation cell.toggleStatus = 1; if (blur == 255) cell.faded = 1; newColor = cell.faded ? bgColor : color_blend(SEGMENT.getPixelColor(cIndex), bgColor, blur); needsColor = true; } else if (!cell.alive) { byte mutationRoll = mutate ? hw_random8(128) : 1; // if 0: 3 neighbor births fail and 2 neighbor births mutate if ((neighbors == 3 && mutationRoll) || (mutate && neighbors == 2 && !mutationRoll)) { // Reproduction or Mutation cell.toggleStatus = 1; cell.faded = 0; if (aliveParents) { // Set color based on random neighbor unsigned parentIndex = parentIdx[random8(aliveParents)]; birthColor = SEGMENT.getPixelColor(parentIndex); } newColor = birthColor; needsColor = true; } else if (!cell.faded) {// No change, fade dead cells uint32_t cellColor = SEGMENT.getPixelColor(cIndex); uint32_t blended = color_blend(cellColor, bgColor, blur); if (blended == cellColor) { blended = bgColor; cell.faded = 1; } newColor = blended; needsColor = true; } } if (needsColor) SEGMENT.setPixelColor(cIndex, newColor); } // Loop through cells, if toggle, swap alive status for (unsigned i = maxIndex; i--; ) { cells[i].alive ^= cells[i].toggleStatus; cells[i].toggleStatus = 0; } if (repeatingOscillator || repeatingSpaceship || emptyGrid) { generation = 0; // reset on next call SEGENV.step += 1024; // pause final generation for ~1 second } else { ++generation; SEGENV.step = strip.now; } } // mode_2Dgameoflife() static const char _data_FX_MODE_2DGAMEOFLIFE[] PROGMEM = "Game Of Life@!,,Blur,,,,,Mutation;!,!;!;2;pal=11,sx=128"; ///////////////////////// // 2D Hiphotic // ///////////////////////// void mode_2DHiphotic() { // By: ldirko https://editor.soulmatelights.com/gallery/810 , Modified by: Andrew Tuline if (!strip.isMatrix || !SEGMENT.is2D()) FX_FALLBACK_STATIC; // not a 2D set-up const int cols = SEG_W; const int rows = SEG_H; const uint32_t a = strip.now / ((SEGMENT.custom3>>1)+1); for (int x = 0; x < cols; x++) { for (int y = 0; y < rows; y++) { SEGMENT.setPixelColorXY(x, y, SEGMENT.color_from_palette(sin8_t(cos8_t(x * SEGMENT.speed/16 + a / 3) + sin8_t(y * SEGMENT.intensity/16 + a / 4) + a), false, PALETTE_SOLID_WRAP, 0)); } } } // mode_2DHiphotic() static const char _data_FX_MODE_2DHIPHOTIC[] PROGMEM = "Hiphotic@X scale,Y scale,,,Speed;!;!;2"; ///////////////////////// // 2D Julia // ///////////////////////// // Sliders are: // intensity = Maximum number of iterations per pixel. // Custom1 = Location of X centerpoint // Custom2 = Location of Y centerpoint // Custom3 = Size of the area (small value = smaller area) typedef struct Julia { float xcen; float ycen; float xymag; } julia; void mode_2DJulia(void) { // An animated Julia set by Andrew Tuline. if (!strip.isMatrix || !SEGMENT.is2D()) FX_FALLBACK_STATIC; // not a 2D set-up const int cols = SEG_W; const int rows = SEG_H; if (!SEGENV.allocateData(sizeof(julia))) FX_FALLBACK_STATIC; Julia* julias = reinterpret_cast(SEGENV.data); float reAl; float imAg; if (SEGENV.call == 0) { // Reset the center if we've just re-started this animation. julias->xcen = 0.; julias->ycen = 0.; julias->xymag = 1.0; SEGMENT.custom1 = 128; // Make sure the location widgets are centered to start. SEGMENT.custom2 = 128; SEGMENT.custom3 = 16; SEGMENT.intensity = 24; } julias->xcen = julias->xcen + (float)(SEGMENT.custom1 - 128)/100000.f; julias->ycen = julias->ycen + (float)(SEGMENT.custom2 - 128)/100000.f; julias->xymag = julias->xymag + (float)((SEGMENT.custom3 - 16)<<3)/100000.f; // reduced resolution slider if (julias->xymag < 0.01f) julias->xymag = 0.01f; if (julias->xymag > 1.0f) julias->xymag = 1.0f; float xmin = julias->xcen - julias->xymag; float xmax = julias->xcen + julias->xymag; float ymin = julias->ycen - julias->xymag; float ymax = julias->ycen + julias->xymag; // Whole set should be within -1.2,1.2 to -.8 to 1. xmin = constrain(xmin, -1.2f, 1.2f); xmax = constrain(xmax, -1.2f, 1.2f); ymin = constrain(ymin, -0.8f, 1.0f); ymax = constrain(ymax, -0.8f, 1.0f); float dx; // Delta x is mapped to the matrix size. float dy; // Delta y is mapped to the matrix size. int maxIterations = 15; // How many iterations per pixel before we give up. Make it 8 bits to match our range of colours. float maxCalc = 16.0; // How big is each calculation allowed to be before we give up. maxIterations = SEGMENT.intensity/2; // Resize section on the fly for some animaton. reAl = -0.94299f; // PixelBlaze example imAg = 0.3162f; reAl += (float)sin16_t(strip.now * 34) / 655340.f; imAg += (float)sin16_t(strip.now * 26) / 655340.f; dx = (xmax - xmin) / (cols); // Scale the delta x and y values to our matrix size. dy = (ymax - ymin) / (rows); // Start y float y = ymin; for (int j = 0; j < rows; j++) { // Start x float x = xmin; for (int i = 0; i < cols; i++) { // Now we test, as we iterate z = z^2 + c does z tend towards infinity? float a = x; float b = y; int iter = 0; while (iter < maxIterations) { // Here we determine whether or not we're out of bounds. float aa = a * a; float bb = b * b; float len = aa + bb; if (len > maxCalc) { // |z| = sqrt(a^2+b^2) OR z^2 = a^2+b^2 to save on having to perform a square root. break; // Bail } // This operation corresponds to z -> z^2+c where z=a+ib c=(x,y). Remember to use 'foil'. b = 2*a*b + imAg; a = aa - bb + reAl; iter++; } // while // We color each pixel based on how long it takes to get to infinity, or black if it never gets there. if (iter == maxIterations) { SEGMENT.setPixelColorXY(i, j, 0); } else { SEGMENT.setPixelColorXY(i, j, SEGMENT.color_from_palette(iter*255/maxIterations, false, PALETTE_SOLID_WRAP, 0)); } x += dx; } y += dy; } if(SEGMENT.check1) SEGMENT.blur(100, true); } // mode_2DJulia() static const char _data_FX_MODE_2DJULIA[] PROGMEM = "Julia@,Max iterations per pixel,X center,Y center,Area size, Blur;!;!;2;ix=24,c1=128,c2=128,c3=16"; ////////////////////////////// // 2D Lissajous // ////////////////////////////// void mode_2DLissajous(void) { // By: Andrew Tuline if (!strip.isMatrix || !SEGMENT.is2D()) FX_FALLBACK_STATIC; // not a 2D set-up const int cols = SEG_W; const int rows = SEG_H; SEGMENT.fadeToBlackBy(SEGMENT.intensity); uint_fast16_t phase = (strip.now * (1 + SEGENV.custom3)) /32; // allow user to control rotation speed //for (int i=0; i < 4*(cols+rows); i ++) { for (int i=0; i < 256; i ++) { //float xlocn = float(sin8_t(now/4+i*(SEGMENT.speed>>5))) / 255.0f; //float ylocn = float(cos8_t(now/4+i*2)) / 255.0f; uint_fast8_t xlocn = sin8_t(phase/2 + (i*SEGMENT.speed)/32); uint_fast8_t ylocn = cos8_t(phase/2 + i*2); xlocn = (cols < 2) ? 1 : (map(2*xlocn, 0,511, 0,2*(cols-1)) +1) /2; // softhack007: "(2* ..... +1) /2" for proper rounding ylocn = (rows < 2) ? 1 : (map(2*ylocn, 0,511, 0,2*(rows-1)) +1) /2; // "rows > 1" is needed to avoid div/0 in map() SEGMENT.setPixelColorXY((uint8_t)xlocn, (uint8_t)ylocn, SEGMENT.color_from_palette(strip.now/100+i, false, PALETTE_SOLID_WRAP, 0)); } SEGMENT.blur(SEGMENT.custom1 >> (1 + SEGMENT.check1 * 3), SEGMENT.check1); } // mode_2DLissajous() static const char _data_FX_MODE_2DLISSAJOUS[] PROGMEM = "Lissajous@X frequency,Fade rate,Blur,,Speed,Smear;!;!;2;c1=0"; /////////////////////// // 2D Matrix // /////////////////////// void mode_2Dmatrix(void) { // Matrix2D. By Jeremy Williams. Adapted by Andrew Tuline & improved by merkisoft and ewowi, and softhack007. if (!strip.isMatrix || !SEGMENT.is2D()) FX_FALLBACK_STATIC; // not a 2D set-up const int cols = SEG_W; const int rows = SEG_H; const auto XY = [&](int x, int y) { return (x%cols) + (y%rows) * cols; }; unsigned dataSize = (SEGMENT.length()+7) >> 3; //1 bit per LED for trails if (!SEGENV.allocateData(dataSize)) FX_FALLBACK_STATIC; //allocation failed if (SEGENV.call == 0) { SEGMENT.fill(BLACK); SEGENV.step = 0; } uint8_t fade = map(SEGMENT.custom1, 0, 255, 50, 250); // equals trail size uint8_t speed = (256-SEGMENT.speed) >> map(min(rows, 150), 0, 150, 0, 3); // slower speeds for small displays uint32_t spawnColor; uint32_t trailColor; if (SEGMENT.check1) { spawnColor = SEGCOLOR(0); trailColor = SEGCOLOR(1); } else { spawnColor = RGBW32(175,255,175,0); trailColor = RGBW32(27,130,39,0); } bool emptyScreen = true; if (strip.now - SEGENV.step >= speed) { SEGENV.step = strip.now; // move pixels one row down. Falling codes keep color and add trail pixels; all others pixels are faded // TODO: it would be better to paint trails idividually instead of relying on fadeToBlackBy() SEGMENT.fadeToBlackBy(fade); for (int row = rows-1; row >= 0; row--) { for (int col = 0; col < cols; col++) { unsigned index = XY(col, row) >> 3; unsigned bitNum = XY(col, row) & 0x07; if (bitRead(SEGENV.data[index], bitNum)) { SEGMENT.setPixelColorXY(col, row, trailColor); // create trail bitClear(SEGENV.data[index], bitNum); if (row < rows-1) { SEGMENT.setPixelColorXY(col, row+1, spawnColor); index = XY(col, row+1) >> 3; bitNum = XY(col, row+1) & 0x07; bitSet(SEGENV.data[index], bitNum); emptyScreen = false; } } } } // spawn new falling code if (hw_random8() <= SEGMENT.intensity || emptyScreen) { uint8_t spawnX = hw_random8(cols); SEGMENT.setPixelColorXY(spawnX, 0, spawnColor); // update hint for next run unsigned index = XY(spawnX, 0) >> 3; unsigned bitNum = XY(spawnX, 0) & 0x07; bitSet(SEGENV.data[index], bitNum); } } } // mode_2Dmatrix() static const char _data_FX_MODE_2DMATRIX[] PROGMEM = "Matrix@!,Spawning rate,Trail,,,Custom color;Spawn,Trail;;2"; ///////////////////////// // 2D Metaballs // ///////////////////////// void mode_2Dmetaballs(void) { // Metaballs by Stefan Petrick. Cannot have one of the dimensions be 2 or less. Adapted by Andrew Tuline. if (!strip.isMatrix || !SEGMENT.is2D()) FX_FALLBACK_STATIC; // not a 2D set-up const int cols = SEG_W; const int rows = SEG_H; float speed = 0.25f * (1+(SEGMENT.speed>>6)); // get some 2 random moving points int x2 = map(perlin8(strip.now * speed, 25355, 685), 0, 255, 0, cols-1); int y2 = map(perlin8(strip.now * speed, 355, 11685), 0, 255, 0, rows-1); int x3 = map(perlin8(strip.now * speed, 55355, 6685), 0, 255, 0, cols-1); int y3 = map(perlin8(strip.now * speed, 25355, 22685), 0, 255, 0, rows-1); // and one Lissajou function int x1 = beatsin8_t(23 * speed, 0, cols-1); int y1 = beatsin8_t(28 * speed, 0, rows-1); for (int y = 0; y < rows; y++) { for (int x = 0; x < cols; x++) { // calculate distances of the 3 points from actual pixel // and add them together with weightening unsigned dx = abs(x - x1); unsigned dy = abs(y - y1); unsigned dist = 2 * sqrt32_bw((dx * dx) + (dy * dy)); dx = abs(x - x2); dy = abs(y - y2); dist += sqrt32_bw((dx * dx) + (dy * dy)); dx = abs(x - x3); dy = abs(y - y3); dist += sqrt32_bw((dx * dx) + (dy * dy)); // inverse result int color = dist ? 1000 / dist : 255; // map color between thresholds if (color > 0 and color < 60) { SEGMENT.setPixelColorXY(x, y, SEGMENT.color_from_palette(map(color * 9, 9, 531, 0, 255), false, PALETTE_SOLID_WRAP, 0)); } else { SEGMENT.setPixelColorXY(x, y, SEGMENT.color_from_palette(0, false, PALETTE_SOLID_WRAP, 0)); } // show the 3 points, too SEGMENT.setPixelColorXY(x1, y1, WHITE); SEGMENT.setPixelColorXY(x2, y2, WHITE); SEGMENT.setPixelColorXY(x3, y3, WHITE); } } } // mode_2Dmetaballs() static const char _data_FX_MODE_2DMETABALLS[] PROGMEM = "Metaballs@!;;!;2"; ////////////////////// // 2D Noise // ////////////////////// void mode_2Dnoise(void) { // By Andrew Tuline if (!strip.isMatrix || !SEGMENT.is2D()) FX_FALLBACK_STATIC; // not a 2D set-up const int cols = SEG_W; const int rows = SEG_H; const unsigned scale = SEGMENT.intensity+2; for (int y = 0; y < rows; y++) { for (int x = 0; x < cols; x++) { uint8_t pixelHue8 = perlin8(x * scale, y * scale, strip.now / (16 - SEGMENT.speed/16)); SEGMENT.setPixelColorXY(x, y, ColorFromPalette(SEGPALETTE, pixelHue8)); } } } // mode_2Dnoise() static const char _data_FX_MODE_2DNOISE[] PROGMEM = "Noise2D@!,Scale;;!;2"; ////////////////////////////// // 2D Plasma Ball // ////////////////////////////// void mode_2DPlasmaball(void) { // By: Stepko https://editor.soulmatelights.com/gallery/659-plasm-ball , Modified by: Andrew Tuline if (!strip.isMatrix || !SEGMENT.is2D()) FX_FALLBACK_STATIC; // not a 2D set-up const int cols = SEG_W; const int rows = SEG_H; SEGMENT.fadeToBlackBy(SEGMENT.custom1>>2); uint_fast32_t t = (strip.now * 8) / (256 - SEGMENT.speed); // optimized to avoid float for (int i = 0; i < cols; i++) { unsigned thisVal = perlin8(i * 30, t, t); unsigned thisMax = map(thisVal, 0, 255, 0, cols-1); for (int j = 0; j < rows; j++) { unsigned thisVal_ = perlin8(t, j * 30, t); unsigned thisMax_ = map(thisVal_, 0, 255, 0, rows-1); int x = (i + thisMax_ - cols / 2); int y = (j + thisMax - cols / 2); int cx = (i + thisMax_); int cy = (j + thisMax); SEGMENT.addPixelColorXY(i, j, ((x - y > -2) && (x - y < 2)) || ((cols - 1 - x - y) > -2 && (cols - 1 - x - y < 2)) || (cols - cx == 0) || (cols - 1 - cx == 0) || ((rows - cy == 0) || (rows - 1 - cy == 0)) ? ColorFromPalette(SEGPALETTE, beat8(5), thisVal, LINEARBLEND) : CRGB::Black); } } SEGMENT.blur(SEGMENT.custom2>>5); } // mode_2DPlasmaball() static const char _data_FX_MODE_2DPLASMABALL[] PROGMEM = "Plasma Ball@Speed,,Fade,Blur;;!;2"; //////////////////////////////// // 2D Polar Lights // //////////////////////////////// void mode_2DPolarLights(void) { // By: Kostyantyn Matviyevskyy https://editor.soulmatelights.com/gallery/762-polar-lights , Modified by: Andrew Tuline & @dedehai (palette support) if (!strip.isMatrix || !SEGMENT.is2D()) FX_FALLBACK_STATIC; // not a 2D set-up const int cols = SEG_W; const int rows = SEG_H; if (SEGENV.call == 0) { SEGMENT.fill(BLACK); SEGENV.step = 0; } float adjustHeight = (float)map(rows, 8, 32, 28, 12); // maybe use mapf() ??? unsigned adjScale = map(cols, 8, 64, 310, 63); unsigned _scale = map(SEGMENT.intensity, 0, 255, 30, adjScale); int _speed = map(SEGMENT.speed, 0, 255, 128, 16); for (int x = 0; x < cols; x++) { for (int y = 0; y < rows; y++) { SEGENV.step++; uint8_t palindex = qsub8(perlin8((SEGENV.step%2) + x * _scale, y * 16 + SEGENV.step % 16, SEGENV.step / _speed), fabsf((float)rows / 2.0f - (float)y) * adjustHeight); uint8_t palbrightness = palindex; if(SEGMENT.check1) palindex = 255 - palindex; //flip palette SEGMENT.setPixelColorXY(x, y, SEGMENT.color_from_palette(palindex, false, false, 255, palbrightness)); } } } // mode_2DPolarLights() static const char _data_FX_MODE_2DPOLARLIGHTS[] PROGMEM = "Polar Lights@!,Scale,,,,Flip Palette;;!;2;pal=71"; ///////////////////////// // 2D Pulser // ///////////////////////// void mode_2DPulser(void) { // By: ldirko https://editor.soulmatelights.com/gallery/878-pulse-test , modifed by: Andrew Tuline if (!strip.isMatrix || !SEGMENT.is2D()) FX_FALLBACK_STATIC; // not a 2D set-up const int cols = SEG_W; const int rows = SEG_H; SEGMENT.fadeToBlackBy(8 - (SEGMENT.intensity>>5)); uint32_t a = strip.now / (18 - SEGMENT.speed / 16); int x = (a / 14) % cols; int y = map((sin8_t(a * 5) + sin8_t(a * 4) + sin8_t(a * 2)), 0, 765, rows-1, 0); SEGMENT.setPixelColorXY(x, y, ColorFromPalette(SEGPALETTE, map(y, 0, rows-1, 0, 255), 255, LINEARBLEND)); SEGMENT.blur(SEGMENT.intensity>>4); } // mode_2DPulser() static const char _data_FX_MODE_2DPULSER[] PROGMEM = "Pulser@!,Blur;;!;2"; ///////////////////////// // 2D Sindots // ///////////////////////// void mode_2DSindots(void) { // By: ldirko https://editor.soulmatelights.com/gallery/597-sin-dots , modified by: Andrew Tuline if (!strip.isMatrix || !SEGMENT.is2D()) FX_FALLBACK_STATIC; // not a 2D set-up const int cols = SEG_W; const int rows = SEG_H; if (SEGENV.call == 0) { SEGMENT.fill(BLACK); } SEGMENT.fadeToBlackBy((SEGMENT.custom1>>3) + (SEGMENT.check1 * 24)); byte t1 = strip.now / (257 - SEGMENT.speed); // 20; byte t2 = sin8_t(t1) / 4 * 2; for (int i = 0; i < 13; i++) { int x = sin8_t(t1 + i * SEGMENT.intensity/8)*(cols-1)/255; // max index now 255x15/255=15! int y = sin8_t(t2 + i * SEGMENT.intensity/8)*(rows-1)/255; // max index now 255x15/255=15! SEGMENT.setPixelColorXY(x, y, ColorFromPalette(SEGPALETTE, i * 255 / 13, 255, LINEARBLEND)); } SEGMENT.blur(SEGMENT.custom2 >> (3 + SEGMENT.check1), SEGMENT.check1); } // mode_2DSindots() static const char _data_FX_MODE_2DSINDOTS[] PROGMEM = "Sindots@!,Dot distance,Fade rate,Blur,,Smear;;!;2;"; ////////////////////////////// // 2D Squared Swirl // ////////////////////////////// // custom3 affects the blur amount. void mode_2Dsquaredswirl(void) { // By: Mark Kriegsman. https://gist.github.com/kriegsman/368b316c55221134b160 // Modifed by: Andrew Tuline if (!strip.isMatrix || !SEGMENT.is2D()) FX_FALLBACK_STATIC; // not a 2D set-up const int cols = SEG_W; const int rows = SEG_H; const uint8_t kBorderWidth = 2; SEGMENT.fadeToBlackBy(1 + SEGMENT.intensity / 5); SEGMENT.blur(SEGMENT.custom3>>1); // Use two out-of-sync sine waves int i = beatsin8_t(19, kBorderWidth, cols-kBorderWidth); int j = beatsin8_t(22, kBorderWidth, cols-kBorderWidth); int k = beatsin8_t(17, kBorderWidth, cols-kBorderWidth); int m = beatsin8_t(18, kBorderWidth, rows-kBorderWidth); int n = beatsin8_t(15, kBorderWidth, rows-kBorderWidth); int p = beatsin8_t(20, kBorderWidth, rows-kBorderWidth); SEGMENT.addPixelColorXY(i, m, ColorFromPalette(SEGPALETTE, strip.now/29, 255, LINEARBLEND)); SEGMENT.addPixelColorXY(j, n, ColorFromPalette(SEGPALETTE, strip.now/41, 255, LINEARBLEND)); SEGMENT.addPixelColorXY(k, p, ColorFromPalette(SEGPALETTE, strip.now/73, 255, LINEARBLEND)); } // mode_2Dsquaredswirl() static const char _data_FX_MODE_2DSQUAREDSWIRL[] PROGMEM = "Squared Swirl@,Fade,,,Blur;;!;2"; ////////////////////////////// // 2D Sun Radiation // ////////////////////////////// void mode_2DSunradiation(void) { // By: ldirko https://editor.soulmatelights.com/gallery/599-sun-radiation , modified by: Andrew Tuline if (!strip.isMatrix || !SEGMENT.is2D()) FX_FALLBACK_STATIC; // not a 2D set-up const int cols = SEG_W; const int rows = SEG_H; if (!SEGENV.allocateData(sizeof(byte)*(cols+2)*(rows+2))) FX_FALLBACK_STATIC; //allocation failed byte *bump = reinterpret_cast(SEGENV.data); if (SEGENV.call == 0) { SEGMENT.fill(BLACK); } unsigned long t = strip.now / 4; unsigned index = 0; uint8_t someVal = SEGMENT.speed/4; // Was 25. for (int j = 0; j < (rows + 2); j++) { for (int i = 0; i < (cols + 2); i++) { //byte col = (inoise8_raw(i * someVal, j * someVal, t)) / 2; byte col = ((int16_t)perlin8(i * someVal, j * someVal, t) - 0x7F) / 3; bump[index++] = col; } } int yindex = cols + 3; int vly = -(rows / 2 + 1); for (int y = 0; y < rows; y++) { ++vly; int vlx = -(cols / 2 + 1); for (int x = 0; x < cols; x++) { ++vlx; int nx = bump[x + yindex + 1] - bump[x + yindex - 1]; int ny = bump[x + yindex + (cols + 2)] - bump[x + yindex - (cols + 2)]; unsigned difx = abs8(vlx * 7 - nx); unsigned dify = abs8(vly * 7 - ny); int temp = difx * difx + dify * dify; int col = 255 - temp / 8; //8 its a size of effect if (col < 0) col = 0; SEGMENT.setPixelColorXY(x, y, HeatColor(col / (3.0f-(float)(SEGMENT.intensity)/128.f))); } yindex += (cols + 2); } } // mode_2DSunradiation() static const char _data_FX_MODE_2DSUNRADIATION[] PROGMEM = "Sun Radiation@Variance,Brightness;;;2"; ///////////////////////// // 2D Tartan // ///////////////////////// void mode_2Dtartan(void) { // By: Elliott Kember https://editor.soulmatelights.com/gallery/3-tartan , Modified by: Andrew Tuline if (!strip.isMatrix || !SEGMENT.is2D()) FX_FALLBACK_STATIC; // not a 2D set-up const int cols = SEG_W; const int rows = SEG_H; if (SEGENV.call == 0) { SEGMENT.fill(BLACK); } uint8_t hue, bri; size_t intensity; int offsetX = beatsin16_t(3, -360, 360); int offsetY = beatsin16_t(2, -360, 360); int sharpness = SEGMENT.custom3 / 8; // 0-3 for (int x = 0; x < cols; x++) { for (int y = 0; y < rows; y++) { hue = x * beatsin16_t(10, 1, 10) + offsetY; intensity = bri = sin8_t(x * SEGMENT.speed/2 + offsetX); for (int i=0; i>= 8*sharpness; SEGMENT.setPixelColorXY(x, y, ColorFromPalette(SEGPALETTE, hue, intensity, LINEARBLEND)); hue = y * 3 + offsetX; intensity = bri = sin8_t(y * SEGMENT.intensity/2 + offsetY); for (int i=0; i>= 8*sharpness; SEGMENT.addPixelColorXY(x, y, ColorFromPalette(SEGPALETTE, hue, intensity, LINEARBLEND)); } } } // mode_2DTartan() static const char _data_FX_MODE_2DTARTAN[] PROGMEM = "Tartan@X scale,Y scale,,,Sharpness;;!;2"; ///////////////////////// // 2D spaceships // ///////////////////////// void mode_2Dspaceships(void) { //// Space ships by stepko (c)05.02.21 [https://editor.soulmatelights.com/gallery/639-space-ships], adapted by Blaz Kristan (AKA blazoncek) if (!strip.isMatrix || !SEGMENT.is2D()) FX_FALLBACK_STATIC; // not a 2D set-up const int cols = SEG_W; const int rows = SEG_H; uint32_t tb = strip.now >> 12; // every ~4s if (tb > SEGENV.step) { int dir = ++SEGENV.aux0; dir += (int)hw_random8(3)-1; if (dir > 7) SEGENV.aux0 = 0; else if (dir < 0) SEGENV.aux0 = 7; else SEGENV.aux0 = dir; SEGENV.step = tb + hw_random8(4); } SEGMENT.fadeToBlackBy(map(SEGMENT.speed, 0, 255, 248, 16)); SEGMENT.move(SEGENV.aux0, 1); for (size_t i = 0; i < 8; i++) { int x = beatsin8_t(12 + i, 2, cols - 3); int y = beatsin8_t(15 + i, 2, rows - 3); uint32_t color = ColorFromPalette(SEGPALETTE, beatsin8_t(12 + i, 0, 255), 255); SEGMENT.addPixelColorXY(x, y, color); if (cols > 24 || rows > 24) { SEGMENT.addPixelColorXY(x+1, y, color); SEGMENT.addPixelColorXY(x-1, y, color); SEGMENT.addPixelColorXY(x, y+1, color); SEGMENT.addPixelColorXY(x, y-1, color); } } SEGMENT.blur(SEGMENT.intensity >> 3, SEGMENT.check1); } static const char _data_FX_MODE_2DSPACESHIPS[] PROGMEM = "Spaceships@!,Blur,,,,Smear;;!;2"; ///////////////////////// // 2D Crazy Bees // ///////////////////////// //// Crazy bees by stepko (c)12.02.21 [https://editor.soulmatelights.com/gallery/651-crazy-bees], adapted by Blaz Kristan (AKA blazoncek), improved by @dedehai #define MAX_BEES 5 void mode_2Dcrazybees(void) { if (!strip.isMatrix || !SEGMENT.is2D()) FX_FALLBACK_STATIC; // not a 2D set-up const int cols = SEG_W; const int rows = SEG_H; byte n = MIN(MAX_BEES, (rows * cols) / 256 + 1); typedef struct Bee { uint8_t posX, posY, aimX, aimY, hue; int8_t deltaX, deltaY, signX, signY, error; void aimed(uint16_t w, uint16_t h) { //random16_set_seed(millis()); aimX = random8(0, w); aimY = random8(0, h); hue = random8(); deltaX = abs(aimX - posX); deltaY = abs(aimY - posY); signX = posX < aimX ? 1 : -1; signY = posY < aimY ? 1 : -1; error = deltaX - deltaY; }; } bee_t; if (!SEGENV.allocateData(sizeof(bee_t)*MAX_BEES)) FX_FALLBACK_STATIC; //allocation failed bee_t *bee = reinterpret_cast(SEGENV.data); if (SEGENV.call == 0) { random16_set_seed(strip.now); for (size_t i = 0; i < n; i++) { bee[i].posX = random8(0, cols); bee[i].posY = random8(0, rows); bee[i].aimed(cols, rows); } } if (strip.now > SEGENV.step) { SEGENV.step = strip.now + (FRAMETIME * 16 / ((SEGMENT.speed>>4)+1)); SEGMENT.fadeToBlackBy(32 + ((SEGMENT.check1*SEGMENT.intensity) / 25)); SEGMENT.blur(SEGMENT.intensity / (2 + SEGMENT.check1 * 9), SEGMENT.check1); for (size_t i = 0; i < n; i++) { uint32_t flowerCcolor = SEGMENT.color_from_palette(bee[i].hue, false, true, 255); SEGMENT.addPixelColorXY(bee[i].aimX + 1, bee[i].aimY, flowerCcolor); SEGMENT.addPixelColorXY(bee[i].aimX, bee[i].aimY + 1, flowerCcolor); SEGMENT.addPixelColorXY(bee[i].aimX - 1, bee[i].aimY, flowerCcolor); SEGMENT.addPixelColorXY(bee[i].aimX, bee[i].aimY - 1, flowerCcolor); if (bee[i].posX != bee[i].aimX || bee[i].posY != bee[i].aimY) { SEGMENT.setPixelColorXY(bee[i].posX, bee[i].posY, CRGB(CHSV(bee[i].hue, 60, 255))); int error2 = bee[i].error * 2; if (error2 > -bee[i].deltaY) { bee[i].error -= bee[i].deltaY; bee[i].posX += bee[i].signX; } if (error2 < bee[i].deltaX) { bee[i].error += bee[i].deltaX; bee[i].posY += bee[i].signY; } } else { bee[i].aimed(cols, rows); } } } } static const char _data_FX_MODE_2DCRAZYBEES[] PROGMEM = "Crazy Bees@!,Blur,,,,Smear;;!;2;pal=11,ix=0"; #undef MAX_BEES #ifdef WLED_PS_DONT_REPLACE_2D_FX ///////////////////////// // 2D Ghost Rider // ///////////////////////// //// Ghost Rider by stepko (c)2021 [https://editor.soulmatelights.com/gallery/716-ghost-rider], adapted by Blaz Kristan (AKA blazoncek) #define LIGHTERS_AM 64 // max lighters (adequate for 32x32 matrix) void mode_2Dghostrider(void) { if (!strip.isMatrix || !SEGMENT.is2D()) FX_FALLBACK_STATIC; // not a 2D set-up const int cols = SEG_W; const int rows = SEG_H; typedef struct Lighter { int16_t gPosX; int16_t gPosY; uint16_t gAngle; int8_t angleSpeed; uint16_t lightersPosX[LIGHTERS_AM]; uint16_t lightersPosY[LIGHTERS_AM]; uint16_t Angle[LIGHTERS_AM]; uint16_t time[LIGHTERS_AM]; bool reg[LIGHTERS_AM]; int8_t Vspeed; } lighter_t; if (!SEGENV.allocateData(sizeof(lighter_t))) FX_FALLBACK_STATIC; //allocation failed lighter_t *lighter = reinterpret_cast(SEGENV.data); const size_t maxLighters = min(cols + rows, LIGHTERS_AM); if (SEGENV.aux0 != cols || SEGENV.aux1 != rows) { SEGENV.aux0 = cols; SEGENV.aux1 = rows; lighter->angleSpeed = hw_random8(0,20) - 10; lighter->gAngle = hw_random16(); lighter->Vspeed = 5; lighter->gPosX = (cols/2) * 10; lighter->gPosY = (rows/2) * 10; for (size_t i = 0; i < maxLighters; i++) { lighter->lightersPosX[i] = lighter->gPosX; lighter->lightersPosY[i] = lighter->gPosY + i; lighter->time[i] = i * 2; lighter->reg[i] = false; } } if (strip.now > SEGENV.step) { SEGENV.step = strip.now + 1024 / (cols+rows); SEGMENT.fadeToBlackBy((SEGMENT.speed>>2)+64); CRGB color = CRGB::White; SEGMENT.wu_pixel(lighter->gPosX * 256 / 10, lighter->gPosY * 256 / 10, color); lighter->gPosX += lighter->Vspeed * sin_t(radians(lighter->gAngle)); lighter->gPosY += lighter->Vspeed * cos_t(radians(lighter->gAngle)); lighter->gAngle += lighter->angleSpeed; if (lighter->gPosX < 0) lighter->gPosX = (cols - 1) * 10; if (lighter->gPosX > (cols - 1) * 10) lighter->gPosX = 0; if (lighter->gPosY < 0) lighter->gPosY = (rows - 1) * 10; if (lighter->gPosY > (rows - 1) * 10) lighter->gPosY = 0; for (size_t i = 0; i < maxLighters; i++) { lighter->time[i] += hw_random8(5, 20); if (lighter->time[i] >= 255 || (lighter->lightersPosX[i] <= 0) || (lighter->lightersPosX[i] >= (cols - 1) * 10) || (lighter->lightersPosY[i] <= 0) || (lighter->lightersPosY[i] >= (rows - 1) * 10)) { lighter->reg[i] = true; } if (lighter->reg[i]) { lighter->lightersPosY[i] = lighter->gPosY; lighter->lightersPosX[i] = lighter->gPosX; lighter->Angle[i] = lighter->gAngle + ((int)hw_random8(20) - 10); lighter->time[i] = 0; lighter->reg[i] = false; } else { lighter->lightersPosX[i] += -7 * sin_t(radians(lighter->Angle[i])); lighter->lightersPosY[i] += -7 * cos_t(radians(lighter->Angle[i])); } SEGMENT.wu_pixel(lighter->lightersPosX[i] * 256 / 10, lighter->lightersPosY[i] * 256 / 10, ColorFromPalette(SEGPALETTE, (256 - lighter->time[i]))); } SEGMENT.blur(SEGMENT.intensity>>3); } } static const char _data_FX_MODE_2DGHOSTRIDER[] PROGMEM = "Ghost Rider@Fade rate,Blur;;!;2"; #undef LIGHTERS_AM //////////////////////////// // 2D Floating Blobs // //////////////////////////// //// Floating Blobs by stepko (c)2021 [https://editor.soulmatelights.com/gallery/573-blobs], adapted by Blaz Kristan (AKA blazoncek) #define MAX_BLOBS 8 void mode_2Dfloatingblobs(void) { if (!strip.isMatrix || !SEGMENT.is2D()) FX_FALLBACK_STATIC; // not a 2D set-up const int cols = SEG_W; const int rows = SEG_H; typedef struct Blob { float x[MAX_BLOBS], y[MAX_BLOBS]; float sX[MAX_BLOBS], sY[MAX_BLOBS]; // speed float r[MAX_BLOBS]; bool grow[MAX_BLOBS]; byte color[MAX_BLOBS]; } blob_t; size_t Amount = (SEGMENT.intensity>>5) + 1; // NOTE: be sure to update MAX_BLOBS if you change this if (!SEGENV.allocateData(sizeof(blob_t))) FX_FALLBACK_STATIC; //allocation failed blob_t *blob = reinterpret_cast(SEGENV.data); if (SEGENV.aux0 != cols || SEGENV.aux1 != rows) { SEGENV.aux0 = cols; // re-initialise if virtual size changes SEGENV.aux1 = rows; //SEGMENT.fill(BLACK); for (size_t i = 0; i < MAX_BLOBS; i++) { blob->r[i] = hw_random8(1, cols>8 ? (cols/4) : 2); blob->sX[i] = (float) hw_random8(3, cols) / (float)(256 - SEGMENT.speed); // speed x blob->sY[i] = (float) hw_random8(3, rows) / (float)(256 - SEGMENT.speed); // speed y blob->x[i] = hw_random8(0, cols-1); blob->y[i] = hw_random8(0, rows-1); blob->color[i] = hw_random8(); blob->grow[i] = (blob->r[i] < 1.f); if (blob->sX[i] == 0) blob->sX[i] = 1; if (blob->sY[i] == 0) blob->sY[i] = 1; } } SEGMENT.fadeToBlackBy((SEGMENT.custom2>>3)+1); // Bounce balls around for (size_t i = 0; i < Amount; i++) { if (SEGENV.step < strip.now) blob->color[i] = add8(blob->color[i], 4); // slowly change color // change radius if needed if (blob->grow[i]) { // enlarge radius until it is >= 4 blob->r[i] += (fabsf(blob->sX[i]) > fabsf(blob->sY[i]) ? fabsf(blob->sX[i]) : fabsf(blob->sY[i])) * 0.05f; if (blob->r[i] >= MIN(cols/4.f,2.f)) { blob->grow[i] = false; } } else { // reduce radius until it is < 1 blob->r[i] -= (fabsf(blob->sX[i]) > fabsf(blob->sY[i]) ? fabsf(blob->sX[i]) : fabsf(blob->sY[i])) * 0.05f; if (blob->r[i] < 1.f) { blob->grow[i] = true; } } uint32_t c = SEGMENT.color_from_palette(blob->color[i], false, false, 0); if (blob->r[i] > 1.f) SEGMENT.fillCircle(roundf(blob->x[i]), roundf(blob->y[i]), roundf(blob->r[i]), c); else SEGMENT.setPixelColorXY((int)roundf(blob->x[i]), (int)roundf(blob->y[i]), c); // move x if (blob->x[i] + blob->r[i] >= cols - 1) blob->x[i] += (blob->sX[i] * ((cols - 1 - blob->x[i]) / blob->r[i] + 0.005f)); else if (blob->x[i] - blob->r[i] <= 0) blob->x[i] += (blob->sX[i] * (blob->x[i] / blob->r[i] + 0.005f)); else blob->x[i] += blob->sX[i]; // move y if (blob->y[i] + blob->r[i] >= rows - 1) blob->y[i] += (blob->sY[i] * ((rows - 1 - blob->y[i]) / blob->r[i] + 0.005f)); else if (blob->y[i] - blob->r[i] <= 0) blob->y[i] += (blob->sY[i] * (blob->y[i] / blob->r[i] + 0.005f)); else blob->y[i] += blob->sY[i]; // bounce x if (blob->x[i] < 0.01f) { blob->sX[i] = (float)hw_random8(3, cols) / (256 - SEGMENT.speed); blob->x[i] = 0.01f; } else if (blob->x[i] > (float)cols - 1.01f) { blob->sX[i] = (float)hw_random8(3, cols) / (256 - SEGMENT.speed); blob->sX[i] = -blob->sX[i]; blob->x[i] = (float)cols - 1.01f; } // bounce y if (blob->y[i] < 0.01f) { blob->sY[i] = (float)hw_random8(3, rows) / (256 - SEGMENT.speed); blob->y[i] = 0.01f; } else if (blob->y[i] > (float)rows - 1.01f) { blob->sY[i] = (float)hw_random8(3, rows) / (256 - SEGMENT.speed); blob->sY[i] = -blob->sY[i]; blob->y[i] = (float)rows - 1.01f; } } SEGMENT.blur(SEGMENT.custom1>>2); if (SEGENV.step < strip.now) SEGENV.step = strip.now + 2000; // change colors every 2 seconds } static const char _data_FX_MODE_2DBLOBS[] PROGMEM = "Blobs@!,# blobs,Blur,Trail;!;!;2;c1=8"; #undef MAX_BLOBS #endif // WLED_PS_DONT_REPLACE_2D_FX //////////////////////////// // 2D Scrolling text // //////////////////////////// void mode_2Dscrollingtext(void) { if (!strip.isMatrix || !SEGMENT.is2D()) FX_FALLBACK_STATIC; // not a 2D set-up const int cols = SEG_W; const int rows = SEG_H; unsigned letterWidth, rotLW; unsigned letterHeight, rotLH; switch (map(SEGMENT.custom2, 0, 255, 1, 5)) { default: case 1: letterWidth = 4; letterHeight = 6; break; case 2: letterWidth = 5; letterHeight = 8; break; case 3: letterWidth = 6; letterHeight = 8; break; case 4: letterWidth = 7; letterHeight = 9; break; case 5: letterWidth = 5; letterHeight = 12; break; } // letters are rotated const int8_t rotate = map(SEGMENT.custom3, 0, 31, -2, 2); if (rotate == 1 || rotate == -1) { rotLH = letterWidth; rotLW = letterHeight; } else { rotLW = letterWidth; rotLH = letterHeight; } char text[WLED_MAX_SEGNAME_LEN+1] = {'\0'}; size_t result_pos = 0; char sec[5]; int AmPmHour = hour(localTime); bool isitAM = true; if (useAMPM) { if (AmPmHour > 11) { AmPmHour -= 12; isitAM = false; } if (AmPmHour == 0) { AmPmHour = 12; } sprintf_P(sec, PSTR(" %2s"), (isitAM ? "AM" : "PM")); } else { sprintf_P(sec, PSTR(":%02d"), second(localTime)); } size_t len = 0; if (SEGMENT.name) len = strlen(SEGMENT.name); // note: SEGMENT.name is limited to WLED_MAX_SEGNAME_LEN if (len == 0) { // fallback if empty segment name: display date and time sprintf_P(text, PSTR("%s %d, %d %d:%02d%s"), monthShortStr(month(localTime)), day(localTime), year(localTime), AmPmHour, minute(localTime), sec); } else { size_t i = 0; while (i < len) { if (SEGMENT.name[i] == '#') { char token[7]; // copy up to 6 chars + null terminator bool zero = false; // a 0 suffix means display leading zeros size_t j = 0; while (j < 6 && i + j < len) { token[j] = std::toupper(SEGMENT.name[i + j]); if(token[j] == '0') zero = true; // 0 suffix found. Note: there is an edge case where a '0' could be part of a trailing text and not the token, handling it is not worth the effort j++; } token[j] = '\0'; int advance = 5; // number of chars to advance in 'text' after processing the token // Process token char temp[32]; if (!strncmp_P(token,PSTR("#DATE"),5)) sprintf_P(temp, zero?PSTR("%02d.%02d.%04d"):PSTR("%d.%d.%d"), day(localTime), month(localTime), year(localTime)); else if (!strncmp_P(token,PSTR("#DDMM"),5)) sprintf_P(temp, zero?PSTR("%02d.%02d") :PSTR("%d.%d"), day(localTime), month(localTime)); else if (!strncmp_P(token,PSTR("#MMDD"),5)) sprintf_P(temp, zero?PSTR("%02d/%02d") :PSTR("%d/%d"), month(localTime), day(localTime)); else if (!strncmp_P(token,PSTR("#TIME"),5)) sprintf_P(temp, zero?PSTR("%02d:%02d%s") :PSTR("%2d:%02d%s"), AmPmHour, minute(localTime), sec); else if (!strncmp_P(token,PSTR("#HHMM"),5)) sprintf_P(temp, zero?PSTR("%02d:%02d") :PSTR("%d:%02d"), AmPmHour, minute(localTime)); else if (!strncmp_P(token,PSTR("#YYYY"),5)) sprintf_P(temp, PSTR("%04d") , year(localTime)); else if (!strncmp_P(token,PSTR("#MONL"),5)) sprintf (temp, ("%s") , monthStr(month(localTime))); else if (!strncmp_P(token,PSTR("#DDDD"),5)) sprintf (temp, ("%s") , dayStr(weekday(localTime))); else if (!strncmp_P(token,PSTR("#YY"),3)) { sprintf (temp, ("%02d") , year(localTime)%100); advance = 3; } else if (!strncmp_P(token,PSTR("#HH"),3)) { sprintf (temp, zero? ("%02d") : ("%d"), AmPmHour); advance = 3; } else if (!strncmp_P(token,PSTR("#MM"),3)) { sprintf (temp, zero? ("%02d") : ("%d"), minute(localTime)); advance = 3; } else if (!strncmp_P(token,PSTR("#SS"),3)) { sprintf (temp, zero? ("%02d") : ("%d"), second(localTime)); advance = 3; } else if (!strncmp_P(token,PSTR("#MON"),4)) { sprintf (temp, ("%s") , monthShortStr(month(localTime))); advance = 4; } else if (!strncmp_P(token,PSTR("#MO"),3)) { sprintf (temp, zero? ("%02d") : ("%d"), month(localTime)); advance = 3; } else if (!strncmp_P(token,PSTR("#DAY"),4)) { sprintf (temp, ("%s") , dayShortStr(weekday(localTime))); advance = 4; } else if (!strncmp_P(token,PSTR("#DD"),3)) { sprintf (temp, zero? ("%02d") : ("%d"), day(localTime)); advance = 3; } else { temp[0] = '#'; temp[1] = '\0'; zero = false; advance = 1; } // Unknown token, just copy the # if(zero) advance++; // skip the '0' suffix size_t temp_len = strlen(temp); if (result_pos + temp_len < WLED_MAX_SEGNAME_LEN) { strcpy(text + result_pos, temp); result_pos += temp_len; } i += advance; } else { if (result_pos < WLED_MAX_SEGNAME_LEN) { text[result_pos++] = SEGMENT.name[i++]; // no token, just copy char } else break; // buffer full } } } const int numberOfLetters = strlen(text); int width = (numberOfLetters * rotLW); int yoffset = map(SEGMENT.intensity, 0, 255, -rows/2, rows/2) + (rows-rotLH)/2; if (width <= cols) { // scroll vertically (e.g. ^^ Way out ^^) if it fits int speed = map(SEGMENT.speed, 0, 255, 5000, 1000); int frac = strip.now % speed + 1; if (SEGMENT.intensity == 255) { yoffset = (2 * frac * rows)/speed - rows; } else if (SEGMENT.intensity == 0) { yoffset = rows - (2 * frac * rows)/speed; } } if (SEGENV.step < strip.now) { // calculate start offset if (width > cols) { if (SEGMENT.check3) { if (SEGENV.aux0 == 0) SEGENV.aux0 = width + cols - 1; else --SEGENV.aux0; } else ++SEGENV.aux0 %= width + cols; } else SEGENV.aux0 = (cols + width)/2; ++SEGENV.aux1 &= 0xFF; // color shift SEGENV.step = strip.now + map(SEGMENT.speed, 0, 255, 250, 50); // shift letters every ~250ms to ~50ms } SEGMENT.fade_out(255 - (SEGMENT.custom1>>4)); // trail uint32_t col1 = SEGMENT.color_from_palette(SEGENV.aux1, false, PALETTE_SOLID_WRAP, 0); uint32_t col2 = BLACK; // if gradient is selected and palette is default (0) drawCharacter() uses gradient from SEGCOLOR(0) to SEGCOLOR(2) // otherwise col2 == BLACK means use currently selected palette for gradient // if gradient is not selected set both colors the same if (SEGMENT.check1) { // use gradient if (SEGMENT.palette == 0) { // use colors for gradient col1 = SEGCOLOR(0); col2 = SEGCOLOR(2); } } else col2 = col1; // force characters to use single color (from palette) for (int i = 0; i < numberOfLetters; i++) { int xoffset = int(cols) - int(SEGENV.aux0) + rotLW*i; if (xoffset + rotLW < 0) continue; // don't draw characters off-screen SEGMENT.drawCharacter(text[i], xoffset, yoffset, letterWidth, letterHeight, col1, col2, rotate); } } static const char _data_FX_MODE_2DSCROLLTEXT[] PROGMEM = "Scrolling Text@!,Y Offset,Trail,Font size,Rotate,Gradient,,Reverse;!,!,Gradient;!;2;ix=128,c1=0,rev=0,mi=0,rY=0,mY=0"; //////////////////////////// // 2D Drift Rose // //////////////////////////// //// Drift Rose by stepko (c)2021 [https://editor.soulmatelights.com/gallery/1369-drift-rose-pattern], adapted by Blaz Kristan (AKA blazoncek) improved by @dedehai void mode_2Ddriftrose(void) { if (!strip.isMatrix || !SEGMENT.is2D()) FX_FALLBACK_STATIC; // not a 2D set-up const int cols = SEG_W; const int rows = SEG_H; const float CX = (cols-cols%2)/2.f - .5f; const float CY = (rows-rows%2)/2.f - .5f; const float L = min(cols, rows) / 2.f; SEGMENT.fadeToBlackBy(32+(SEGMENT.speed>>3)); for (size_t i = 1; i < 37; i++) { float angle = radians(i * 10); uint32_t x = (CX + (sin_t(angle) * (beatsin8_t(i, 0, L*2)-L))) * 255.f; uint32_t y = (CY + (cos_t(angle) * (beatsin8_t(i, 0, L*2)-L))) * 255.f; if(SEGMENT.palette == 0) SEGMENT.wu_pixel(x, y, CHSV(i * 10, 255, 255)); else SEGMENT.wu_pixel(x, y, ColorFromPalette(SEGPALETTE, i * 10)); } SEGMENT.blur(SEGMENT.intensity >> 4, SEGMENT.check1); } static const char _data_FX_MODE_2DDRIFTROSE[] PROGMEM = "Drift Rose@Fade,Blur,,,,Smear;;!;2;pal=11"; ///////////////////////////// // 2D PLASMA ROTOZOOMER // ///////////////////////////// // Plasma Rotozoomer by ldirko (c)2020 [https://editor.soulmatelights.com/gallery/457-plasma-rotozoomer], adapted for WLED by Blaz Kristan (AKA blazoncek) void mode_2Dplasmarotozoom() { if (!strip.isMatrix || !SEGMENT.is2D()) FX_FALLBACK_STATIC; // not a 2D set-up const int cols = SEG_W; const int rows = SEG_H; unsigned dataSize = SEGMENT.length() + sizeof(float); if (!SEGENV.allocateData(dataSize)) FX_FALLBACK_STATIC; //allocation failed float *a = reinterpret_cast(SEGENV.data); byte *plasma = reinterpret_cast(SEGENV.data+sizeof(float)); unsigned ms = strip.now/15; // plasma for (int j = 0; j < rows; j++) { int index = j*cols; for (int i = 0; i < cols; i++) { if (SEGMENT.check1) plasma[index+i] = (i * 4 ^ j * 4) + ms / 6; else plasma[index+i] = inoise8(i * 40, j * 40, ms); } } // rotozoom float f = (sin_t(*a/2)+((128-SEGMENT.intensity)/128.0f)+1.1f)/1.5f; // scale factor float kosinus = cos_t(*a) * f; float sinus = sin_t(*a) * f; for (int i = 0; i < cols; i++) { float u1 = i * kosinus; float v1 = i * sinus; for (int j = 0; j < rows; j++) { byte u = abs8(u1 - j * sinus) % cols; byte v = abs8(v1 + j * kosinus) % rows; SEGMENT.setPixelColorXY(i, j, SEGMENT.color_from_palette(plasma[v*cols+u], false, PALETTE_SOLID_WRAP, 255)); } } *a -= 0.03f + float(SEGENV.speed-128)*0.0002f; // rotation speed if(*a < -6283.18530718f) *a += 6283.18530718f; // 1000*2*PI, protect sin/cos from very large input float values (will give wrong results) } static const char _data_FX_MODE_2DPLASMAROTOZOOM[] PROGMEM = "Rotozoomer@!,Scale,,,,Alt;;!;2;pal=54"; #endif // WLED_DISABLE_2D /////////////////////////////////////////////////////////////////////////////// /******************** audio enhanced routines ************************/ /////////////////////////////////////////////////////////////////////////////// ///////////////////////////////// // * Ripple Peak // ///////////////////////////////// void mode_ripplepeak(void) { // * Ripple peak. By Andrew Tuline. // This currently has no controls. #define MAXSTEPS 16 // Case statement wouldn't allow a variable. unsigned maxRipples = 16; unsigned dataSize = sizeof(Ripple) * maxRipples; if (!SEGENV.allocateData(dataSize)) FX_FALLBACK_STATIC; //allocation failed Ripple* ripples = reinterpret_cast(SEGENV.data); um_data_t *um_data = getAudioData(); uint8_t samplePeak = *(uint8_t*)um_data->u_data[3]; #ifdef ESP32 float FFT_MajorPeak = *(float*) um_data->u_data[4]; #endif uint8_t *maxVol = (uint8_t*)um_data->u_data[6]; uint8_t *binNum = (uint8_t*)um_data->u_data[7]; // printUmData(); if (SEGENV.call == 0) { SEGMENT.custom1 = *binNum; SEGMENT.custom2 = *maxVol * 2; } *binNum = SEGMENT.custom1; // Select a bin. *maxVol = SEGMENT.custom2 / 2; // Our volume comparator. SEGMENT.fade_out(240); // Lower frame rate means less effective fading than FastLED SEGMENT.fade_out(240); for (int i = 0; i < SEGMENT.intensity/16; i++) { // Limit the number of ripples. if (samplePeak) ripples[i].state = 255; switch (ripples[i].state) { case 254: // Inactive mode break; case 255: // Initialize ripple variables. ripples[i].pos = hw_random16(SEGLEN); #ifdef ESP32 if (FFT_MajorPeak > 1) // log10(0) is "forbidden" (throws exception) ripples[i].color = (int)(log10f(FFT_MajorPeak)*128); else ripples[i].color = 0; #else ripples[i].color = hw_random8(); #endif ripples[i].state = 0; break; case 0: SEGMENT.setPixelColor(ripples[i].pos, SEGMENT.color_from_palette(ripples[i].color, false, PALETTE_SOLID_WRAP, 0)); ripples[i].state++; break; case MAXSTEPS: // At the end of the ripples. 254 is an inactive mode. ripples[i].state = 254; break; default: // Middle of the ripples. SEGMENT.setPixelColor((ripples[i].pos + ripples[i].state + SEGLEN) % SEGLEN, color_blend(SEGCOLOR(1), SEGMENT.color_from_palette(ripples[i].color, false, PALETTE_SOLID_WRAP, 0), uint8_t(2*255/ripples[i].state))); SEGMENT.setPixelColor((ripples[i].pos - ripples[i].state + SEGLEN) % SEGLEN, color_blend(SEGCOLOR(1), SEGMENT.color_from_palette(ripples[i].color, false, PALETTE_SOLID_WRAP, 0), uint8_t(2*255/ripples[i].state))); ripples[i].state++; // Next step. break; } // switch step } // for i } // mode_ripplepeak() static const char _data_FX_MODE_RIPPLEPEAK[] PROGMEM = "Ripple Peak@Fade rate,Max # of ripples,Select bin,Volume (min);!,!;!;1v;c2=0,m12=0,si=0"; // Pixel, Beatsin #ifndef WLED_DISABLE_2D ///////////////////////// // * 2D Swirl // ///////////////////////// // By: Mark Kriegsman https://gist.github.com/kriegsman/5adca44e14ad025e6d3b , modified by Andrew Tuline void mode_2DSwirl(void) { if (!strip.isMatrix || !SEGMENT.is2D()) FX_FALLBACK_STATIC; // not a 2D set-up const int cols = SEG_W; const int rows = SEG_H; if (SEGENV.call == 0) { SEGMENT.fill(BLACK); } const uint8_t borderWidth = 2; SEGMENT.blur(SEGMENT.custom1); int i = beatsin8_t( 27*SEGMENT.speed/255, borderWidth, cols - borderWidth); int j = beatsin8_t( 41*SEGMENT.speed/255, borderWidth, rows - borderWidth); int ni = (cols - 1) - i; int nj = (cols - 1) - j; um_data_t *um_data = getAudioData(); float volumeSmth = *(float*) um_data->u_data[0]; //ewowi: use instead of sampleAvg??? int volumeRaw = *(int16_t*) um_data->u_data[1]; SEGMENT.addPixelColorXY( i, j, ColorFromPalette(SEGPALETTE, (strip.now / 11 + volumeSmth*4), volumeRaw * SEGMENT.intensity / 64, LINEARBLEND)); //CHSV( ms / 11, 200, 255); SEGMENT.addPixelColorXY( j, i, ColorFromPalette(SEGPALETTE, (strip.now / 13 + volumeSmth*4), volumeRaw * SEGMENT.intensity / 64, LINEARBLEND)); //CHSV( ms / 13, 200, 255); SEGMENT.addPixelColorXY(ni,nj, ColorFromPalette(SEGPALETTE, (strip.now / 17 + volumeSmth*4), volumeRaw * SEGMENT.intensity / 64, LINEARBLEND)); //CHSV( ms / 17, 200, 255); SEGMENT.addPixelColorXY(nj,ni, ColorFromPalette(SEGPALETTE, (strip.now / 29 + volumeSmth*4), volumeRaw * SEGMENT.intensity / 64, LINEARBLEND)); //CHSV( ms / 29, 200, 255); SEGMENT.addPixelColorXY( i,nj, ColorFromPalette(SEGPALETTE, (strip.now / 37 + volumeSmth*4), volumeRaw * SEGMENT.intensity / 64, LINEARBLEND)); //CHSV( ms / 37, 200, 255); SEGMENT.addPixelColorXY(ni, j, ColorFromPalette(SEGPALETTE, (strip.now / 41 + volumeSmth*4), volumeRaw * SEGMENT.intensity / 64, LINEARBLEND)); //CHSV( ms / 41, 200, 255); } // mode_2DSwirl() static const char _data_FX_MODE_2DSWIRL[] PROGMEM = "Swirl@!,Sensitivity,Blur;,Bg Swirl;!;2v;ix=64,si=0"; // Beatsin // TODO: color 1 unused? ///////////////////////// // * 2D Waverly // ///////////////////////// // By: Stepko, https://editor.soulmatelights.com/gallery/652-wave , modified by Andrew Tuline void mode_2DWaverly(void) { if (!strip.isMatrix || !SEGMENT.is2D()) FX_FALLBACK_STATIC; // not a 2D set-up const int cols = SEG_W; const int rows = SEG_H; um_data_t *um_data = getAudioData(); float volumeSmth = *(float*) um_data->u_data[0]; SEGMENT.fadeToBlackBy(SEGMENT.speed); long t = strip.now / 2; for (int i = 0; i < cols; i++) { unsigned thisVal = (1 + SEGMENT.intensity/64) * perlin8(i * 45 , t , t)/2; // use audio if available if (um_data) { thisVal /= 32; // reduce intensity of perlin8() thisVal *= volumeSmth; } int thisMax = map(thisVal, 0, 512, 0, rows); for (int j = 0; j < thisMax; j++) { SEGMENT.addPixelColorXY(i, j, ColorFromPalette(SEGPALETTE, map(j, 0, thisMax, 250, 0), 255, LINEARBLEND)); SEGMENT.addPixelColorXY((cols - 1) - i, (rows - 1) - j, ColorFromPalette(SEGPALETTE, map(j, 0, thisMax, 250, 0), 255, LINEARBLEND)); } } if (SEGMENT.check3) SEGMENT.blur(16, cols*rows < 100); } // mode_2DWaverly() static const char _data_FX_MODE_2DWAVERLY[] PROGMEM = "Waverly@Amplification,Sensitivity,,,,,Blur;;!;2v;ix=64,si=0"; // Beatsin #endif // WLED_DISABLE_2D // Gravity struct requited for GRAV* effects typedef struct Gravity { int topLED; int gravityCounter; } gravity; /////////////////////// // * GRAVCENTER // /////////////////////// // Gravcenter effects By Andrew Tuline. // Gravcenter base function for Gravcenter (0), Gravcentric (1), Gravimeter (2), Gravfreq (3) (merged by @dedehai) void mode_gravcenter_base(unsigned mode) { if (SEGLEN == 1) FX_FALLBACK_STATIC; const unsigned dataSize = sizeof(gravity); if (!SEGENV.allocateData(dataSize)) FX_FALLBACK_STATIC; //allocation failed Gravity* gravcen = reinterpret_cast(SEGENV.data); um_data_t *um_data = getAudioData(); float volumeSmth = *(float*) um_data->u_data[0]; if(mode == 1) SEGMENT.fade_out(253); // //Gravcentric else if(mode == 2) SEGMENT.fade_out(249); // Gravimeter else if(mode == 3) SEGMENT.fade_out(250); // Gravfreq else SEGMENT.fade_out(251); // Gravcenter float mySampleAvg; int tempsamp; float segmentSampleAvg = volumeSmth * (float)SEGMENT.intensity / 255.0f; if(mode == 2) { //Gravimeter segmentSampleAvg *= 0.25; // divide by 4, to compensate for later "sensitivity" upscaling mySampleAvg = mapf(segmentSampleAvg*2.0, 0, 64, 0, (SEGLEN-1)); // map to pixels availeable in current segment tempsamp = constrain(mySampleAvg,0,SEGLEN-1); // Keep the sample from overflowing. } else { // Gravcenter or Gravcentric or Gravfreq segmentSampleAvg *= 0.125f; // divide by 8, to compensate for later "sensitivity" upscaling mySampleAvg = mapf(segmentSampleAvg*2.0, 0.0f, 32.0f, 0.0f, (float)SEGLEN/2.0f); // map to pixels availeable in current segment tempsamp = constrain(mySampleAvg, 0, SEGLEN/2); // Keep the sample from overflowing. } uint8_t gravity = 8 - SEGMENT.speed/32; int offset = 1; if(mode == 2) offset = 0; // Gravimeter if (tempsamp >= gravcen->topLED) gravcen->topLED = tempsamp-offset; else if (gravcen->gravityCounter % gravity == 0) gravcen->topLED--; if(mode == 1) { //Gravcentric for (int i=0; itopLED >= 0) { SEGMENT.setPixelColor(gravcen->topLED+SEGLEN/2, CRGB::Gray); SEGMENT.setPixelColor(SEGLEN/2-1-gravcen->topLED, CRGB::Gray); } } else if(mode == 2) { //Gravimeter for (int i=0; itopLED > 0) { SEGMENT.setPixelColor(gravcen->topLED, SEGMENT.color_from_palette(strip.now, false, PALETTE_SOLID_WRAP, 0)); } } else if(mode == 3) { //Gravfreq for (int i=0; iu_data[4]; // used in mode 3: Gravfreq if (FFT_MajorPeak < 1) FFT_MajorPeak = 1; uint8_t index = (log10f(FFT_MajorPeak) - (MAX_FREQ_LOG10 - 1.78f)) * 255; SEGMENT.setPixelColor(i+SEGLEN/2, SEGMENT.color_from_palette(index, false, PALETTE_SOLID_WRAP, 0)); SEGMENT.setPixelColor(SEGLEN/2-i-1, SEGMENT.color_from_palette(index, false, PALETTE_SOLID_WRAP, 0)); } if (gravcen->topLED >= 0) { SEGMENT.setPixelColor(gravcen->topLED+SEGLEN/2, CRGB::Gray); SEGMENT.setPixelColor(SEGLEN/2-1-gravcen->topLED, CRGB::Gray); } } else { //Gravcenter for (int i=0; itopLED >= 0) { SEGMENT.setPixelColor(gravcen->topLED+SEGLEN/2, SEGMENT.color_from_palette(strip.now, false, PALETTE_SOLID_WRAP, 0)); SEGMENT.setPixelColor(SEGLEN/2-1-gravcen->topLED, SEGMENT.color_from_palette(strip.now, false, PALETTE_SOLID_WRAP, 0)); } } gravcen->gravityCounter = (gravcen->gravityCounter + 1) % gravity; } void mode_gravcenter(void) { // Gravcenter. By Andrew Tuline. mode_gravcenter_base(0); } static const char _data_FX_MODE_GRAVCENTER[] PROGMEM = "Gravcenter@Rate of fall,Sensitivity;!,!;!;1v;ix=128,m12=2,si=0"; // Circle, Beatsin /////////////////////// // * GRAVCENTRIC // /////////////////////// void mode_gravcentric(void) { // Gravcentric. By Andrew Tuline. mode_gravcenter_base(1); } static const char _data_FX_MODE_GRAVCENTRIC[] PROGMEM = "Gravcentric@Rate of fall,Sensitivity;!,!;!;1v;ix=128,m12=3,si=0"; // Corner, Beatsin /////////////////////// // * GRAVIMETER // /////////////////////// void mode_gravimeter(void) { // Gravmeter. By Andrew Tuline. mode_gravcenter_base(2); } static const char _data_FX_MODE_GRAVIMETER[] PROGMEM = "Gravimeter@Rate of fall,Sensitivity;!,!;!;1v;ix=128,m12=2,si=0"; // Circle, Beatsin /////////////////////// // ** Gravfreq // /////////////////////// void mode_gravfreq(void) { // Gravfreq. By Andrew Tuline. mode_gravcenter_base(3); } static const char _data_FX_MODE_GRAVFREQ[] PROGMEM = "Gravfreq@Rate of fall,Sensitivity;!,!;!;1f;ix=128,m12=0,si=0"; // Pixels, Beatsin ////////////////////// // * JUGGLES // ////////////////////// void mode_juggles(void) { // Juggles. By Andrew Tuline. um_data_t *um_data = getAudioData(); float volumeSmth = *(float*) um_data->u_data[0]; SEGMENT.fade_out(224); // 6.25% uint8_t my_sampleAgc = fmax(fmin(volumeSmth, 255.0), 0); for (size_t i=0; i(SEGENV.data); um_data_t *um_data = getAudioData(); int volumeRaw = *(int16_t*)um_data->u_data[1]; if (SEGENV.call == 0) { for (unsigned i = 0; i < SEGLEN; i++) pixels[i] = BLACK; // may not be needed as resetIfRequired() clears buffer } uint8_t secondHand = micros()/(256-SEGMENT.speed)/500 % 16; if(SEGENV.aux0 != secondHand) { SEGENV.aux0 = secondHand; int pixBri = volumeRaw * SEGMENT.intensity / 64; unsigned k = SEGLEN-1; // loop will not execute if SEGLEN equals 1 for (unsigned i = 0; i < k; i++) { pixels[i] = pixels[i+1]; // shift left SEGMENT.setPixelColor(i, pixels[i]); } pixels[k] = color_blend(SEGCOLOR(1), SEGMENT.color_from_palette(strip.now, false, PALETTE_SOLID_WRAP, 0), pixBri); SEGMENT.setPixelColor(k, pixels[k]); } } // mode_matripix() static const char _data_FX_MODE_MATRIPIX[] PROGMEM = "Matripix@!,Brightness;!,!;!;1v;ix=64,m12=2,si=1"; //,rev=1,mi=1,rY=1,mY=1 Circle, WeWillRockYou, reverseX ////////////////////// // * MIDNOISE // ////////////////////// void mode_midnoise(void) { // Midnoise. By Andrew Tuline. if (SEGLEN <= 1) FX_FALLBACK_STATIC; // Changing xdist to SEGENV.aux0 and ydist to SEGENV.aux1. um_data_t *um_data = getAudioData(); float volumeSmth = *(float*) um_data->u_data[0]; SEGMENT.fade_out(SEGMENT.speed); SEGMENT.fade_out(SEGMENT.speed); float tmpSound2 = volumeSmth * (float)SEGMENT.intensity / 256.0; // Too sensitive. tmpSound2 *= (float)SEGMENT.intensity / 128.0; // Reduce sensitivity/length. unsigned maxLen = mapf(tmpSound2, 0, 127, 0, SEGLEN/2); if (maxLen >SEGLEN/2) maxLen = SEGLEN/2; for (unsigned i=(SEGLEN/2-maxLen); i<(SEGLEN/2+maxLen); i++) { uint8_t index = perlin8(i*volumeSmth+SEGENV.aux0, SEGENV.aux1+i*volumeSmth); // Get a value from the noise function. I'm using both x and y axis. SEGMENT.setPixelColor(i, SEGMENT.color_from_palette(index, false, PALETTE_SOLID_WRAP, 0)); } SEGENV.aux0=SEGENV.aux0+beatsin8_t(5,0,10); SEGENV.aux1=SEGENV.aux1+beatsin8_t(4,0,10); } // mode_midnoise() static const char _data_FX_MODE_MIDNOISE[] PROGMEM = "Midnoise@Fade rate,Max. length;!,!;!;1v;ix=128,m12=1,si=0"; // Bar, Beatsin ////////////////////// // * NOISEFIRE // ////////////////////// // I am the god of hellfire. . . Volume (only) reactive fire routine. Oh, look how short this is. void mode_noisefire(void) { // Noisefire. By Andrew Tuline. CRGBPalette16 myPal = CRGBPalette16(CHSV(0,255,2), CHSV(0,255,4), CHSV(0,255,8), CHSV(0, 255, 8), // Fire palette definition. Lower value = darker. CHSV(0, 255, 16), CRGB::Red, CRGB::Red, CRGB::Red, CRGB::DarkOrange, CRGB::DarkOrange, CRGB::Orange, CRGB::Orange, CRGB::Yellow, CRGB::Orange, CRGB::Yellow, CRGB::Yellow); um_data_t *um_data = getAudioData(); float volumeSmth = *(float*) um_data->u_data[0]; if (SEGENV.call == 0) SEGMENT.fill(BLACK); for (unsigned i = 0; i < SEGLEN; i++) { unsigned index = perlin8(i*SEGMENT.speed/64,strip.now*SEGMENT.speed/64*SEGLEN/255); // X location is constant, but we move along the Y at the rate of millis(). By Andrew Tuline. index = (255 - i*256/SEGLEN) * index/(256-SEGMENT.intensity); // Now we need to scale index so that it gets blacker as we get close to one of the ends. // This is a simple y=mx+b equation that's been scaled. index/128 is another scaling. SEGMENT.setPixelColor(i, ColorFromPalette(myPal, index, volumeSmth*2, LINEARBLEND)); // Use my own palette. } } // mode_noisefire() static const char _data_FX_MODE_NOISEFIRE[] PROGMEM = "Noisefire@!,!;;;01v;m12=2,si=0"; // Circle, Beatsin /////////////////////// // * Noisemeter // /////////////////////// void mode_noisemeter(void) { // Noisemeter. By Andrew Tuline. um_data_t *um_data = getAudioData(); float volumeSmth = *(float*) um_data->u_data[0]; int volumeRaw = *(int16_t*)um_data->u_data[1]; //uint8_t fadeRate = map(SEGMENT.speed,0,255,224,255); uint8_t fadeRate = map(SEGMENT.speed,0,255,200,254); SEGMENT.fade_out(fadeRate); float tmpSound2 = volumeRaw * 2.0 * (float)SEGMENT.intensity / 255.0; unsigned maxLen = mapf(tmpSound2, 0, 255, 0, SEGLEN); // map to pixels availeable in current segment // Still a bit too sensitive. if (maxLen < 0) maxLen = 0; if (maxLen > SEGLEN) maxLen = SEGLEN; for (unsigned i=0; iu_data[1]; uint8_t secondHand = micros()/(256-SEGMENT.speed)/500+1 % 16; if (SEGENV.aux0 != secondHand) { SEGENV.aux0 = secondHand; uint8_t pixBri = volumeRaw * SEGMENT.intensity / 64; SEGMENT.setPixelColor(SEGLEN/2, color_blend(SEGCOLOR(1), SEGMENT.color_from_palette(strip.now, false, PALETTE_SOLID_WRAP, 0), pixBri)); for (unsigned i = SEGLEN - 1; i > SEGLEN/2; i--) SEGMENT.setPixelColor(i, SEGMENT.getPixelColor(i-1)); //move to the left for (unsigned i = 0; i < SEGLEN/2; i++) SEGMENT.setPixelColor(i, SEGMENT.getPixelColor(i+1)); // move to the right } } // mode_pixelwave() static const char _data_FX_MODE_PIXELWAVE[] PROGMEM = "Pixelwave@!,Sensitivity;!,!;!;1v;ix=64,m12=2,si=0"; // Circle, Beatsin ////////////////////// // * PLASMOID // ////////////////////// typedef struct Plasphase { int16_t thisphase; int16_t thatphase; } plasphase; void mode_plasmoid(void) { // Plasmoid. By Andrew Tuline. // even with 1D effect we have to take logic for 2D segments for allocation as fill_solid() fills whole segment if (!SEGENV.allocateData(sizeof(plasphase))) FX_FALLBACK_STATIC; //allocation failed Plasphase* plasmoip = reinterpret_cast(SEGENV.data); um_data_t *um_data = getAudioData(); float volumeSmth = *(float*) um_data->u_data[0]; SEGMENT.fadeToBlackBy(32); plasmoip->thisphase += beatsin8_t(6,-4,4); // You can change direction and speed individually. plasmoip->thatphase += beatsin8_t(7,-4,4); // Two phase values to make a complex pattern. By Andrew Tuline. for (unsigned i = 0; i < SEGLEN; i++) { // For each of the LED's in the strand, set a brightness based on a wave as follows. // updated, similar to "plasma" effect - softhack007 uint8_t thisbright = cubicwave8(((i*(1 + (3*SEGMENT.speed/32)))+plasmoip->thisphase) & 0xFF)/2; thisbright += cos8_t(((i*(97 +(5*SEGMENT.speed/32)))+plasmoip->thatphase) & 0xFF)/2; // Let's munge the brightness a bit and animate it all with the phases. uint8_t colorIndex=thisbright; if (volumeSmth * SEGMENT.intensity / 64 < thisbright) {thisbright = 0;} SEGMENT.addPixelColor(i, color_blend(SEGCOLOR(1), SEGMENT.color_from_palette(colorIndex, false, PALETTE_SOLID_WRAP, 0), thisbright)); } } // mode_plasmoid() static const char _data_FX_MODE_PLASMOID[] PROGMEM = "Plasmoid@Phase,# of pixels;!,!;!;01v;sx=128,ix=128,m12=0,si=0"; // Pixels, Beatsin ////////////////////// // * PUDDLES // ////////////////////// // Puddles/Puddlepeak By Andrew Tuline. Merged by @dedehai void mode_puddles_base(bool peakdetect) { if (SEGLEN <= 1) FX_FALLBACK_STATIC; unsigned size = 0; uint8_t fadeVal = map(SEGMENT.speed, 0, 255, 224, 254); unsigned pos = hw_random16(SEGLEN); // Set a random starting position. SEGMENT.fade_out(fadeVal); um_data_t *um_data = getAudioData(); int volumeRaw = *(int16_t*)um_data->u_data[1]; uint8_t samplePeak = *(uint8_t*)um_data->u_data[3]; uint8_t *maxVol = (uint8_t*)um_data->u_data[6]; uint8_t *binNum = (uint8_t*)um_data->u_data[7]; float volumeSmth = *(float*) um_data->u_data[0]; if(peakdetect) { // puddles peak *binNum = SEGMENT.custom1; // Select a bin. *maxVol = SEGMENT.custom2 / 2; // Our volume comparator. if (samplePeak == 1) { size = volumeSmth * SEGMENT.intensity /256 /4 + 1; // Determine size of the flash based on the volume. if (pos+size>= SEGLEN) size = SEGLEN - pos; } } else { // puddles if (volumeRaw > 1) { size = volumeRaw * SEGMENT.intensity /256 /8 + 1; // Determine size of the flash based on the volume. if (pos+size >= SEGLEN) size = SEGLEN - pos; } } for (unsigned i=0; i(SEGENV.data); // Used to store a pile of samples because WLED frame rate and WLED sample rate are not synchronized. Frame rate is too low. um_data_t *um_data; if (!UsermodManager::getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) { um_data = simulateSound(SEGMENT.soundSim); } float volumeSmth = *(float*) um_data->u_data[0]; myVals[strip.now%32] = volumeSmth; // filling values semi randomly SEGMENT.fade_out(64+(SEGMENT.speed>>1)); for (int i=0; i u_data[2]; if (SEGENV.call == 0) { SEGMENT.fill(BLACK); SEGENV.aux0 = 0; } int fadeoutDelay = (256 - SEGMENT.speed) / 32; if ((fadeoutDelay <= 1 ) || ((SEGENV.call % fadeoutDelay) == 0)) SEGMENT.fade_out(SEGMENT.speed); SEGENV.step += FRAMETIME; if (SEGENV.step > SPEED_FORMULA_L) { unsigned segLoc = hw_random16(SEGLEN); SEGMENT.setPixelColor(segLoc, color_blend(SEGCOLOR(1), SEGMENT.color_from_palette(2*fftResult[SEGENV.aux0%16]*240/max(1, (int)SEGLEN-1), false, PALETTE_SOLID_WRAP, 0), uint8_t(2*fftResult[SEGENV.aux0%16]))); ++(SEGENV.aux0) %= 16; // make sure it doesn't cross 16 SEGENV.step = 1; SEGMENT.blur(SEGMENT.intensity); // note: blur > 210 results in a alternating pattern, this could be fixed by mapping but some may like it (very old bug) } } // mode_blurz() static const char _data_FX_MODE_BLURZ[] PROGMEM = "Blurz@Fade rate,Blur;!,Color mix;!;1f;m12=0,si=0"; // Pixels, Beatsin ///////////////////////// // ** DJLight // ///////////////////////// void mode_DJLight(void) { // Written by ??? Adapted by Will Tatam. // No need to prevent from executing on single led strips, only mid will be set (mid = 0) const int mid = SEGLEN / 2; um_data_t *um_data = getAudioData(); uint8_t *fftResult = (uint8_t*)um_data->u_data[2]; if (SEGENV.call == 0) { SEGMENT.fill(BLACK); } uint8_t secondHand = micros()/(256-SEGMENT.speed)/500+1 % 64; if (SEGENV.aux0 != secondHand) { // Triggered millis timing. SEGENV.aux0 = secondHand; CRGB color = CRGB(fftResult[15]/2, fftResult[5]/2, fftResult[0]/2); // 16-> 15 as 16 is out of bounds SEGMENT.setPixelColor(mid, color.fadeToBlackBy(map(fftResult[4], 0, 255, 255, 4))); // TODO - Update // if SEGLEN equals 1 these loops won't execute for (int i = SEGLEN - 1; i > mid; i--) SEGMENT.setPixelColor(i, SEGMENT.getPixelColor(i-1)); // move to the left for (int i = 0; i < mid; i++) SEGMENT.setPixelColor(i, SEGMENT.getPixelColor(i+1)); // move to the right } } // mode_DJLight() static const char _data_FX_MODE_DJLIGHT[] PROGMEM = "DJ Light@Speed;;;01f;m12=2,si=0"; // Circle, Beatsin //////////////////// // ** Freqmap // //////////////////// void mode_freqmap(void) { // Map FFT_MajorPeak to SEGLEN. Would be better if a higher framerate. if (SEGLEN <= 1) FX_FALLBACK_STATIC; // Start frequency = 60 Hz and log10(60) = 1.78 // End frequency = MAX_FREQUENCY in Hz and lo10(MAX_FREQUENCY) = MAX_FREQ_LOG10 um_data_t *um_data = getAudioData(); float FFT_MajorPeak = *(float*)um_data->u_data[4]; float my_magnitude = *(float*)um_data->u_data[5] / 4.0f; if (FFT_MajorPeak < 1) FFT_MajorPeak = 1; // log10(0) is "forbidden" (throws exception) if (SEGENV.call == 0) SEGMENT.fill(BLACK); int fadeoutDelay = (256 - SEGMENT.speed) / 32; if ((fadeoutDelay <= 1 ) || ((SEGENV.call % fadeoutDelay) == 0)) SEGMENT.fade_out(SEGMENT.speed); int locn = (log10f((float)FFT_MajorPeak) - 1.78f) * (float)SEGLEN/(MAX_FREQ_LOG10 - 1.78f); // log10 frequency range is from 1.78 to 3.71. Let's scale to SEGLEN. if (locn < 1) locn = 0; // avoid underflow if (locn >= (int)SEGLEN) locn = SEGLEN-1; unsigned pixCol = (log10f(FFT_MajorPeak) - 1.78f) * 255.0f/(MAX_FREQ_LOG10 - 1.78f); // Scale log10 of frequency values to the 255 colour index. if (FFT_MajorPeak < 61.0f) pixCol = 0; // handle underflow uint8_t bright = (uint8_t)my_magnitude; SEGMENT.setPixelColor(locn, color_blend(SEGCOLOR(1), SEGMENT.color_from_palette(SEGMENT.intensity+pixCol, false, PALETTE_SOLID_WRAP, 0), bright)); } // mode_freqmap() static const char _data_FX_MODE_FREQMAP[] PROGMEM = "Freqmap@Fade rate,Starting color;!,!;!;1f;m12=0,si=0"; // Pixels, Beatsin /////////////////////// // ** Freqmatrix // /////////////////////// void mode_freqmatrix(void) { // Freqmatrix. By Andreas Pleschung. // No need to prevent from executing on single led strips, we simply change pixel 0 each time and avoid the shift um_data_t *um_data = getAudioData(); float FFT_MajorPeak = *(float*)um_data->u_data[4]; float volumeSmth = *(float*)um_data->u_data[0]; if (SEGENV.call == 0) { SEGMENT.fill(BLACK); } uint8_t secondHand = micros()/(256-SEGMENT.speed)/500 % 16; if(SEGENV.aux0 != secondHand) { SEGENV.aux0 = secondHand; uint8_t sensitivity = map(SEGMENT.custom3, 0, 31, 1, 10); // reduced resolution slider int pixVal = (volumeSmth * SEGMENT.intensity * sensitivity) / 256.0f; if (pixVal > 255) pixVal = 255; float intensity = map(pixVal, 0, 255, 0, 100) / 100.0f; // make a brightness from the last avg CRGB color = CRGB::Black; if (FFT_MajorPeak > MAX_FREQUENCY) FFT_MajorPeak = 1; // MajorPeak holds the freq. value which is most abundant in the last sample. // With our sampling rate of 10240Hz we have a usable freq range from roughly 80Hz to 10240/2 Hz // we will treat everything with less than 65Hz as 0 if (FFT_MajorPeak < 80) { color = CRGB::Black; } else { int upperLimit = 80 + 42 * SEGMENT.custom2; int lowerLimit = 80 + 3 * SEGMENT.custom1; uint8_t i = lowerLimit!=upperLimit ? map(FFT_MajorPeak, lowerLimit, upperLimit, 0, 255) : FFT_MajorPeak; // may under/overflow - so we enforce uint8_t unsigned b = 255 * intensity; if (b > 255) b = 255; color = CHSV(i, 240, (uint8_t)b); // implicit conversion to RGB supplied by FastLED } // shift the pixels one pixel up SEGMENT.setPixelColor(0, color); // if SEGLEN equals 1 this loop won't execute for (int i = SEGLEN - 1; i > 0; i--) SEGMENT.setPixelColor(i, SEGMENT.getPixelColor(i-1)); //move to the left } } // mode_freqmatrix() static const char _data_FX_MODE_FREQMATRIX[] PROGMEM = "Freqmatrix@Speed,Sound effect,Low bin,High bin,Sensitivity;;;01f;m12=3,si=0"; // Corner, Beatsin ////////////////////// // ** Freqpixels // ////////////////////// // Start frequency = 60 Hz and log10(60) = 1.78 // End frequency = 5120 Hz and lo10(5120) = 3.71 // SEGMENT.speed select faderate // SEGMENT.intensity select colour index void mode_freqpixels(void) { // Freqpixel. By Andrew Tuline. um_data_t *um_data = getAudioData(); float FFT_MajorPeak = *(float*)um_data->u_data[4]; float my_magnitude = *(float*)um_data->u_data[5] / 16.0f; if (FFT_MajorPeak < 1) FFT_MajorPeak = 1.0f; // log10(0) is "forbidden" (throws exception) // this code translates to speed * (2 - speed/255) which is a) speed*2 or b) speed (when speed is 255) // and since fade_out() can only take 0-255 it will behave incorrectly when speed > 127 //uint16_t fadeRate = 2*SEGMENT.speed - SEGMENT.speed*SEGMENT.speed/255; // Get to 255 as quick as you can. unsigned fadeRate = SEGMENT.speed*SEGMENT.speed; // Get to 255 as quick as you can. fadeRate = map(fadeRate, 0, 65535, 1, 255); int fadeoutDelay = (256 - SEGMENT.speed) / 64; if ((fadeoutDelay <= 1 ) || ((SEGENV.call % fadeoutDelay) == 0)) SEGMENT.fade_out(fadeRate); uint8_t pixCol = (log10f(FFT_MajorPeak) - 1.78f) * 255.0f/(MAX_FREQ_LOG10 - 1.78f); // Scale log10 of frequency values to the 255 colour index. if (FFT_MajorPeak < 61.0f) pixCol = 0; // handle underflow for (int i=0; i < SEGMENT.intensity/32+1; i++) { unsigned locn = hw_random16(0,SEGLEN); SEGMENT.setPixelColor(locn, color_blend(SEGCOLOR(1), SEGMENT.color_from_palette(SEGMENT.intensity+pixCol, false, PALETTE_SOLID_WRAP, 0), (uint8_t)my_magnitude)); } } // mode_freqpixels() static const char _data_FX_MODE_FREQPIXELS[] PROGMEM = "Freqpixels@Fade rate,Starting color and # of pixels;!,!,;!;1f;m12=0,si=0"; // Pixels, Beatsin ////////////////////// // ** Freqwave // ////////////////////// // Assign a color to the central (starting pixels) based on the predominant frequencies and the volume. The color is being determined by mapping the MajorPeak from the FFT // and then mapping this to the HSV color circle. Currently we are sampling at 10240 Hz, so the highest frequency we can look at is 5120Hz. // // SEGMENT.custom1: the lower cut off point for the FFT. (many, most time the lowest values have very little information since they are FFT conversion artifacts. Suggested value is close to but above 0 // SEGMENT.custom2: The high cut off point. This depends on your sound profile. Most music looks good when this slider is between 50% and 100%. // SEGMENT.custom3: "preamp" for the audio signal for audio10. // // I suggest that for this effect you turn the brightness to 95%-100% but again it depends on your soundprofile you find yourself in. // Instead of using colorpalettes, This effect works on the HSV color circle with red being the lowest frequency // // As a compromise between speed and accuracy we are currently sampling with 10240Hz, from which we can then determine with a 512bin FFT our max frequency is 5120Hz. // Depending on the music stream you have you might find it useful to change the frequency mapping. void mode_freqwave(void) { // Freqwave. By Andreas Pleschung. // As before, this effect can also work on single pixels, we just lose the shifting effect um_data_t *um_data = getAudioData(); float FFT_MajorPeak = *(float*)um_data->u_data[4]; float volumeSmth = *(float*)um_data->u_data[0]; if (SEGENV.call == 0) { SEGMENT.fill(BLACK); } uint8_t secondHand = micros()/(256-SEGMENT.speed)/500 % 16; if(SEGENV.aux0 != secondHand) { SEGENV.aux0 = secondHand; float sensitivity = mapf(SEGMENT.custom3, 1, 31, 1, 10); // reduced resolution slider float pixVal = min(255.0f, volumeSmth * (float)SEGMENT.intensity / 256.0f * sensitivity); float intensity = mapf(pixVal, 0.0f, 255.0f, 0.0f, 100.0f) / 100.0f; // make a brightness from the last avg CRGB color = 0; if (FFT_MajorPeak > MAX_FREQUENCY) FFT_MajorPeak = 1.0f; // MajorPeak holds the freq. value which is most abundant in the last sample. // With our sampling rate of 10240Hz we have a usable freq range from roughly 80Hz to 10240/2 Hz // we will treat everything with less than 65Hz as 0 if (FFT_MajorPeak < 80) { color = CRGB::Black; } else { int upperLimit = 80 + 42 * SEGMENT.custom2; int lowerLimit = 80 + 3 * SEGMENT.custom1; uint8_t i = lowerLimit!=upperLimit ? map(FFT_MajorPeak, lowerLimit, upperLimit, 0, 255) : FFT_MajorPeak; // may under/overflow - so we enforce uint8_t unsigned b = min(255.0f, 255.0f * intensity); color = CHSV(i, 240, (uint8_t)b); // implicit conversion to RGB supplied by FastLED } SEGMENT.setPixelColor(SEGLEN/2, color); // shift the pixels one pixel outwards // if SEGLEN equals 1 these loops won't execute for (unsigned i = SEGLEN - 1; i > SEGLEN/2; i--) SEGMENT.setPixelColor(i, SEGMENT.getPixelColor(i-1)); //move to the left for (unsigned i = 0; i < SEGLEN/2; i++) SEGMENT.setPixelColor(i, SEGMENT.getPixelColor(i+1)); // move to the right } } // mode_freqwave() static const char _data_FX_MODE_FREQWAVE[] PROGMEM = "Freqwave@Speed,Sound effect,Low bin,High bin,Pre-amp;;;01f;m12=2,si=0"; // Circle, Beatsin ////////////////////// // ** Noisemove // ////////////////////// void mode_noisemove(void) { // Noisemove. By: Andrew Tuline um_data_t *um_data = getAudioData(); uint8_t *fftResult = (uint8_t*)um_data->u_data[2]; int fadeoutDelay = (256 - SEGMENT.speed) / 96; if ((fadeoutDelay <= 1 ) || ((SEGENV.call % fadeoutDelay) == 0)) SEGMENT.fadeToBlackBy(4+ SEGMENT.speed/4); uint8_t numBins = map(SEGMENT.intensity,0,255,0,16); // Map slider to fftResult bins. for (int i=0; iu_data[4]; float my_magnitude = *(float*) um_data->u_data[5] / 16.0f; SEGMENT.fadeToBlackBy(16); // Just in case something doesn't get faded. float frTemp = FFT_MajorPeak; uint8_t octCount = 0; // Octave counter. uint8_t volTemp = 0; volTemp = 32.0f + my_magnitude * 1.5f; // brightness = volume (overflows are handled in next lines) if (my_magnitude < 48) volTemp = 0; // We need to squelch out the background noise. if (my_magnitude > 144) volTemp = 255; // everything above this is full brightness while ( frTemp > 249 ) { octCount++; // This should go up to 5. frTemp = frTemp/2; } frTemp -= 132.0f; // This should give us a base musical note of C3 frTemp = fabsf(frTemp * 2.1f); // Fudge factors to compress octave range starting at 0 and going to 255; unsigned i = map(beatsin8_t(8+octCount*4, 0, 255, 0, octCount*8), 0, 255, 0, SEGLEN-1); i = constrain(i, 0U, SEGLEN-1U); SEGMENT.addPixelColor(i, color_blend(SEGCOLOR(1), SEGMENT.color_from_palette((uint8_t)frTemp, false, PALETTE_SOLID_WRAP, 0), volTemp)); } // mode_rocktaves() static const char _data_FX_MODE_ROCKTAVES[] PROGMEM = "Rocktaves@;!,!;!;01f;m12=1,si=0"; // Bar, Beatsin /////////////////////// // ** Waterfall // /////////////////////// // Combines peak detection with FFT_MajorPeak and FFT_Magnitude. void mode_waterfall(void) { // Waterfall. By: Andrew Tuline // effect can work on single pixels, we just lose the shifting effect unsigned dataSize = sizeof(uint32_t) * SEGLEN; if (!SEGENV.allocateData(dataSize)) FX_FALLBACK_STATIC; //allocation failed uint32_t* pixels = reinterpret_cast(SEGENV.data); um_data_t *um_data = getAudioData(); uint8_t samplePeak = *(uint8_t*)um_data->u_data[3]; float FFT_MajorPeak = *(float*) um_data->u_data[4]; uint8_t *maxVol = (uint8_t*)um_data->u_data[6]; uint8_t *binNum = (uint8_t*)um_data->u_data[7]; float my_magnitude = *(float*) um_data->u_data[5] / 8.0f; if (FFT_MajorPeak < 1) FFT_MajorPeak = 1; // log10(0) is "forbidden" (throws exception) if (SEGENV.call == 0) { for (unsigned i = 0; i < SEGLEN; i++) pixels[i] = BLACK; // may not be needed as resetIfRequired() clears buffer SEGENV.aux0 = 255; SEGMENT.custom1 = *binNum; SEGMENT.custom2 = *maxVol * 2; } *binNum = SEGMENT.custom1; // Select a bin. *maxVol = SEGMENT.custom2 / 2; // Our volume comparator. uint8_t secondHand = micros() / (256-SEGMENT.speed)/500 + 1 % 16; if (SEGENV.aux0 != secondHand) { // Triggered millis timing. SEGENV.aux0 = secondHand; //uint8_t pixCol = (log10f((float)FFT_MajorPeak) - 2.26f) * 177; // 10Khz sampling - log10 frequency range is from 2.26 (182hz) to 3.7 (5012hz). Let's scale accordingly. uint8_t pixCol = (log10f(FFT_MajorPeak) - 2.26f) * 150; // 22Khz sampling - log10 frequency range is from 2.26 (182hz) to 3.967 (9260hz). Let's scale accordingly. if (FFT_MajorPeak < 182.0f) pixCol = 0; // handle underflow unsigned k = SEGLEN-1; if (samplePeak) { pixels[k] = (uint32_t)CRGB(CHSV(92,92,92)); } else { pixels[k] = color_blend(SEGCOLOR(1), SEGMENT.color_from_palette(pixCol+SEGMENT.intensity, false, PALETTE_SOLID_WRAP, 0), (uint8_t)my_magnitude); } SEGMENT.setPixelColor(k, pixels[k]); // loop will not execute if SEGLEN equals 1 for (unsigned i = 0; i < k; i++) { pixels[i] = pixels[i+1]; // shift left SEGMENT.setPixelColor(i, pixels[i]); } } } // mode_waterfall() static const char _data_FX_MODE_WATERFALL[] PROGMEM = "Waterfall@!,Adjust color,Select bin,Volume (min);!,!;!;01f;c2=0,m12=2,si=0"; // Circles, Beatsin #ifndef WLED_DISABLE_2D ///////////////////////// // ** 2D GEQ // ///////////////////////// void mode_2DGEQ(void) { // By Will Tatam. Code reduction by Ewoud Wijma. if (!strip.isMatrix || !SEGMENT.is2D()) FX_FALLBACK_STATIC; // not a 2D set-up const int NUM_BANDS = map(SEGMENT.custom1, 0, 255, 1, 16); const int CENTER_BIN = map(SEGMENT.custom3, 0, 31, 0, 15); const int cols = SEG_W; const int rows = SEG_H; if (!SEGENV.allocateData(cols*sizeof(uint16_t))) FX_FALLBACK_STATIC; //allocation failed uint16_t *previousBarHeight = reinterpret_cast(SEGENV.data); //array of previous bar heights per frequency band um_data_t *um_data = getAudioData(); uint8_t *fftResult = (uint8_t*)um_data->u_data[2]; if (SEGENV.call == 0) for (int i=0; i= (256U - SEGMENT.intensity)) { SEGENV.step = strip.now; rippleTime = true; } int fadeoutDelay = (256 - SEGMENT.speed) / 64; if ((fadeoutDelay <= 1 ) || ((SEGENV.call % fadeoutDelay) == 0)) SEGMENT.fadeToBlackBy(SEGMENT.speed); for (int x=0; x < cols; x++) { int band = map(x, 0, cols, 0, NUM_BANDS); if (NUM_BANDS < 16) { int startBin = constrain(CENTER_BIN - NUM_BANDS/2, 0, 15 - NUM_BANDS + 1); if(NUM_BANDS <= 1) band = CENTER_BIN; // map() does not work for single band else band = map(band, 0, NUM_BANDS - 1, startBin, startBin + NUM_BANDS - 1); } band = constrain(band, 0, 15); unsigned colorIndex = band * 17; int barHeight = map(fftResult[band], 0, 255, 0, rows); // do not subtract -1 from rows here if (barHeight > previousBarHeight[x]) previousBarHeight[x] = barHeight; //drive the peak up uint32_t ledColor = BLACK; for (int y=0; y < barHeight; y++) { if (SEGMENT.check1) //color_vertical / color bars toggle colorIndex = map(y, 0, rows-1, 0, 255); ledColor = SEGMENT.color_from_palette(colorIndex, false, PALETTE_SOLID_WRAP, 0); SEGMENT.setPixelColorXY(x, rows-1 - y, ledColor); } if (previousBarHeight[x] > 0) SEGMENT.setPixelColorXY(x, rows - previousBarHeight[x], (SEGCOLOR(2) != BLACK) ? SEGCOLOR(2) : ledColor); if (rippleTime && previousBarHeight[x]>0) previousBarHeight[x]--; //delay/ripple effect } } // mode_2DGEQ() static const char _data_FX_MODE_2DGEQ[] PROGMEM = "GEQ@Fade speed,Ripple decay,# of bands,,Bin,Color bars;!,,Peaks;!;2f;c1=255,c2=64,pal=11,si=0,c3=0"; ///////////////////////// // ** 2D Funky plank // ///////////////////////// void mode_2DFunkyPlank(void) { // Written by ??? Adapted by Will Tatam. if (!strip.isMatrix || !SEGMENT.is2D()) FX_FALLBACK_STATIC; // not a 2D set-up const int cols = SEG_W; const int rows = SEG_H; int NUMB_BANDS = map(SEGMENT.custom1, 0, 255, 1, 16); int barWidth = (cols / NUMB_BANDS); int bandInc = 1; if (barWidth == 0) { // Matrix narrower than fft bands barWidth = 1; bandInc = (NUMB_BANDS / cols); } um_data_t *um_data = getAudioData(); uint8_t *fftResult = (uint8_t*)um_data->u_data[2]; if (SEGENV.call == 0) { SEGMENT.fill(BLACK); } uint8_t secondHand = micros()/(256-SEGMENT.speed)/500+1 % 64; if (SEGENV.aux0 != secondHand) { // Triggered millis timing. SEGENV.aux0 = secondHand; // display values of int b = 0; for (int band = 0; band < NUMB_BANDS; band += bandInc, b++) { int hue = fftResult[band % 16]; int v = map(fftResult[band % 16], 0, 255, 10, 255); for (int w = 0; w < barWidth; w++) { int xpos = (barWidth * b) + w; SEGMENT.setPixelColorXY(xpos, 0, CHSV(hue, 255, v)); } } // Update the display: for (int i = (rows - 1); i > 0; i--) { for (int j = (cols - 1); j >= 0; j--) { SEGMENT.setPixelColorXY(j, i, SEGMENT.getPixelColorXY(j, i-1)); } } } } // mode_2DFunkyPlank static const char _data_FX_MODE_2DFUNKYPLANK[] PROGMEM = "Funky Plank@Scroll speed,,# of bands;;;2f;si=0"; // Beatsin ///////////////////////// // 2D Akemi // ///////////////////////// static uint8_t akemi[] PROGMEM = { 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0,0,2,2,2,2,2,2,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,2,2,3,3,3,3,3,3,2,2,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,2,3,3,0,0,0,0,0,0,3,3,2,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,2,3,0,0,0,6,5,5,4,0,0,0,3,2,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,2,3,0,0,6,6,5,5,5,5,4,4,0,0,3,2,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,2,3,0,6,5,5,5,5,5,5,5,5,4,0,3,2,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,2,3,0,6,5,5,5,5,5,5,5,5,5,5,4,0,3,2,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,3,2,0,6,5,5,5,5,5,5,5,5,5,5,4,0,2,3,0,0,0,0,0,0,0, 0,0,0,0,0,0,3,2,3,6,5,5,7,7,5,5,5,5,7,7,5,5,4,3,2,3,0,0,0,0,0,0, 0,0,0,0,0,2,3,1,3,6,5,1,7,7,7,5,5,1,7,7,7,5,4,3,1,3,2,0,0,0,0,0, 0,0,0,0,0,8,3,1,3,6,5,1,7,7,7,5,5,1,7,7,7,5,4,3,1,3,8,0,0,0,0,0, 0,0,0,0,0,8,3,1,3,6,5,5,1,1,5,5,5,5,1,1,5,5,4,3,1,3,8,0,0,0,0,0, 0,0,0,0,0,2,3,1,3,6,5,5,5,5,5,5,5,5,5,5,5,5,4,3,1,3,2,0,0,0,0,0, 0,0,0,0,0,0,3,2,3,6,5,5,5,5,5,5,5,5,5,5,5,5,4,3,2,3,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,6,5,5,5,5,5,7,7,5,5,5,5,5,4,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,6,5,5,5,5,5,5,5,5,5,5,5,5,4,0,0,0,0,0,0,0,0,0, 1,0,0,0,0,0,0,0,0,6,5,5,5,5,5,5,5,5,5,5,5,5,4,0,0,0,0,0,0,0,0,2, 0,2,2,2,0,0,0,0,0,6,5,5,5,5,5,5,5,5,5,5,5,5,4,0,0,0,0,0,2,2,2,0, 0,0,0,3,2,0,0,0,6,5,4,4,4,4,4,4,4,4,4,4,4,4,4,4,0,0,0,2,2,0,0,0, 0,0,0,3,2,0,0,0,6,5,5,5,5,5,5,5,5,5,5,5,5,5,5,4,0,0,0,2,3,0,0,0, 0,0,0,0,3,2,0,0,0,0,3,3,0,3,3,0,0,3,3,0,3,3,0,0,0,0,2,2,0,0,0,0, 0,0,0,0,3,2,0,0,0,0,3,2,0,3,2,0,0,3,2,0,3,2,0,0,0,0,2,3,0,0,0,0, 0,0,0,0,0,3,2,0,0,3,2,0,0,3,2,0,0,3,2,0,0,3,2,0,0,2,3,0,0,0,0,0, 0,0,0,0,0,3,2,2,2,2,0,0,0,3,2,0,0,3,2,0,0,0,3,2,2,2,3,0,0,0,0,0, 0,0,0,0,0,0,3,3,3,0,0,0,0,3,2,0,0,3,2,0,0,0,0,3,3,3,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0,0,3,2,0,0,3,2,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0,0,3,2,0,0,3,2,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0,0,3,2,0,0,3,2,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0,0,3,2,0,0,3,2,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0,0,3,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0,0,3,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 }; void mode_2DAkemi(void) { if (!strip.isMatrix || !SEGMENT.is2D()) FX_FALLBACK_STATIC; // not a 2D set-up const int cols = SEG_W; const int rows = SEG_H; unsigned counter = (strip.now * ((SEGMENT.speed >> 2) +2)) & 0xFFFF; counter = counter >> 8; const float lightFactor = 0.15f; const float normalFactor = 0.4f; um_data_t *um_data; if (!UsermodManager::getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) { um_data = simulateSound(SEGMENT.soundSim); } uint8_t *fftResult = (uint8_t*)um_data->u_data[2]; float base = fftResult[0]/255.0f; //draw and color Akemi for (int y=0; y < rows; y++) for (int x=0; x < cols; x++) { CRGB color; CRGB soundColor = CRGB::Orange; CRGB faceColor = CRGB(SEGMENT.color_wheel(counter)); CRGB armsAndLegsColor = CRGB(SEGCOLOR(1) > 0 ? SEGCOLOR(1) : 0xFFE0A0); //default warmish white 0xABA8FF; //0xFF52e5;// uint8_t ak = pgm_read_byte_near(akemi + ((y * 32)/rows) * 32 + (x * 32)/cols); // akemi[(y * 32)/rows][(x * 32)/cols] switch (ak) { case 3: armsAndLegsColor.r *= lightFactor; armsAndLegsColor.g *= lightFactor; armsAndLegsColor.b *= lightFactor; color = armsAndLegsColor; break; //light arms and legs 0x9B9B9B case 2: armsAndLegsColor.r *= normalFactor; armsAndLegsColor.g *= normalFactor; armsAndLegsColor.b *= normalFactor; color = armsAndLegsColor; break; //normal arms and legs 0x888888 case 1: color = armsAndLegsColor; break; //dark arms and legs 0x686868 case 6: faceColor.r *= lightFactor; faceColor.g *= lightFactor; faceColor.b *= lightFactor; color=faceColor; break; //light face 0x31AAFF case 5: faceColor.r *= normalFactor; faceColor.g *= normalFactor; faceColor.b *= normalFactor; color=faceColor; break; //normal face 0x0094FF case 4: color = faceColor; break; //dark face 0x007DC6 case 7: color = SEGCOLOR(2) > 0 ? SEGCOLOR(2) : 0xFFFFFF; break; //eyes and mouth default white case 8: if (base > 0.4) {soundColor.r *= base; soundColor.g *= base; soundColor.b *= base; color=soundColor;} else color = armsAndLegsColor; break; default: color = BLACK; break; } if (SEGMENT.intensity > 128 && fftResult && fftResult[0] > 128) { //dance if base is high SEGMENT.setPixelColorXY(x, 0, BLACK); SEGMENT.setPixelColorXY(x, y+1, color); } else SEGMENT.setPixelColorXY(x, y, color); } //add geq left and right if (um_data && fftResult) { int xMax = cols/8; for (int x=0; x < xMax; x++) { unsigned band = map(x, 0, max(xMax,4), 0, 15); // map 0..cols/8 to 16 GEQ bands band = constrain(band, 0, 15); int barHeight = map(fftResult[band], 0, 255, 0, 17*rows/32); uint32_t color = SEGMENT.color_from_palette((band * 35), false, PALETTE_SOLID_WRAP, 0); for (int y=0; y < barHeight; y++) { SEGMENT.setPixelColorXY(x, rows/2-y, color); SEGMENT.setPixelColorXY(cols-1-x, rows/2-y, color); } } } } // mode_2DAkemi static const char _data_FX_MODE_2DAKEMI[] PROGMEM = "Akemi@Color speed,Dance;Head palette,Arms & Legs,Eyes & Mouth;Face palette;2f;si=0"; //beatsin // Distortion waves - ldirko // https://editor.soulmatelights.com/gallery/1089-distorsion-waves // adapted for WLED by @blazoncek, improvements by @dedehai void mode_2Ddistortionwaves() { if (!strip.isMatrix || !SEGMENT.is2D()) FX_FALLBACK_STATIC; // not a 2D set-up const int cols = SEG_W; const int rows = SEG_H; uint8_t speed = SEGMENT.speed/32; uint8_t scale = SEGMENT.intensity/32; if(SEGMENT.check2) scale += 192 / (cols+rows); // zoom out some more. note: not changing scale slider for backwards compatibility unsigned a = strip.now/32; unsigned a2 = a/2; unsigned a3 = a/3; unsigned colsScaled = cols * scale; unsigned rowsScaled = rows * scale; unsigned cx = beatsin16_t(10-speed,0,colsScaled); unsigned cy = beatsin16_t(12-speed,0,rowsScaled); unsigned cx1 = beatsin16_t(13-speed,0,colsScaled); unsigned cy1 = beatsin16_t(15-speed,0,rowsScaled); unsigned cx2 = beatsin16_t(17-speed,0,colsScaled); unsigned cy2 = beatsin16_t(14-speed,0,rowsScaled); byte rdistort, gdistort, bdistort; unsigned xoffs = 0; for (int x = 0; x < cols; x++) { xoffs += scale; unsigned yoffs = 0; for (int y = 0; y < rows; y++) { yoffs += scale; if(SEGMENT.check3) { // alternate mode from original code rdistort = cos8_t (((x+y)*8+a2)&255)>>1; gdistort = cos8_t (((x+y)*8+a3+32)&255)>>1; bdistort = cos8_t (((x+y)*8+a+64)&255)>>1; } else { rdistort = cos8_t((cos8_t(((x<<3)+a )&255)+cos8_t(((y<<3)-a2)&255)+a3 )&255)>>1; gdistort = cos8_t((cos8_t(((x<<3)-a2)&255)+cos8_t(((y<<3)+a3)&255)+a+32 )&255)>>1; bdistort = cos8_t((cos8_t(((x<<3)+a3)&255)+cos8_t(((y<<3)-a) &255)+a2+64)&255)>>1; } byte valueR = rdistort + ((a- ( ((xoffs - cx) * (xoffs - cx) + (yoffs - cy) * (yoffs - cy))>>7 ))<<1); byte valueG = gdistort + ((a2-( ((xoffs - cx1) * (xoffs - cx1) + (yoffs - cy1) * (yoffs - cy1))>>7 ))<<1); byte valueB = bdistort + ((a3-( ((xoffs - cx2) * (xoffs - cx2) + (yoffs - cy2) * (yoffs - cy2))>>7 ))<<1); valueR = cos8_t(valueR); valueG = cos8_t(valueG); valueB = cos8_t(valueB); if(SEGMENT.palette == 0) { // use RGB values (original color mode) SEGMENT.setPixelColorXY(x, y, RGBW32(valueR, valueG, valueB, 0)); } else { // use palette uint8_t brightness = (valueR + valueG + valueB) / 3; if(SEGMENT.check1) { // map brightness to palette index SEGMENT.setPixelColorXY(x, y, ColorFromPalette(SEGPALETTE, brightness, 255, LINEARBLEND_NOWRAP)); } else { // color mapping: calculate hue from pixel color, map it to palette index CHSV hsvclr = rgb2hsv_approximate(CRGB(valueR>>2, valueG>>2, valueB>>2)); // scale colors down to not saturate for better hue extraction SEGMENT.setPixelColorXY(x, y, ColorFromPalette(SEGPALETTE, hsvclr.h, brightness)); } } } } // palette mode and not filling: smear-blur to cover up palette wrapping artefacts if(!SEGMENT.check1 && SEGMENT.palette) SEGMENT.blur(200, true); } static const char _data_FX_MODE_2DDISTORTIONWAVES[] PROGMEM = "Distortion Waves@!,Scale,,,,Fill,Zoom,Alt;;!;2;pal=0"; //Soap //@Stepko //Idea from https://www.youtube.com/watch?v=DiHBgITrZck&ab_channel=StefanPetrick // adapted for WLED by @blazoncek, optimization by @dedehai static void soapPixels(bool isRow, uint8_t *noise3d, CRGB *pixels) { const int cols = SEG_W; const int rows = SEG_H; const auto XY = [&](int x, int y) { return x + y * cols; }; const auto abs = [](int x) { return x<0 ? -x : x; }; const int tRC = isRow ? rows : cols; // transpose if isRow const int tCR = isRow ? cols : rows; // transpose if isRow const int amplitude = max(1, (tCR - 8) >> 3) * (1 + (SEGMENT.custom1 >> 5)); const int shift = 0; //(128 - SEGMENT.custom2)*2; CRGB ledsbuff[tCR]; for (int i = 0; i < tRC; i++) { int amount = ((int)noise3d[isRow ? i*cols : i] - 128) * amplitude + shift; // use first row/column: XY(0,i)/XY(i,0) int delta = abs(amount) >> 8; int fraction = abs(amount) & 255; for (int j = 0; j < tCR; j++) { int zD, zF; if (amount < 0) { zD = j - delta; zF = zD - 1; } else { zD = j + delta; zF = zD + 1; } int yA = abs(zD)%tCR; int yB = abs(zF)%tCR; int xA = i; int xB = i; if (isRow) { std::swap(xA,yA); std::swap(xB,yB); } const int indxA = XY(xA,yA); const int indxB = XY(xB,yB); CRGB PixelA; CRGB PixelB; if ((zD >= 0) && (zD < tCR)) PixelA = pixels[indxA]; else PixelA = ColorFromPalette(SEGPALETTE, ~noise3d[indxA]*3); if ((zF >= 0) && (zF < tCR)) PixelB = pixels[indxB]; else PixelB = ColorFromPalette(SEGPALETTE, ~noise3d[indxB]*3); ledsbuff[j] = (PixelA.nscale8(ease8InOutApprox(255 - fraction))) + (PixelB.nscale8(ease8InOutApprox(fraction))); } for (int j = 0; j < tCR; j++) { CRGB c = ledsbuff[j]; if (isRow) std::swap(j,i); SEGMENT.setPixelColorXY(i, j, pixels[XY(i,j)] = c); if (isRow) std::swap(j,i); } } } void mode_2Dsoap() { if (!strip.isMatrix || !SEGMENT.is2D()) FX_FALLBACK_STATIC; // not a 2D set-up const int cols = SEG_W; const int rows = SEG_H; const auto XY = [&](int x, int y) { return x + y * cols; }; const size_t segSize = SEGMENT.width() * SEGMENT.height(); // prevent reallocation if mirrored or grouped const size_t dataSize = segSize * (sizeof(uint8_t) + sizeof(CRGB)); // pixels and noise if (!SEGENV.allocateData(dataSize + sizeof(uint32_t)*3)) FX_FALLBACK_STATIC; //allocation failed uint8_t *noise3d = reinterpret_cast(SEGENV.data); CRGB *pixels = reinterpret_cast(SEGENV.data + segSize * sizeof(uint8_t)); uint32_t *noisecoord = reinterpret_cast(SEGENV.data + dataSize); // x, y, z coordinates const uint32_t scale32_x = 160000U/cols; const uint32_t scale32_y = 160000U/rows; const uint32_t mov = MIN(cols,rows)*(SEGMENT.speed+2)/2; const uint8_t smoothness = MIN(250,SEGMENT.intensity); // limit as >250 produces very little changes if (SEGENV.call == 0) for (int i = 0; i < 3; i++) noisecoord[i] = hw_random(); // init else for (int i = 0; i < 3; i++) noisecoord[i] += mov; for (int i = 0; i < cols; i++) { int32_t ioffset = scale32_x * (i - cols / 2); for (int j = 0; j < rows; j++) { int32_t joffset = scale32_y * (j - rows / 2); uint8_t data = perlin16(noisecoord[0] + ioffset, noisecoord[1] + joffset, noisecoord[2]) >> 8; noise3d[XY(i,j)] = scale8(noise3d[XY(i,j)], smoothness) + scale8(data, 255 - smoothness); } } // init also if dimensions changed if (SEGENV.call == 0 || SEGMENT.aux0 != cols || SEGMENT.aux1 != rows) { SEGMENT.aux0 = cols; SEGMENT.aux1 = rows; for (int i = 0; i < cols; i++) { for (int j = 0; j < rows; j++) { SEGMENT.setPixelColorXY(i, j, ColorFromPalette(SEGPALETTE,~noise3d[XY(i,j)]*3)); } } } soapPixels(true, noise3d, pixels); // rows soapPixels(false, noise3d, pixels); // cols } static const char _data_FX_MODE_2DSOAP[] PROGMEM = "Soap@!,Smoothness,Density;;!;2;pal=11"; //Idea from https://www.youtube.com/watch?v=HsA-6KIbgto&ab_channel=GreatScott%21 //Octopus (https://editor.soulmatelights.com/gallery/671-octopus) //Stepko and Sutaburosu // adapted for WLED by @blazoncek void mode_2Doctopus() { if (!strip.isMatrix || !SEGMENT.is2D()) FX_FALLBACK_STATIC; // not a 2D set-up const int cols = SEG_W; const int rows = SEG_H; const auto XY = [&](int x, int y) { return (x%cols) + (y%rows) * cols; }; const uint8_t mapp = 180 / MAX(cols,rows); typedef struct { uint8_t angle; uint8_t radius; } map_t; const size_t dataSize = SEGMENT.width() * SEGMENT.height() * sizeof(map_t); // prevent reallocation if mirrored or grouped if (!SEGENV.allocateData(dataSize + 2)) FX_FALLBACK_STATIC; //allocation failed map_t *rMap = reinterpret_cast(SEGENV.data); uint8_t *offsX = reinterpret_cast(SEGENV.data + dataSize); uint8_t *offsY = reinterpret_cast(SEGENV.data + dataSize + 1); // re-init if SEGMENT dimensions or offset changed if (SEGENV.call == 0 || SEGENV.aux0 != cols || SEGENV.aux1 != rows || SEGMENT.custom1 != *offsX || SEGMENT.custom2 != *offsY) { SEGENV.step = 0; // t SEGENV.aux0 = cols; SEGENV.aux1 = rows; *offsX = SEGMENT.custom1; *offsY = SEGMENT.custom2; const int C_X = (cols / 2) + ((SEGMENT.custom1 - 128)*cols)/255; const int C_Y = (rows / 2) + ((SEGMENT.custom2 - 128)*rows)/255; for (int x = 0; x < cols; x++) { for (int y = 0; y < rows; y++) { int dx = (x - C_X); int dy = (y - C_Y); rMap[XY(x, y)].angle = int(40.7436f * atan2_t(dy, dx)); // avoid 128*atan2()/PI rMap[XY(x, y)].radius = sqrtf(dx * dx + dy * dy) * mapp; //thanks Sutaburosu } } } SEGENV.step += SEGMENT.speed / 32 + 1; // 1-4 range for (int x = 0; x < cols; x++) { for (int y = 0; y < rows; y++) { byte angle = rMap[XY(x,y)].angle; byte radius = rMap[XY(x,y)].radius; //CRGB c = CHSV(SEGENV.step / 2 - radius, 255, sin8_t(sin8_t((angle * 4 - radius) / 4 + SEGENV.step) + radius - SEGENV.step * 2 + angle * (SEGMENT.custom3/3+1))); unsigned intensity = sin8_t(sin8_t((angle * 4 - radius) / 4 + SEGENV.step/2) + radius - SEGENV.step + angle * (SEGMENT.custom3/4+1)); intensity = map((intensity*intensity) & 0xFFFF, 0, 65535, 0, 255); // add a bit of non-linearity for cleaner display SEGMENT.setPixelColorXY(x, y, ColorFromPalette(SEGPALETTE, SEGENV.step / 2 - radius, intensity)); } } } static const char _data_FX_MODE_2DOCTOPUS[] PROGMEM = "Octopus@!,,Offset X,Offset Y,Legs,fasttan;;!;2;"; //Waving Cell //@Stepko (https://editor.soulmatelights.com/gallery/1704-wavingcells) // adapted for WLED by @blazoncek, improvements by @dedehai void mode_2Dwavingcell() { if (!strip.isMatrix || !SEGMENT.is2D()) FX_FALLBACK_STATIC; // not a 2D set-up const int cols = SEG_W; const int rows = SEG_H; uint32_t t = (strip.now*(SEGMENT.speed + 1))>>3; uint32_t aX = SEGMENT.custom1/16 + 9; uint32_t aY = SEGMENT.custom2/16 + 1; uint32_t aZ = SEGMENT.custom3 + 1; for (int x = 0; x < cols; x++) { for (int y = 0; y < rows; y++) { uint32_t wave = sin8_t((x * aX) + sin8_t((((y<<8) + t) * aY)>>8)) + cos8_t(y * aZ); // bit shifts to increase temporal resolution uint8_t colorIndex = wave + (t>>(8-(SEGMENT.check2*3))); SEGMENT.setPixelColorXY(x, y, ColorFromPalette(SEGPALETTE, colorIndex)); } } SEGMENT.blur(SEGMENT.intensity); } static const char _data_FX_MODE_2DWAVINGCELL[] PROGMEM = "Waving Cell@!,Blur,Amplitude 1,Amplitude 2,Amplitude 3,,Flow;;!;2;ix=0"; #ifndef WLED_DISABLE_PARTICLESYSTEM2D /* Particle System Vortex Particles sprayed from center with a rotating spray Uses palette for particle color by DedeHai (Damian Schneider) */ #define NUMBEROFSOURCES 8 void mode_particlevortex(void) { if (SEGLEN == 1) FX_FALLBACK_STATIC; ParticleSystem2D *PartSys = nullptr; uint32_t i, j; if (SEGMENT.call == 0) { // initialization if (!initParticleSystem2D(PartSys, NUMBEROFSOURCES)) FX_FALLBACK_STATIC; // allocation failed #ifdef ESP8266 PartSys->setMotionBlur(180); #else PartSys->setMotionBlur(130); #endif for (i = 0; i < min(PartSys->numSources, (uint32_t)NUMBEROFSOURCES); i++) { PartSys->sources[i].source.x = (PartSys->maxX + 1) >> 1; // center PartSys->sources[i].source.y = (PartSys->maxY + 1) >> 1; // center PartSys->sources[i].maxLife = 900; PartSys->sources[i].minLife = 800; } PartSys->setKillOutOfBounds(true); } else PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS if (PartSys == nullptr) FX_FALLBACK_STATIC; // something went wrong, no data! PartSys->updateSystem(); // update system properties (dimensions and data pointers) uint32_t spraycount = min(PartSys->numSources, (uint32_t)(1 + (SEGMENT.custom1 >> 5))); // number of sprays to display, 1-8 #ifdef ESP8266 for (i = 1; i < 4; i++) { // need static particles in the center to reduce blinking (would be black every other frame without this hack), just set them there fixed int partindex = (int)PartSys->usedParticles - (int)i; if (partindex >= 0) { PartSys->particles[partindex].x = (PartSys->maxX + 1) >> 1; // center PartSys->particles[partindex].y = (PartSys->maxY + 1) >> 1; // center PartSys->particles[partindex].sat = 230; PartSys->particles[partindex].ttl = 256; //keep alive } } #endif if (SEGMENT.check1) PartSys->setSmearBlur(90); // enable smear blur else PartSys->setSmearBlur(0); // disable smear blur // update colors of the sprays for (i = 0; i < spraycount; i++) { uint32_t coloroffset = 0xFF / spraycount; PartSys->sources[i].source.hue = coloroffset * i; } // set rotation direction and speed // can use direction flag to determine current direction bool direction = SEGMENT.check2; //no automatic direction change, set it to flag int32_t currentspeed = (int32_t)SEGENV.step; // make a signed integer out of step if (SEGMENT.custom2 > 0) { // automatic direction change enabled uint32_t changeinterval = 1040 - ((uint32_t)SEGMENT.custom2 << 2); direction = SEGENV.aux1 & 0x01; //set direction according to flag if (SEGMENT.check3) // random interval changeinterval = 20 + changeinterval + hw_random16(changeinterval); if (SEGMENT.call % changeinterval == 0) { //flip direction on next frame SEGENV.aux1 |= 0x02; // set the update flag (for random interval update) if (direction) SEGENV.aux1 &= ~0x01; // clear the direction flag else SEGENV.aux1 |= 0x01; // set the direction flag } } int32_t targetspeed = (direction ? 1 : -1) * (SEGMENT.speed << 3); int32_t speeddiff = targetspeed - currentspeed; int32_t speedincrement = speeddiff / 50; if (speedincrement == 0) { //if speeddiff is not zero, make the increment at least 1 so it reaches target speed if (speeddiff < 0) speedincrement = -1; else if (speeddiff > 0) speedincrement = 1; } currentspeed += speedincrement; SEGENV.aux0 += currentspeed; SEGENV.step = (uint32_t)currentspeed; //save it back uint16_t angleoffset = 0xFFFF / spraycount; // angle offset for an even distribution uint32_t skip = PS_P_HALFRADIUS / (SEGMENT.intensity + 1) + 1; // intensity is emit speed, emit less on low speeds if (SEGMENT.call % skip == 0) { j = hw_random16(spraycount); // start with random spray so all get a chance to emit a particle if maximum number of particles alive is reached. for (i = 0; i < spraycount; i++) { // emit one particle per spray (if available) PartSys->sources[j].var = (SEGMENT.custom3 >> 1); //update speed variation #ifdef ESP8266 if (SEGMENT.call & 0x01) // every other frame, do not emit to save particles #endif PartSys->angleEmit(PartSys->sources[j], SEGENV.aux0 + angleoffset * j, (SEGMENT.intensity >> 2)+1); j = (j + 1) % spraycount; } } PartSys->update(); //update all particles and render to frame } #undef NUMBEROFSOURCES static const char _data_FX_MODE_PARTICLEVORTEX[] PROGMEM = "PS Vortex@Rotation Speed,Particle Speed,Arms,Flip,Nozzle,Smear,Direction,Random Flip;;!;2;pal=27,c1=200,c2=0,c3=0"; /* Particle Fireworks Rockets shoot up and explode in a random color, sometimes in a defined pattern by DedeHai (Damian Schneider) */ #define NUMBEROFSOURCES 8 void mode_particlefireworks(void) { ParticleSystem2D *PartSys = nullptr; uint32_t numRockets; if (SEGMENT.call == 0) { // initialization if (!initParticleSystem2D(PartSys, NUMBEROFSOURCES)) FX_FALLBACK_STATIC; // allocation failed PartSys->setKillOutOfBounds(true); // out of bounds particles dont return (except on top, taken care of by gravity setting) PartSys->setWallHardness(120); // ground bounce is fixed numRockets = min(PartSys->numSources, (uint32_t)NUMBEROFSOURCES); for (uint32_t j = 0; j < numRockets; j++) { PartSys->sources[j].source.ttl = 500 * j; // first rocket starts immediately, others follow soon PartSys->sources[j].source.vy = -1; // at negative speed, no particles are emitted and if rocket dies, it will be relaunched } } else PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS if (PartSys == nullptr) FX_FALLBACK_STATIC; // something went wrong, no data! PartSys->updateSystem(); // update system properties (dimensions and data pointers) numRockets = map(SEGMENT.speed, 0 , 255, 4, min(PartSys->numSources, (uint32_t)NUMBEROFSOURCES)); PartSys->setWrapX(SEGMENT.check1); PartSys->setBounceY(SEGMENT.check2); PartSys->setGravity(map(SEGMENT.custom3, 0, 31, SEGMENT.check2 ? 1 : 0, 10)); // if bounded, set gravity to minimum of 1 or they will bounce at top PartSys->setMotionBlur(map(SEGMENT.custom2, 0, 255, 0, 245)); // anable motion blur // update the rockets, set the speed state for (uint32_t j = 0; j < numRockets; j++) { PartSys->applyGravity(PartSys->sources[j].source); PartSys->particleMoveUpdate(PartSys->sources[j].source, PartSys->sources[j].sourceFlags); if (PartSys->sources[j].source.ttl == 0) { if (PartSys->sources[j].source.vy > 0) { // rocket has died and is moving up. stop it so it will explode (is handled in the code below) PartSys->sources[j].source.vy = 0; } else if (PartSys->sources[j].source.vy < 0) { // rocket is exploded and time is up (ttl=0 and negative speed), relaunch it PartSys->sources[j].source.y = PS_P_RADIUS; // start from bottom PartSys->sources[j].source.x = (PartSys->maxX >> 2) + hw_random(PartSys->maxX >> 1); // centered half PartSys->sources[j].source.vy = (SEGMENT.custom3) + hw_random16(SEGMENT.custom1 >> 3) + 5; // rocket speed TODO: need to adjust for segment height PartSys->sources[j].source.vx = hw_random16(7) - 3; // not perfectly straight up PartSys->sources[j].source.sat = 30; // low saturation -> exhaust is off-white PartSys->sources[j].source.ttl = hw_random16(SEGMENT.custom1) + (SEGMENT.custom1 >> 1); // set fuse time PartSys->sources[j].maxLife = 40; // exhaust particle life PartSys->sources[j].minLife = 10; PartSys->sources[j].vx = 0; // emitting speed PartSys->sources[j].vy = -5; // emitting speed PartSys->sources[j].var = 4; // speed variation around vx,vy (+/- var) } } } // check each rocket's state and emit particles according to its state: moving up = emit exhaust, at top = explode; falling down = standby time uint32_t emitparticles, frequency, baseangle, hueincrement; // number of particles to emit for each rocket's state // variables for circular explosions [[maybe_unused]] int32_t speed, currentspeed, speedvariation, percircle; int32_t counter = 0; [[maybe_unused]] uint16_t angle; [[maybe_unused]] unsigned angleincrement; bool circularexplosion = false; // emit particles for each rocket for (uint32_t j = 0; j < numRockets; j++) { // determine rocket state by its speed: if (PartSys->sources[j].source.vy > 0) { // moving up, emit exhaust emitparticles = 1; } else if (PartSys->sources[j].source.vy < 0) { // falling down, standby time emitparticles = 0; } else { // speed is zero, explode! PartSys->sources[j].source.hue = hw_random16(); // random color PartSys->sources[j].source.sat = hw_random16(55) + 200; PartSys->sources[j].maxLife = 200; PartSys->sources[j].minLife = 100; PartSys->sources[j].source.ttl = hw_random16((2000 - ((uint32_t)SEGMENT.speed << 2))) + 550 - (SEGMENT.speed << 1); // standby time til next launch PartSys->sources[j].var = ((SEGMENT.intensity >> 4) + 5); // speed variation around vx,vy (+/- var) PartSys->sources[j].source.vy = -1; // set speed negative so it will emit no more particles after this explosion until relaunch #ifdef ESP8266 emitparticles = hw_random16(SEGMENT.intensity >> 3) + (SEGMENT.intensity >> 3) + 5; // defines the size of the explosion #else emitparticles = hw_random16(SEGMENT.intensity >> 2) + (SEGMENT.intensity >> 2) + 5; // defines the size of the explosion #endif if (random16() & 1) { // 50% chance for circular explosion circularexplosion = true; speed = 2 + hw_random16(3) + ((SEGMENT.intensity >> 6)); currentspeed = speed; angleincrement = 2730 + hw_random16(5461); // minimum 15° + random(30°) angle = hw_random16(); // random start angle baseangle = angle; // save base angle for modulation percircle = 0xFFFF / angleincrement + 1; // number of particles to make complete circles hueincrement = hw_random16() & 127; // &127 is equivalent to %128 int circles = 1 + hw_random16(3) + ((SEGMENT.intensity >> 6)); frequency = hw_random16() & 127; // modulation frequency (= "waves per circle"), x.4 fixed point emitparticles = percircle * circles; PartSys->sources[j].var = angle & 1; // 0 or 1 variation, angle is random } } uint32_t i; for (i = 0; i < emitparticles; i++) { if (circularexplosion) { int32_t sineMod = 0xEFFF + sin16_t((uint16_t)(((angle * frequency) >> 4) + baseangle)); // shifted to positive values currentspeed = (speed/2 + ((sineMod * speed) >> 16)) >> 1; // sine modulation on speed based on emit angle PartSys->angleEmit(PartSys->sources[j], angle, currentspeed); // note: compiler warnings can be ignored, variables are set just above counter++; if (counter > percircle) { // full circle completed, increase speed counter = 0; speed += 3 + ((SEGMENT.intensity >> 6)); // increase speed to form a second wave PartSys->sources[j].source.hue += hueincrement; // new color for next circle PartSys->sources[j].source.sat = 100 + hw_random16(156); } angle += angleincrement; // set angle for next particle } else { // random explosion or exhaust PartSys->sprayEmit(PartSys->sources[j]); if ((j % 3) == 0) { PartSys->sources[j].source.hue = hw_random16(); // random color for each particle (this is also true for exhaust, but that is white anyways) } } } if (i == 0) // no particles emitted, this rocket is falling PartSys->sources[j].source.y = 1000; // reset position so gravity wont pull it to the ground and bounce it (vy MUST stay negative until relaunch) circularexplosion = false; // reset for next rocket } if (SEGMENT.check3) { // fast speed, move particles twice for (uint32_t i = 0; i < PartSys->usedParticles; i++) { PartSys->particleMoveUpdate(PartSys->particles[i], PartSys->particleFlags[i], nullptr, nullptr); } } PartSys->update(); // update and render } #undef NUMBEROFSOURCES static const char _data_FX_MODE_PARTICLEFIREWORKS[] PROGMEM = "PS Fireworks@Launches,Explosion Size,Fuse,Blur,Gravity,Cylinder,Ground,Fast;;!;2;pal=11,ix=50,c1=40,c2=0,c3=12"; /* Particle Volcano Particles are sprayed from below, spray moves back and forth if option is set Uses palette for particle color by DedeHai (Damian Schneider) */ #define NUMBEROFSOURCES 1 void mode_particlevolcano(void) { ParticleSystem2D *PartSys = nullptr; PSsettings2D volcanosettings; volcanosettings.asByte = 0b00000100; // PS settings for volcano movement: bounceX is enabled uint8_t numSprays; // note: so far only one tested but more is possible uint32_t i = 0; if (SEGMENT.call == 0) { // initialization if (!initParticleSystem2D(PartSys, NUMBEROFSOURCES)) // init, no additional data needed FX_FALLBACK_STATIC; // allocation failed or not 2D PartSys->setBounceY(true); PartSys->setGravity(); // enable with default gforce PartSys->setKillOutOfBounds(true); // out of bounds particles dont return (except on top, taken care of by gravity setting) PartSys->setMotionBlur(230); // anable motion blur numSprays = min(PartSys->numSources, (uint32_t)NUMBEROFSOURCES); // number of sprays for (i = 0; i < numSprays; i++) { PartSys->sources[i].source.hue = hw_random16(); PartSys->sources[i].source.x = PartSys->maxX / (numSprays + 1) * (i + 1); // distribute evenly PartSys->sources[i].maxLife = 300; // lifetime in frames PartSys->sources[i].minLife = 250; PartSys->sources[i].sourceFlags.collide = true; // seeded particles will collide (if enabled) PartSys->sources[i].sourceFlags.perpetual = true; // source never dies } } else PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS if (PartSys == nullptr) FX_FALLBACK_STATIC; // something went wrong, no data! numSprays = min(PartSys->numSources, (uint32_t)NUMBEROFSOURCES); // number of volcanoes // change source emitting color from time to time, emit one particle per spray if (SEGMENT.call % (11 - (SEGMENT.intensity / 25)) == 0) { // every nth frame, cycle color and emit particles (and update the sources) for (i = 0; i < numSprays; i++) { PartSys->sources[i].source.y = PS_P_RADIUS + 5; // reset to just above the lower edge that is allowed for bouncing particles, if zero, particles already 'bounce' at start and loose speed. PartSys->sources[i].source.vy = 0; //reset speed (so no extra particlesettin is required to keep the source 'afloat') PartSys->sources[i].source.hue++; // = hw_random16(); //change hue of spray source (note: random does not look good) PartSys->sources[i].source.vx = PartSys->sources[i].source.vx > 0 ? (SEGMENT.custom1 >> 2) : -(SEGMENT.custom1 >> 2); // set moving speed but keep the direction given by PS PartSys->sources[i].vy = SEGMENT.speed >> 2; // emitting speed (upwards) PartSys->sources[i].vx = 0; PartSys->sources[i].var = SEGMENT.custom3 >> 1; // emiting variation = nozzle size (custom 3 goes from 0-31) PartSys->sprayEmit(PartSys->sources[i]); PartSys->setWallHardness(255); // full hardness for source bounce PartSys->particleMoveUpdate(PartSys->sources[i].source, PartSys->sources[i].sourceFlags, &volcanosettings); //move the source } } // Particle System settings PartSys->updateSystem(); // update system properties (dimensions and data pointers) PartSys->setColorByAge(SEGMENT.check1); PartSys->setBounceX(SEGMENT.check2); PartSys->setWallHardness(SEGMENT.custom2); if (SEGMENT.check3) // collisions enabled PartSys->enableParticleCollisions(true, SEGMENT.custom2); // enable collisions and set particle collision hardness else PartSys->enableParticleCollisions(false); PartSys->update(); // update and render } #undef NUMBEROFSOURCES static const char _data_FX_MODE_PARTICLEVOLCANO[] PROGMEM = "PS Volcano@Speed,Intensity,Move,Bounce,Spread,AgeColor,Walls,Collide;;!;2;pal=35,sx=100,ix=190,c1=0,c2=160,c3=6,o1=1"; /* Particle Fire realistic fire effect using particles. heat based and using perlin-noise for wind by DedeHai (Damian Schneider) */ void mode_particlefire(void) { ParticleSystem2D *PartSys = nullptr; uint32_t i; // index variable uint32_t numFlames; // number of flames: depends on fire width. for a fire width of 16 pixels, about 25-30 flames give good results if (SEGMENT.call == 0) { // initialization if (!initParticleSystem2D(PartSys, SEGMENT.vWidth(), 4)) //maximum number of source (PS may limit based on segment size); need 4 additional bytes for time keeping (uint32_t lastcall) FX_FALLBACK_STATIC; // allocation failed or not 2D SEGENV.aux0 = hw_random16(); // aux0 is wind position (index) in the perlin noise } else PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS if (PartSys == nullptr) FX_FALLBACK_STATIC; // something went wrong, no data! PartSys->updateSystem(); // update system properties (dimensions and data pointers) PartSys->setWrapX(SEGMENT.check2); PartSys->setMotionBlur(SEGMENT.check1 * 170); // anable/disable motion blur PartSys->setSmearBlur(!SEGMENT.check1 * 60); // enable smear blur if motion blur is not enabled uint32_t firespeed = max((uint8_t)100, SEGMENT.speed); //limit speed to 100 minimum, reduce frame rate to make it slower (slower speeds than 100 do not look nice) if (SEGMENT.speed < 100) { //slow, limit FPS uint32_t *lastcall = reinterpret_cast(PartSys->PSdataEnd); uint32_t period = strip.now - *lastcall; if (period < (uint32_t)map(SEGMENT.speed, 0, 99, 50, 10)) { // limit to 90FPS - 20FPS SEGMENT.call--; //skipping a frame, decrement the counter (on call0, this is never executed as lastcall is 0, so its fine to not check if >0) return; //do not update this frame } *lastcall = strip.now; } uint32_t spread = (PartSys->maxX >> 5) * (SEGMENT.custom3 + 1); //fire around segment center (in subpixel points) numFlames = min((uint32_t)PartSys->numSources, (4 + ((spread / PS_P_RADIUS) << 1))); // number of flames used depends on spread with, good value is (fire width in pixel) * 2 uint32_t percycle = (numFlames * 2) / 3; // maximum number of particles emitted per cycle (TODO: for ESP826 maybe use flames/2) // update the flame sprays: for (i = 0; i < numFlames; i++) { if (SEGMENT.call & 1 && PartSys->sources[i].source.ttl > 0) { // every second frame PartSys->sources[i].source.ttl--; } else { // flame source is dead: initialize new flame: set properties of source PartSys->sources[i].source.x = (PartSys->maxX >> 1) - (spread >> 1) + hw_random(spread); // change flame position: distribute randomly on chosen width PartSys->sources[i].source.y = -(PS_P_RADIUS << 2); // set the source below the frame PartSys->sources[i].source.ttl = 20 + hw_random16((SEGMENT.custom1 * SEGMENT.custom1) >> 8) / (1 + (firespeed >> 5)); //'hotness' of fire, faster flames reduce the effect or flame height will scale too much with speed PartSys->sources[i].maxLife = hw_random16(SEGMENT.vHeight() >> 1) + 16; // defines flame height together with the vy speed, vy speed*maxlife/PS_P_RADIUS is the average flame height PartSys->sources[i].minLife = PartSys->sources[i].maxLife >> 1; PartSys->sources[i].vx = hw_random16(5) - 2; // emitting speed (sideways) PartSys->sources[i].vy = (SEGMENT.vHeight() >> 1) + (firespeed >> 4) + (SEGMENT.custom1 >> 4); // emitting speed (upwards) PartSys->sources[i].var = 2 + hw_random16(2 + (firespeed >> 4)); // speed variation around vx,vy (+/- var) } } if (SEGMENT.call % 3 == 0) { // update noise position and add wind SEGENV.aux0++; // position in the perlin noise matrix for wind generation if (SEGMENT.call % 10 == 0) SEGENV.aux1++; // move in noise y direction so noise does not repeat as often // add wind force to all particles int8_t windspeed = ((int16_t)(perlin8(SEGENV.aux0, SEGENV.aux1) - 127) * SEGMENT.custom2) >> 7; PartSys->applyForce(windspeed, 0); } SEGENV.step++; if (SEGMENT.check3) { //add turbulance (parameters and algorithm found by experimentation) if (SEGMENT.call % map(firespeed, 0, 255, 4, 15) == 0) { for (i = 0; i < PartSys->usedParticles; i++) { if (PartSys->particles[i].y < PartSys->maxY / 4) { // do not apply turbulance everywhere -> bottom quarter seems a good balance int32_t curl = ((int32_t)perlin8(PartSys->particles[i].x, PartSys->particles[i].y, SEGENV.step << 4) - 127); PartSys->particles[i].vx += (curl * (firespeed + 10)) >> 9; } } } } // emit faster sparks at first flame position, amount and speed mostly dependends on intensity if(hw_random8() < 10 + (SEGMENT.intensity >> 2)) { for (i = 0; i < PartSys->usedParticles; i++) { if (PartSys->particles[i].ttl == 0) { // find a dead particle PartSys->particles[i].ttl = hw_random16(SEGMENT.vHeight()) + 30; PartSys->particles[i].x = PartSys->sources[0].source.x; PartSys->particles[i].y = PartSys->sources[0].source.y; PartSys->particles[i].vx = PartSys->sources[0].source.vx; PartSys->particles[i].vy = (SEGMENT.vHeight() >> 1) + (firespeed >> 4) + ((30 + (SEGMENT.intensity >> 1) + SEGMENT.custom1) >> 4); // emitting speed (upwards) break; // emit only one particle } } } uint8_t j = hw_random16(); // start with a random flame (so each flame gets the chance to emit a particle if available particles is smaller than number of flames) for (i = 0; i < percycle; i++) { j = (j + 1) % numFlames; PartSys->flameEmit(PartSys->sources[j]); } PartSys->updateFire(SEGMENT.intensity); // update and render the fire } static const char _data_FX_MODE_PARTICLEFIRE[] PROGMEM = "PS Fire@Speed,Intensity,Flame Height,Wind,Spread,Smooth,Cylinder,Turbulence;;!;2;pal=35,sx=110,c1=110,c2=50,c3=31,o1=1"; /* PS Ballpit: particles falling down, user can enable these three options: X-wraparound, side bounce, ground bounce sliders control falling speed, intensity (number of particles spawned), inter-particle collision hardness (0 means no particle collisions) and render saturation this is quite versatile, can be made to look like rain or snow or confetti etc. Uses palette for particle color by DedeHai (Damian Schneider) */ void mode_particlepit(void) { ParticleSystem2D *PartSys = nullptr; if (SEGMENT.call == 0) { // initialization if (!initParticleSystem2D(PartSys, 0, 0, true, false)) // init FX_FALLBACK_STATIC; // allocation failed or not 2D PartSys->setKillOutOfBounds(true); PartSys->setGravity(); // enable with default gravity PartSys->setUsedParticles(170); // use 75% of available particles } else PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS if (PartSys == nullptr) FX_FALLBACK_STATIC; // something went wrong, no data! PartSys->updateSystem(); // update system properties (dimensions and data pointers) PartSys->setWrapX(SEGMENT.check1); PartSys->setBounceX(SEGMENT.check2); PartSys->setBounceY(SEGMENT.check3); PartSys->setWallHardness(min(SEGMENT.custom2, (uint8_t)150)); // limit to 100 min (if collisions are disabled, still want bouncy) if (SEGMENT.custom2 > 0) PartSys->enableParticleCollisions(true, SEGMENT.custom2); // enable collisions and set particle collision hardness else PartSys->enableParticleCollisions(false); uint32_t i; if (SEGMENT.call % (128 - (SEGMENT.intensity >> 1)) == 0 && SEGMENT.intensity > 0) { // every nth frame emit particles, stop emitting if set to zero for (i = 0; i < PartSys->usedParticles; i++) { // emit particles if (PartSys->particles[i].ttl == 0) { // find a dead particle // emit particle at random position over the top of the matrix (random16 is not random enough) PartSys->particles[i].ttl = 1500 - (SEGMENT.speed << 2) + hw_random16(500); // if speed is higher, make them die sooner PartSys->particles[i].x = hw_random(PartSys->maxX); //random(PartSys->maxX >> 1) + (PartSys->maxX >> 2); PartSys->particles[i].y = (PartSys->maxY << 1); // particles appear somewhere above the matrix, maximum is double the height PartSys->particles[i].vx = (int16_t)hw_random16(SEGMENT.speed >> 1) - (SEGMENT.speed >> 2); // side speed is +/- PartSys->particles[i].vy = map(SEGMENT.speed, 0, 255, -5, -100); // downward speed PartSys->particles[i].hue = hw_random16(); // set random color PartSys->particleFlags[i].collide = true; // enable collision for particle PartSys->particles[i].sat = ((SEGMENT.custom3) << 3) + 7; // set particle size if (SEGMENT.custom1 == 255) { PartSys->perParticleSize = true; PartSys->advPartProps[i].size = hw_random16(SEGMENT.custom1); // set each particle to random size } else { PartSys->setParticleSize(SEGMENT.custom1); // set global size PartSys->advPartProps[i].size = SEGMENT.custom1; // also set individual size for consistency } break; // emit only one particle per round } } } uint32_t frictioncoefficient = 1 + SEGMENT.check1; //need more friction if wrapX is set, see below note if (SEGMENT.speed < 50) // for low speeds, apply more friction frictioncoefficient = 50 - SEGMENT.speed; if (SEGMENT.call % 6 == 0)// (3 + max(3, (SEGMENT.speed >> 2))) == 0) // note: if friction is too low, hard particles uncontrollably 'wander' left and right if wrapX is enabled PartSys->applyFriction(frictioncoefficient); PartSys->update(); // update and render } static const char _data_FX_MODE_PARTICLEPIT[] PROGMEM = "PS Ballpit@Speed,Intensity,Size,Hardness,Saturation,Cylinder,Walls,Ground;;!;2;pal=11,sx=100,ix=220,c1=70,c2=180,c3=31,o3=1"; /* Particle Waterfall Uses palette for particle color, spray source at top emitting particles, many config options by DedeHai (Damian Schneider) */ void mode_particlewaterfall(void) { ParticleSystem2D *PartSys = nullptr; uint8_t numSprays; uint32_t i = 0; if (SEGMENT.call == 0) { // initialization if (!initParticleSystem2D(PartSys, 12)) // init, request 12 sources, no additional data needed FX_FALLBACK_STATIC; // allocation failed or not 2D PartSys->setGravity(); // enable with default gforce PartSys->setKillOutOfBounds(true); // out of bounds particles dont return (except on top, taken care of by gravity setting) PartSys->setMotionBlur(190); // anable motion blur PartSys->setSmearBlur(30); // enable 2D blurring (smearing) for (i = 0; i < PartSys->numSources; i++) { PartSys->sources[i].source.hue = i*90; PartSys->sources[i].sourceFlags.collide = true; // seeded particles will collide #ifdef ESP8266 PartSys->sources[i].maxLife = 250; // lifetime in frames (ESP8266 has less particles, make them short lived to keep the water flowing) PartSys->sources[i].minLife = 100; #else PartSys->sources[i].maxLife = 400; // lifetime in frames PartSys->sources[i].minLife = 150; #endif } } else PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS if (PartSys == nullptr) FX_FALLBACK_STATIC; // something went wrong, no data! // Particle System settings PartSys->updateSystem(); // update system properties (dimensions and data pointers) PartSys->setWrapX(SEGMENT.check1); // cylinder PartSys->setBounceX(SEGMENT.check2); // walls PartSys->setBounceY(SEGMENT.check3); // ground PartSys->setWallHardness(SEGMENT.custom2); numSprays = min((int32_t)PartSys->numSources, max(PartSys->maxXpixel / 6, (int32_t)2)); // number of sprays depends on segment width if (SEGMENT.custom2 > 0) // collisions enabled PartSys->enableParticleCollisions(true, SEGMENT.custom2); // enable collisions and set particle collision hardness else { PartSys->enableParticleCollisions(false); PartSys->setWallHardness(120); // set hardness (for ground bounce) to fixed value if not using collisions } for (i = 0; i < numSprays; i++) { PartSys->sources[i].source.hue += 1 + hw_random16(SEGMENT.custom1>>1); // change hue of spray source } if (SEGMENT.call % (12 - (SEGMENT.intensity >> 5)) == 0 && SEGMENT.intensity > 0) { // every nth frame, emit particles, do not emit if intensity is zero for (i = 0; i < numSprays; i++) { PartSys->sources[i].vy = -SEGMENT.speed >> 3; // emitting speed, down //PartSys->sources[i].source.x = map(SEGMENT.custom3, 0, 31, 0, (PartSys->maxXpixel - numSprays * 2) * PS_P_RADIUS) + i * PS_P_RADIUS * 2; // emitter position PartSys->sources[i].source.x = map(SEGMENT.custom3, 0, 31, 0, (PartSys->maxXpixel - numSprays) * PS_P_RADIUS) + i * PS_P_RADIUS * 2; // emitter position PartSys->sources[i].source.y = PartSys->maxY + (PS_P_RADIUS * ((i<<2) + 4)); // source y position, few pixels above the top to increase spreading before entering the matrix PartSys->sources[i].var = (SEGMENT.custom1 >> 3); // emiting variation 0-32 PartSys->sprayEmit(PartSys->sources[i]); } } if (SEGMENT.call % 20 == 0) PartSys->applyFriction(1); // add just a tiny amount of friction to help smooth things PartSys->update(); // update and render } static const char _data_FX_MODE_PARTICLEWATERFALL[] PROGMEM = "PS Waterfall@Speed,Intensity,Variation,Collide,Position,Cylinder,Walls,Ground;;!;2;pal=9,sx=15,ix=200,c1=32,c2=160,o3=1"; /* Particle Box, applies gravity to particles in either a random direction or random but only downwards (sloshing) Uses palette for particle color by DedeHai (Damian Schneider) */ void mode_particlebox(void) { ParticleSystem2D *PartSys = nullptr; uint32_t i; if (SEGMENT.call == 0) { // initialization if (!initParticleSystem2D(PartSys, 1, 0, true)) // init FX_FALLBACK_STATIC; // allocation failed or not 2D PartSys->setBounceX(true); PartSys->setBounceY(true); SEGENV.aux0 = hw_random16(); // position in perlin noise } else PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS if (PartSys == nullptr) FX_FALLBACK_STATIC; // something went wrong, no data! PartSys->updateSystem(); // update system properties (dimensions and data pointers) PartSys->setWallHardness(min(SEGMENT.custom2, (uint8_t)200)); // wall hardness is 200 or more PartSys->enableParticleCollisions(true, max(2, (int)SEGMENT.custom2)); // enable collisions and set particle collision hardness int maxParticleSize = min(((SEGMENT.vWidth() * SEGMENT.vHeight()) >> 2), 255U); // max particle size based on matrix size unsigned currentParticleSize = map(SEGMENT.custom3, 0, 31, 0, maxParticleSize); PartSys->setUsedParticles(map(SEGMENT.intensity, 0, 255, 2, 153) / (1 + (currentParticleSize >> 4))); // 1% - 60%, reduce if using larger size if (SEGMENT.custom3 < 31) PartSys->setParticleSize(currentParticleSize); // set global size if not max (resets perParticleSize) else PartSys->perParticleSize = true; // per particle size, uses advPartProps.size (randomized below) // add in new particles if amount has changed for (i = 0; i < PartSys->usedParticles; i++) { if (PartSys->particles[i].ttl < 260) { // initialize dead particles PartSys->particles[i].ttl = 260; // full brigthness PartSys->particles[i].x = hw_random16(PartSys->maxX); PartSys->particles[i].y = hw_random16(PartSys->maxY); PartSys->particles[i].hue = hw_random8(); // make it colorful PartSys->particleFlags[i].perpetual = true; // never die PartSys->particleFlags[i].collide = true; // all particles colllide PartSys->advPartProps[i].size = hw_random8(maxParticleSize); // random size, used only if size is set to max (SEGMENT.custom3=31) break; // only spawn one particle per frame for less chaotic transitions } } if (SEGMENT.call % (((255 - SEGMENT.speed) >> 6) + 1) == 0 && SEGMENT.speed > 0) { // how often the force is applied depends on speed setting int32_t xgravity; int32_t ygravity; int32_t increment = (SEGMENT.speed >> 6) + 1; if (SEGMENT.check2) { // washing machine int speed = tristate_square8(strip.now >> 7, 90, 15) / ((400 - SEGMENT.speed) >> 3); SEGENV.aux0 += speed; if (speed == 0) SEGENV.aux0 = 190; //down (= 270°) } else SEGENV.aux0 -= increment; if (SEGMENT.check1) { // random, use perlin noise xgravity = ((int16_t)perlin8(SEGENV.aux0) - 127); ygravity = ((int16_t)perlin8(SEGENV.aux0 + 10000) - 127); // scale the gravity force xgravity = (xgravity * SEGMENT.custom1) / 128; ygravity = (ygravity * SEGMENT.custom1) / 128; } else { // go in a circle xgravity = ((int32_t)(SEGMENT.custom1) * cos16_t(SEGENV.aux0 << 8)) / 0xFFFF; ygravity = ((int32_t)(SEGMENT.custom1) * sin16_t(SEGENV.aux0 << 8)) / 0xFFFF; } if (SEGMENT.check3) { // sloshing, y force is always downwards if (ygravity > 0) ygravity = -ygravity; } PartSys->applyForce(xgravity, ygravity); } if ((SEGMENT.call & 0x0F) == 0) // every 16th frame PartSys->applyFriction(1); PartSys->update(); // update and render } static const char _data_FX_MODE_PARTICLEBOX[] PROGMEM = "PS Box@!,Particles,Tilt,Hardness,Size,Random,Washing Machine,Sloshing;;!;2;pal=53,ix=50,c3=1,o1=1"; /* Fuzzy Noise: Perlin noise 'gravity' mapping as in particles on 'noise hills' viewed from above calculates slope gradient at the particle positions and applies 'downhill' force, resulting in a fuzzy perlin noise display by DedeHai (Damian Schneider) */ void mode_particleperlin(void) { ParticleSystem2D *PartSys = nullptr; uint32_t i; if (SEGMENT.call == 0) { // initialization if (!initParticleSystem2D(PartSys, 1, 0, true)) // init with 1 source and advanced properties FX_FALLBACK_STATIC; // allocation failed or not 2D PartSys->setKillOutOfBounds(true); // should never happen, but lets make sure there are no stray particles PartSys->setMotionBlur(230); // anable motion blur PartSys->setBounceY(true); SEGENV.aux0 = rand(); } else PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS if (PartSys == nullptr) FX_FALLBACK_STATIC; // something went wrong, no data! PartSys->updateSystem(); // update system properties (dimensions and data pointers) PartSys->setWrapX(SEGMENT.check1); PartSys->setBounceX(!SEGMENT.check1); PartSys->setWallHardness(SEGMENT.custom1); // wall hardness PartSys->enableParticleCollisions(SEGMENT.check3, SEGMENT.custom1); // enable collisions and set particle collision hardness PartSys->setUsedParticles(map(SEGMENT.intensity, 0, 255, 25, 128)); // min is 10%, max is 50% PartSys->setSmearBlur(SEGMENT.check2 * 15); // enable 2D blurring (smearing) // apply 'gravity' from a 2D perlin noise map SEGENV.aux0 += 1 + (SEGMENT.speed >> 5); // noise z-position // update position in noise for (i = 0; i < PartSys->usedParticles; i++) { if (PartSys->particles[i].ttl == 0) { // revive dead particles (do not keep them alive forever, they can clump up, need to reseed) PartSys->particles[i].ttl = hw_random16(500) + 200; PartSys->particles[i].x = hw_random(PartSys->maxX); PartSys->particles[i].y = hw_random(PartSys->maxY); PartSys->particleFlags[i].collide = true; // particle colllides } uint32_t scale = 16 - ((31 - SEGMENT.custom3) >> 1); uint16_t xnoise = PartSys->particles[i].x / scale; // position in perlin noise, scaled by slider uint16_t ynoise = PartSys->particles[i].y / scale; int16_t baseheight = perlin8(xnoise, ynoise, SEGENV.aux0); // noise value at particle position PartSys->particles[i].hue = baseheight; // color particles to perlin noise value if (SEGMENT.call % 8 == 0) { // do not apply the force every frame, is too chaotic int8_t xslope = (baseheight + (int16_t)perlin8(xnoise - 10, ynoise, SEGENV.aux0)); int8_t yslope = (baseheight + (int16_t)perlin8(xnoise, ynoise - 10, SEGENV.aux0)); PartSys->applyForce(i, xslope, yslope); } } if (SEGMENT.call % (16 - (SEGMENT.custom2 >> 4)) == 0) PartSys->applyFriction(2); PartSys->update(); // update and render } static const char _data_FX_MODE_PARTICLEPERLIN[] PROGMEM = "PS Fuzzy Noise@Speed,Particles,Bounce,Friction,Scale,Cylinder,Smear,Collide;;!;2;pal=64,sx=50,ix=200,c1=130,c2=30,c3=5,o3=1"; /* Particle smashing down like meteors and exploding as they hit the ground, has many parameters to play with by DedeHai (Damian Schneider) */ #define NUMBEROFSOURCES 8 void mode_particleimpact(void) { ParticleSystem2D *PartSys = nullptr; uint32_t numMeteors; PSsettings2D meteorsettings; meteorsettings.asByte = 0b00101000; // PS settings for meteors: bounceY and gravity enabled if (SEGMENT.call == 0) { // initialization if (!initParticleSystem2D(PartSys, NUMBEROFSOURCES)) // init, no additional data needed FX_FALLBACK_STATIC; // allocation failed or not 2D PartSys->setKillOutOfBounds(true); PartSys->setGravity(); // enable default gravity PartSys->setBounceY(true); // always use ground bounce PartSys->setWallRoughness(220); // high roughness numMeteors = min(PartSys->numSources, (uint32_t)NUMBEROFSOURCES); for (uint32_t i = 0; i < numMeteors; i++) { PartSys->sources[i].source.ttl = hw_random16(10 * i); // set initial delay for meteors PartSys->sources[i].source.vy = 10; // at positive speeds, no particles are emitted and if particle dies, it will be relaunched } } else PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS if (PartSys == nullptr) FX_FALLBACK_STATIC; // something went wrong, no data! // Particle System settings PartSys->updateSystem(); // update system properties (dimensions and data pointers) PartSys->setWrapX(SEGMENT.check1); PartSys->setBounceX(SEGMENT.check2); PartSys->setMotionBlur(SEGMENT.custom3<<3); uint8_t hardness = map(SEGMENT.custom2, 0, 255, PS_P_MINSURFACEHARDNESS - 2, 255); PartSys->setWallHardness(hardness); PartSys->enableParticleCollisions(SEGMENT.check3, hardness); // enable collisions and set particle collision hardness numMeteors = min(PartSys->numSources, (uint32_t)NUMBEROFSOURCES); uint32_t emitparticles; // number of particles to emit for each rocket's state for (uint32_t i = 0; i < numMeteors; i++) { // determine meteor state by its speed: if ( PartSys->sources[i].source.vy < 0) // moving down, emit sparks emitparticles = 1; else if ( PartSys->sources[i].source.vy > 0) // moving up means meteor is on 'standby' emitparticles = 0; else { // speed is zero, explode! PartSys->sources[i].source.vy = 10; // set source speed positive so it goes into timeout and launches again emitparticles = map(SEGMENT.intensity, 0, 255, 10, hw_random16(PartSys->usedParticles>>2)); // defines the size of the explosion } for (int e = emitparticles; e > 0; e--) { PartSys->sprayEmit(PartSys->sources[i]); } } // update the meteors, set the speed state for (uint32_t i = 0; i < numMeteors; i++) { if (PartSys->sources[i].source.ttl) { PartSys->sources[i].source.ttl--; // note: this saves an if statement, but moving down particles age twice if (PartSys->sources[i].source.vy < 0) { // move down PartSys->applyGravity(PartSys->sources[i].source); PartSys->particleMoveUpdate(PartSys->sources[i].source, PartSys->sources[i].sourceFlags, &meteorsettings); // if source reaches the bottom, set speed to 0 so it will explode on next function call (handled above) if (PartSys->sources[i].source.y < PS_P_RADIUS<<1) { // reached the bottom pixel on its way down PartSys->sources[i].source.vy = 0; // set speed zero so it will explode PartSys->sources[i].source.vx = 0; PartSys->sources[i].sourceFlags.collide = true; #ifdef ESP8266 PartSys->sources[i].maxLife = 900; PartSys->sources[i].minLife = 100; #else PartSys->sources[i].maxLife = 1250; PartSys->sources[i].minLife = 250; #endif PartSys->sources[i].source.ttl = hw_random16((768 - (SEGMENT.speed << 1))) + 40; // standby time til next launch (in frames) PartSys->sources[i].vy = (SEGMENT.custom1 >> 2); // emitting speed y PartSys->sources[i].var = (SEGMENT.custom1 >> 2); // speed variation around vx,vy (+/- var) } } } else if (PartSys->sources[i].source.vy > 0) { // meteor is exploded and time is up (ttl==0 and positive speed), relaunch it // reinitialize meteor PartSys->sources[i].source.y = PartSys->maxY + (PS_P_RADIUS << 2); // start 4 pixels above the top PartSys->sources[i].source.x = hw_random(PartSys->maxX); PartSys->sources[i].source.vy = -hw_random16(30) - 30; // meteor downward speed PartSys->sources[i].source.vx = hw_random16(50) - 25; // TODO: make this dependent on position so they do not move out of frame PartSys->sources[i].source.hue = hw_random16(); // random color PartSys->sources[i].source.ttl = 500; // long life, will explode at bottom PartSys->sources[i].sourceFlags.collide = false; // trail particles will not collide PartSys->sources[i].maxLife = 300; // spark particle life PartSys->sources[i].minLife = 100; PartSys->sources[i].vy = -9; // emitting speed (down) PartSys->sources[i].var = 3; // speed variation around vx,vy (+/- var) } } for (uint32_t i = 0; i < PartSys->usedParticles; i++) { if (PartSys->particles[i].ttl > 5) PartSys->particles[i].ttl -= 5; //ttl is linked to brightness, this allows to use higher brightness but still a short spark lifespan } PartSys->update(); // update and render } #undef NUMBEROFSOURCES static const char _data_FX_MODE_PARTICLEIMPACT[] PROGMEM = "PS Impact@Launches,!,Force,Hardness,Blur,Cylinder,Walls,Collide;;!;2;pal=0,sx=32,ix=85,c1=70,c2=130,c3=0,o3=1"; /* Particle Attractor, a particle attractor sits in the matrix center, a spray bounces around and seeds particles uses inverse square law like in planetary motion Uses palette for particle color by DedeHai (Damian Schneider) */ void mode_particleattractor(void) { ParticleSystem2D *PartSys = nullptr; PSsettings2D sourcesettings; sourcesettings.asByte = 0b00001100; // PS settings for bounceY, bounceY used for source movement (it always bounces whereas particles do not) PSparticleFlags attractorFlags; attractorFlags.asByte = 0; // no flags set PSparticle *attractor; // particle pointer to the attractor if (SEGMENT.call == 0) { // initialization if (!initParticleSystem2D(PartSys, 1, sizeof(PSparticle), true)) // init using 1 source and advanced particle settings FX_FALLBACK_STATIC; // allocation failed or not 2D PartSys->sources[0].source.hue = hw_random16(); PartSys->sources[0].source.vx = -7; // will collied with wall and get random bounce direction PartSys->sources[0].sourceFlags.collide = true; // seeded particles will collide PartSys->sources[0].sourceFlags.perpetual = true; //source does not age #ifdef ESP8266 PartSys->sources[0].maxLife = 200; // lifetime in frames (ESP8266 has less particles) PartSys->sources[0].minLife = 30; #else PartSys->sources[0].maxLife = 350; // lifetime in frames PartSys->sources[0].minLife = 50; #endif PartSys->sources[0].var = 4; // emiting variation PartSys->setWallHardness(255); //bounce forever PartSys->setWallRoughness(200); //randomize wall bounce } else { PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS } if (PartSys == nullptr) FX_FALLBACK_STATIC; // something went wrong, no data! // Particle System settings PartSys->updateSystem(); // update system properties (dimensions and data pointers) PartSys->setColorByAge(SEGMENT.check1); PartSys->setParticleSize(SEGMENT.custom1 >> 1); //set size globally PartSys->setUsedParticles(map(SEGMENT.intensity, 0, 255, 25, 190)); attractor = reinterpret_cast(PartSys->PSdataEnd); // set attractor properties attractor->ttl = 100; // never dies if (SEGMENT.check2) { if ((SEGMENT.call % 3) == 0) // move slowly PartSys->particleMoveUpdate(*attractor, attractorFlags, &sourcesettings); // move the attractor } else { attractor->x = PartSys->maxX >> 1; // set to center attractor->y = PartSys->maxY >> 1; } if (SEGMENT.call == 0) { attractor->vx = PartSys->sources[0].source.vy; // set to spray movemement but reverse x and y attractor->vy = PartSys->sources[0].source.vx; } if (SEGMENT.custom2 > 0) // collisions enabled PartSys->enableParticleCollisions(true, map(SEGMENT.custom2, 1, 255, 120, 255)); // enable collisions and set particle collision hardness else PartSys->enableParticleCollisions(false); if (SEGMENT.call % 5 == 0) PartSys->sources[0].source.hue++; SEGENV.aux0 += 256; // emitting angle, one full turn in 255 frames (0xFFFF is 360°) if (SEGMENT.call % 2 == 0) // alternate direction of emit PartSys->angleEmit(PartSys->sources[0], SEGENV.aux0, 12); else PartSys->angleEmit(PartSys->sources[0], SEGENV.aux0 + 0x7FFF, 12); // emit at 180° as well // apply force uint32_t strength = SEGMENT.speed; um_data_t *um_data; if (UsermodManager::getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) { // AR active, do not use simulated data uint32_t volumeSmth = (uint32_t)(*(float*) um_data->u_data[0]); // 0-255 strength = (SEGMENT.speed * volumeSmth) >> 8; } for (uint32_t i = 0; i < PartSys->usedParticles; i++) { PartSys->pointAttractor(i, *attractor, strength, SEGMENT.check3); } if (SEGMENT.call % (33 - SEGMENT.custom3) == 0) PartSys->applyFriction(2); PartSys->particleMoveUpdate(PartSys->sources[0].source, PartSys->sources[0].sourceFlags, &sourcesettings); // move the source PartSys->update(); // update and render } //static const char _data_FX_MODE_PARTICLEATTRACTOR[] PROGMEM = "PS Attractor@Mass,Particles,Size,Collide,Friction,AgeColor,Move,Swallow;;!;2;pal=9,sx=100,ix=82,c1=1,c2=0"; static const char _data_FX_MODE_PARTICLEATTRACTOR[] PROGMEM = "PS Attractor@Mass,Particles,Size,Collide,Friction,AgeColor,Move,Swallow;;!;2;pal=9,sx=100,ix=82,c1=2,c2=0"; /* Particle Spray, just a particle spray with many parameters Uses palette for particle color by DedeHai (Damian Schneider) */ void mode_particlespray(void) { ParticleSystem2D *PartSys = nullptr; const uint8_t hardness = 200; // collision hardness is fixed if (SEGMENT.call == 0) { // initialization if (!initParticleSystem2D(PartSys, 1)) // init, no additional data needed FX_FALLBACK_STATIC; // allocation failed or not 2D PartSys->setKillOutOfBounds(true); // out of bounds particles dont return (except on top, taken care of by gravity setting) PartSys->setBounceY(true); PartSys->setMotionBlur(200); // anable motion blur PartSys->setSmearBlur(10); // anable motion blur PartSys->sources[0].source.hue = hw_random16(); PartSys->sources[0].sourceFlags.collide = true; // seeded particles will collide (if enabled) PartSys->sources[0].var = 3; } else PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS if (PartSys == nullptr) FX_FALLBACK_STATIC; // something went wrong, no data! // Particle System settings PartSys->updateSystem(); // update system properties (dimensions and data pointers) PartSys->setBounceX(!SEGMENT.check2); PartSys->setWrapX(SEGMENT.check2); PartSys->setWallHardness(hardness); PartSys->setGravity(8 * SEGMENT.check1); // enable gravity if checked (8 is default strength) //numSprays = min(PartSys->numSources, (uint8_t)1); // number of sprays if (SEGMENT.check3) // collisions enabled PartSys->enableParticleCollisions(true, hardness); // enable collisions and set particle collision hardness else PartSys->enableParticleCollisions(false); //position according to sliders PartSys->sources[0].source.x = map(SEGMENT.custom1, 0, 255, 0, PartSys->maxX); PartSys->sources[0].source.y = map(SEGMENT.custom2, 0, 255, 0, PartSys->maxY); uint16_t angle = (256 - (((int32_t)SEGMENT.custom3 + 1) << 3)) << 8; um_data_t *um_data; if (UsermodManager::getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) { // get AR data, do not use simulated data uint32_t volumeSmth = (uint8_t)(*(float*) um_data->u_data[0]); //0 to 255 uint32_t volumeRaw = *(int16_t*)um_data->u_data[1]; //0 to 255 PartSys->sources[0].minLife = 30; if (SEGMENT.call % 20 == 0 || SEGMENT.call % (11 - volumeSmth / 25) == 0) { // defines interval of particle emit PartSys->sources[0].maxLife = (volumeSmth >> 1) + (SEGMENT.intensity >> 1); // lifetime in frames PartSys->sources[0].var = 1 + ((volumeRaw * SEGMENT.speed) >> 12); uint32_t emitspeed = (SEGMENT.speed >> 2) + (volumeRaw >> 3); PartSys->sources[0].source.hue += volumeSmth/30; PartSys->angleEmit(PartSys->sources[0], angle, emitspeed); } } else { //no AR data, fall back to normal mode // change source properties if (SEGMENT.call % (11 - (SEGMENT.intensity / 25)) == 0) { // every nth frame, cycle color and emit particles PartSys->sources[0].maxLife = 300 + SEGMENT.intensity; // lifetime in frames PartSys->sources[0].minLife = 150 + SEGMENT.intensity; PartSys->sources[0].source.hue++; // = hw_random16(); //change hue of spray source PartSys->angleEmit(PartSys->sources[0], angle, SEGMENT.speed >> 2); } } PartSys->update(); // update and render } static const char _data_FX_MODE_PARTICLESPRAY[] PROGMEM = "PS Spray@Speed,!,Left/Right,Up/Down,Angle,Gravity,Cylinder/Square,Collide;;!;2v;pal=0,sx=150,ix=150,c1=220,c2=30,c3=21"; /* Particle base Graphical Equalizer Uses palette for particle color by DedeHai (Damian Schneider) */ void mode_particleGEQ(void) { ParticleSystem2D *PartSys = nullptr; if (SEGMENT.call == 0) { // initialization if (!initParticleSystem2D(PartSys, 1)) FX_FALLBACK_STATIC; // allocation failed or not 2D PartSys->setKillOutOfBounds(true); PartSys->setUsedParticles(170); // use 2/3 of available particles } else PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS if (PartSys == nullptr) FX_FALLBACK_STATIC; // something went wrong, no data! uint32_t i; // set particle system properties PartSys->updateSystem(); // update system properties (dimensions and data pointers) PartSys->setWrapX(SEGMENT.check1); PartSys->setBounceX(SEGMENT.check2); PartSys->setBounceY(SEGMENT.check3); //PartSys->enableParticleCollisions(false); PartSys->setWallHardness(SEGMENT.custom2); PartSys->setGravity(SEGMENT.custom3 << 2); // set gravity strength um_data_t *um_data = getAudioData(); uint8_t *fftResult = (uint8_t *)um_data->u_data[2]; // 16 bins with FFT data, log mapped already, each band contains frequency amplitude 0-255 //map the bands into 16 positions on x axis, emit some particles according to frequency loudness i = 0; uint32_t binwidth = (PartSys->maxX + 1)>>4; //emit poisition variation for one bin (+/-) is equal to width/16 (for 16 bins) uint32_t threshold = 300 - SEGMENT.intensity; uint32_t emitparticles = 0; for (uint32_t bin = 0; bin < 16; bin++) { uint32_t xposition = binwidth*bin + (binwidth>>1); // emit position according to frequency band uint8_t emitspeed = ((uint32_t)fftResult[bin] * (uint32_t)SEGMENT.speed) >> 9; // emit speed according to loudness of band (127 max!) emitparticles = 0; if (fftResult[bin] > threshold) { emitparticles = 1;// + (fftResult[bin]>>6); } else if (fftResult[bin] > 0) { // band has low volue uint32_t restvolume = ((threshold - fftResult[bin])>>2) + 2; if (hw_random16() % restvolume == 0) emitparticles = 1; } while (i < PartSys->usedParticles && emitparticles > 0) { // emit particles if there are any left, low frequencies take priority if (PartSys->particles[i].ttl == 0) { // find a dead particle //set particle properties TODO: could also use the spray... PartSys->particles[i].ttl = 20 + map(SEGMENT.intensity, 0,255, emitspeed>>1, emitspeed + hw_random16(emitspeed)) ; // set particle alive, particle lifespan is in number of frames PartSys->particles[i].x = xposition + hw_random16(binwidth) - (binwidth>>1); // position randomly, deviating half a bin width PartSys->particles[i].y = 0; // start at the bottom PartSys->particles[i].vx = hw_random16(SEGMENT.custom1>>1)-(SEGMENT.custom1>>2) ; //x-speed variation: +/- custom1/4 PartSys->particles[i].vy = emitspeed; PartSys->particles[i].hue = (bin<<4) + hw_random16(17) - 8; // color from palette according to bin emitparticles--; } i++; } } PartSys->update(); // update and render } static const char _data_FX_MODE_PARTICLEGEQ[] PROGMEM = "PS GEQ 2D@Speed,Intensity,Diverge,Bounce,Gravity,Cylinder,Walls,Floor;;!;2f;pal=0,sx=155,ix=200,c1=0"; /* Particle rotating GEQ Particles sprayed from center with rotating spray Uses palette for particle color by DedeHai (Damian Schneider) */ #define NUMBEROFSOURCES 16 void mode_particlecenterGEQ(void) { ParticleSystem2D *PartSys = nullptr; uint8_t numSprays; uint32_t i; if (SEGMENT.call == 0) { // initialization if (!initParticleSystem2D(PartSys, NUMBEROFSOURCES)) // init, request 16 sources FX_FALLBACK_STATIC; // allocation failed or not 2D numSprays = min(PartSys->numSources, (uint32_t)NUMBEROFSOURCES); for (i = 0; i < numSprays; i++) { PartSys->sources[i].source.x = (PartSys->maxX + 1) >> 1; // center PartSys->sources[i].source.y = (PartSys->maxY + 1) >> 1; // center PartSys->sources[i].source.hue = i * 16; // even color distribution PartSys->sources[i].maxLife = 400; PartSys->sources[i].minLife = 200; } PartSys->setKillOutOfBounds(true); } else PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS if (PartSys == nullptr) FX_FALLBACK_STATIC; // something went wrong, no data! PartSys->updateSystem(); // update system properties (dimensions and data pointers) numSprays = min(PartSys->numSources, (uint32_t)NUMBEROFSOURCES); um_data_t *um_data = getAudioData(); uint8_t *fftResult = (uint8_t *)um_data->u_data[2]; // 16 bins with FFT data, log mapped already, each band contains frequency amplitude 0-255 uint32_t threshold = 300 - SEGMENT.intensity; if (SEGMENT.check2) SEGENV.aux0 += SEGMENT.custom1 << 2; else SEGENV.aux0 -= SEGMENT.custom1 << 2; uint16_t angleoffset = (uint16_t)0xFFFF / (uint16_t)numSprays; uint32_t j = hw_random16(numSprays); // start with random spray so all get a chance to emit a particle if maximum number of particles alive is reached. for (i = 0; i < numSprays; i++) { if (SEGMENT.call % (32 - (SEGMENT.custom2 >> 3)) == 0 && SEGMENT.custom2 > 0) PartSys->sources[j].source.hue += 1 + (SEGMENT.custom2 >> 4); PartSys->sources[j].var = SEGMENT.custom3 >> 2; int8_t emitspeed = 5 + (((uint32_t)fftResult[j] * ((uint32_t)SEGMENT.speed + 20)) >> 10); // emit speed according to loudness of band uint16_t emitangle = j * angleoffset + SEGENV.aux0; uint32_t emitparticles = 0; if (fftResult[j] > threshold) emitparticles = 1; else if (fftResult[j] > 0) { // band has low value uint32_t restvolume = ((threshold - fftResult[j]) >> 2) + 2; if (hw_random16() % restvolume == 0) emitparticles = 1; } if (emitparticles) PartSys->angleEmit(PartSys->sources[j], emitangle, emitspeed); j = (j + 1) % numSprays; } PartSys->update(); // update and render } static const char _data_FX_MODE_PARTICLECIRCULARGEQ[] PROGMEM = "PS GEQ Nova@Speed,Intensity,Rotation Speed,Color Change,Nozzle,,Direction;;!;2f;pal=13,ix=180,c1=0,c2=0,c3=8"; /* Particle replacement of Ghost Rider by DedeHai (Damian Schneider), original FX by stepko adapted by Blaz Kristan (AKA blazoncek) */ #define MAXANGLESTEP 2200 //32767 means 180° void mode_particleghostrider(void) { ParticleSystem2D *PartSys = nullptr; PSsettings2D ghostsettings; ghostsettings.asByte = 0b0000011; //enable wrapX and wrapY if (SEGMENT.call == 0) { // initialization if (!initParticleSystem2D(PartSys, 1)) // init, no additional data needed FX_FALLBACK_STATIC; // allocation failed or not 2D PartSys->setKillOutOfBounds(true); // out of bounds particles dont return (except on top, taken care of by gravity setting) PartSys->sources[0].maxLife = 260; // lifetime in frames PartSys->sources[0].minLife = 250; PartSys->sources[0].source.x = hw_random16(PartSys->maxX); PartSys->sources[0].source.y = hw_random16(PartSys->maxY); SEGENV.step = hw_random16(MAXANGLESTEP) - (MAXANGLESTEP>>1); // angle increment } else { PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS } if (PartSys == nullptr) FX_FALLBACK_STATIC; // something went wrong, no data! if (SEGMENT.intensity > 0) { // spiraling if (SEGENV.aux1) { SEGENV.step += SEGMENT.intensity>>3; if ((int32_t)SEGENV.step > MAXANGLESTEP) SEGENV.aux1 = 0; } else { SEGENV.step -= SEGMENT.intensity>>3; if ((int32_t)SEGENV.step < -MAXANGLESTEP) SEGENV.aux1 = 1; } } // Particle System settings PartSys->updateSystem(); // update system properties (dimensions and data pointers) PartSys->setMotionBlur(SEGMENT.custom1); PartSys->sources[0].var = SEGMENT.custom3 >> 1; // color by age (PS 'color by age' always starts with hue = 255, don't want that here) if (SEGMENT.check1) { for (uint32_t i = 0; i < PartSys->usedParticles; i++) { PartSys->particles[i].hue = PartSys->sources[0].source.hue + (PartSys->particles[i].ttl<<2); } } // enable/disable walls ghostsettings.bounceX = SEGMENT.check2; ghostsettings.bounceY = SEGMENT.check2; SEGENV.aux0 += (int32_t)SEGENV.step; // step is angle increment uint16_t emitangle = SEGENV.aux0 + 32767; // +180° int32_t speed = map(SEGMENT.speed, 0, 255, 12, 64); PartSys->sources[0].source.vx = ((int32_t)cos16_t(SEGENV.aux0) * speed) / (int32_t)32767; PartSys->sources[0].source.vy = ((int32_t)sin16_t(SEGENV.aux0) * speed) / (int32_t)32767; PartSys->sources[0].source.ttl = 500; // source never dies (note: setting 'perpetual' is not needed if replenished each frame) PartSys->particleMoveUpdate(PartSys->sources[0].source, PartSys->sources[0].sourceFlags, &ghostsettings); // set head (steal one of the particles) PartSys->particles[PartSys->usedParticles-1].x = PartSys->sources[0].source.x; PartSys->particles[PartSys->usedParticles-1].y = PartSys->sources[0].source.y; PartSys->particles[PartSys->usedParticles-1].ttl = 255; PartSys->particles[PartSys->usedParticles-1].sat = 0; //white // emit two particles PartSys->angleEmit(PartSys->sources[0], emitangle, speed); PartSys->angleEmit(PartSys->sources[0], emitangle, speed); if (SEGMENT.call % (11 - (SEGMENT.custom2 / 25)) == 0) { // every nth frame, cycle color and emit particles PartSys->sources[0].source.hue++; } if (SEGMENT.custom2 > 190) //fast color change PartSys->sources[0].source.hue += (SEGMENT.custom2 - 190) >> 2; PartSys->update(); // update and render } static const char _data_FX_MODE_PARTICLEGHOSTRIDER[] PROGMEM = "PS Ghost Rider@Speed,Spiral,Blur,Color Cycle,Spread,AgeColor,Walls;;!;2;pal=1,sx=70,ix=0,c1=220,c2=30,c3=21,o1=1"; /* PS Blobs: large particles bouncing around, changing size and form Uses palette for particle color by DedeHai (Damian Schneider) */ void mode_particleblobs(void) { ParticleSystem2D *PartSys = nullptr; if (SEGMENT.call == 0) { if (!initParticleSystem2D(PartSys, 0, 0, true, true)) //init, no additional bytes, advanced size & size control FX_FALLBACK_STATIC; // allocation failed or not 2D PartSys->setBounceX(true); PartSys->setBounceY(true); PartSys->setWallHardness(255); PartSys->setWallRoughness(255); PartSys->setCollisionHardness(255); PartSys->perParticleSize = true; // enable per particle size control } else PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS if (PartSys == nullptr) FX_FALLBACK_STATIC; // something went wrong, no data! PartSys->updateSystem(); // update system properties (dimensions and data pointers) PartSys->setUsedParticles(map(SEGMENT.intensity, 0, 255, 25, 128)); // minimum 10%, maximum 50% of available particles (note: PS ensures at least 1) PartSys->enableParticleCollisions(SEGMENT.check2); for (uint32_t i = 0; i < PartSys->usedParticles; i++) { // update particles if (SEGENV.aux0 != SEGMENT.speed || PartSys->particles[i].ttl == 0) { // speed changed or dead PartSys->particles[i].vx = (int8_t)hw_random16(SEGMENT.speed >> 1) - (SEGMENT.speed >> 2); // +/- speed/4 PartSys->particles[i].vy = (int8_t)hw_random16(SEGMENT.speed >> 1) - (SEGMENT.speed >> 2); } if (SEGENV.aux1 != SEGMENT.custom1 || PartSys->particles[i].ttl == 0) // size changed or dead PartSys->advPartSize[i].maxsize = 60 + (SEGMENT.custom1 >> 1) + hw_random16((SEGMENT.custom1 >> 2)); // set each particle to slightly randomized size //PartSys->particles[i].perpetual = SEGMENT.check2; //infinite life if set if (PartSys->particles[i].ttl == 0) { // find dead particle, renitialize PartSys->particles[i].ttl = 300 + hw_random16(((uint16_t)SEGMENT.custom2 << 3) + 100); PartSys->particles[i].x = hw_random(PartSys->maxX); PartSys->particles[i].y = hw_random16(PartSys->maxY); PartSys->particles[i].hue = hw_random16(); // set random color PartSys->particleFlags[i].collide = true; // enable collision for particle PartSys->advPartProps[i].size = 0; // start out small PartSys->advPartSize[i].asymmetry = hw_random16(220); PartSys->advPartSize[i].asymdir = hw_random16(255); // set advanced size control properties PartSys->advPartSize[i].grow = true; PartSys->advPartSize[i].growspeed = 1 + hw_random16(9); PartSys->advPartSize[i].shrinkspeed = 1 + hw_random16(9); PartSys->advPartSize[i].wobblespeed = 1 + hw_random16(3); } //PartSys->advPartSize[i].asymmetry++; PartSys->advPartSize[i].pulsate = SEGMENT.check3; PartSys->advPartSize[i].wobble = SEGMENT.check1; } SEGENV.aux0 = SEGMENT.speed; //write state back SEGENV.aux1 = SEGMENT.custom1; um_data_t *um_data; if (UsermodManager::getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) { // get AR data if available, do not use simulated data uint8_t volumeSmth = (uint8_t)(*(float*)um_data->u_data[0]); for (uint32_t i = 0; i < PartSys->usedParticles; i++) { // update particles if (SEGMENT.check3) //pulsate selected PartSys->advPartProps[i].size = volumeSmth; } } PartSys->setMotionBlur(((SEGMENT.custom3) << 3) + 7); PartSys->update(); // update and render } static const char _data_FX_MODE_PARTICLEBLOBS[] PROGMEM = "PS Blobs@Speed,Blobs,Size,Life,Blur,Wobble,Collide,Pulsate;;!;2v;sx=30,ix=64,c1=200,c2=130,c3=0,o3=1"; /* Particle Galaxy, particles spiral like in a galaxy Uses palette for particle color by DedeHai (Damian Schneider) */ void mode_particlegalaxy(void) { ParticleSystem2D *PartSys = nullptr; PSsettings2D sourcesettings; sourcesettings.asByte = 0b00001100; // PS settings for bounceY, bounceY used for source movement (it always bounces whereas particles do not) if (SEGMENT.call == 0) { // initialization if (!initParticleSystem2D(PartSys, 1, 0, true)) // init using 1 source and advanced particle settings FX_FALLBACK_STATIC; // allocation failed or not 2D PartSys->sources[0].source.vx = -4; // will collide with wall and get random bounce direction PartSys->sources[0].source.x = PartSys->maxX >> 1; // start in the center PartSys->sources[0].source.y = PartSys->maxY >> 1; PartSys->sources[0].sourceFlags.perpetual = true; //source does not age PartSys->sources[0].maxLife = 4000; // lifetime in frames PartSys->sources[0].minLife = 800; PartSys->sources[0].source.hue = hw_random16(); // start with random color PartSys->setWallHardness(255); //bounce forever PartSys->setWallRoughness(200); //randomize wall bounce } else { PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS } if (PartSys == nullptr) FX_FALLBACK_STATIC; // something went wrong, no data! // Particle System settings PartSys->updateSystem(); // update system properties (dimensions and data pointers) uint8_t particlesize = SEGMENT.custom1; PartSys->setParticleSize(particlesize); // set size globally PartSys->setMotionBlur(250 * SEGMENT.check3); // adds trails to single/quad pixel particles, no effect if size > 1 if ((SEGMENT.call % ((33 - SEGMENT.custom3) >> 1)) == 0) // change hue of emitted particles PartSys->sources[0].source.hue+=2; if (hw_random8() < (10 + (SEGMENT.intensity >> 1))) // 5%-55% chance to emit a particle in this frame PartSys->sprayEmit(PartSys->sources[0]); if ((SEGMENT.call & 0x3) == 0) // every 4th frame, move the emitter PartSys->particleMoveUpdate(PartSys->sources[0].source, PartSys->sources[0].sourceFlags, &sourcesettings); // move alive particles in a spiral motion (or almost straight in fast starfield mode) int32_t centerx = PartSys->maxX >> 1; // center of matrix in subpixel coordinates int32_t centery = PartSys->maxY >> 1; if (SEGMENT.check2) { // starfield mode PartSys->setKillOutOfBounds(true); PartSys->sources[0].var = 7; // emiting variation PartSys->sources[0].source.x = centerx; // set emitter to center PartSys->sources[0].source.y = centery; } else { PartSys->setKillOutOfBounds(false); PartSys->sources[0].var = 1; // emiting variation } for (uint32_t i = 0; i < PartSys->usedParticles; i++) { //check all particles if (PartSys->particles[i].ttl == 0) continue; //skip dead particles // (dx/dy): vector pointing from particle to center int32_t dx = centerx - PartSys->particles[i].x; int32_t dy = centery - PartSys->particles[i].y; //speed towards center: int32_t distance = sqrt32_bw(dx * dx + dy * dy); // absolute distance to center if (distance < 20) distance = 20; // avoid division by zero, keep a minimum int32_t speedfactor; if (SEGMENT.check2) { // starfield mode speedfactor = 1 + (1 + (SEGMENT.speed >> 1)) * distance; // speed increases towards edge //apply velocity PartSys->particles[i].x += (-speedfactor * dx) / 400000 - (dy >> 6); PartSys->particles[i].y += (-speedfactor * dy) / 400000 + (dx >> 6); } else { speedfactor = 2 + (((50 + SEGMENT.speed) << 6) / distance); // speed increases towards center // rotate clockwise int32_t tempVx = (-speedfactor * dy); // speed is orthogonal to center vector int32_t tempVy = (speedfactor * dx); //add speed towards center to make particles spiral in int vxc = (dx << 9) / (distance - 19); // subtract value from distance to make the pull-in force a bit stronger (helps on faster speeds) int vyc = (dy << 9) / (distance - 19); //apply velocity PartSys->particles[i].x += (tempVx + vxc) / 1024; // note: cannot use bit shift as that causes asymmetric rounding PartSys->particles[i].y += (tempVy + vyc) / 1024; if (distance < 128) { // close to center if (PartSys->particles[i].ttl > 3) PartSys->particles[i].ttl -= 4; //age fast PartSys->particles[i].sat = distance << 1; // turn white towards center } } if(SEGMENT.custom3 == 31) // color by age but mapped to 1024 as particles have a long life, since age is random, this gives more or less random colors PartSys->particles[i].hue = PartSys->particles[i].ttl >> 2; else if(SEGMENT.custom3 == 0) // color by distance PartSys->particles[i].hue = map(distance, 20, (PartSys->maxX + PartSys->maxY) >> 2, 0, 180); // color by distance to center } PartSys->update(); // update and render } static const char _data_FX_MODE_PARTICLEGALAXY[] PROGMEM = "PS Galaxy@!,!,Size,,Color,,Starfield,Trace;;!;2;pal=59,sx=80,c1=1,c3=4"; #endif //WLED_DISABLE_PARTICLESYSTEM2D #endif // WLED_DISABLE_2D /////////////////////////// // 1D Particle System FX // /////////////////////////// #ifndef WLED_DISABLE_PARTICLESYSTEM1D /* Particle version of Drip and Rain Uses palette for particle color by DedeHai (Damian Schneider) */ void mode_particleDrip(void) { ParticleSystem1D *PartSys = nullptr; //uint8_t numSprays; if (SEGMENT.call == 0) { // initialization if (!initParticleSystem1D(PartSys, 4)) // init FX_FALLBACK_STATIC; // allocation failed or single pixel PartSys->setKillOutOfBounds(true); // out of bounds particles dont return (except on top, taken care of by gravity setting) PartSys->sources[0].source.hue = hw_random16(); SEGENV.aux1 = 0xFFFF; // invalidate } else PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS if (PartSys == nullptr) FX_FALLBACK_STATIC; // something went wrong, no data! // Particle System settings PartSys->updateSystem(); // update system properties (dimensions and data pointers) PartSys->setBounce(true); PartSys->setWallHardness(50); PartSys->setMotionBlur(SEGMENT.custom2); // anable motion blur PartSys->setGravity(SEGMENT.custom3 >> 1); // set gravity (8 is default strength) PartSys->setParticleSize(SEGMENT.check3); // 1 or 2 pixel rendering if (SEGMENT.check2) { //collisions enabled PartSys->enableParticleCollisions(true); //enable, full hardness } else PartSys->enableParticleCollisions(false); PartSys->sources[0].sourceFlags.collide = false; //drops do not collide if (SEGMENT.check1) { //rain mode, emit at random position, short life (3-8 seconds at 50fps) if (SEGMENT.custom1 == 0) //splash disabled, do not bounce raindrops PartSys->setBounce(false); PartSys->sources[0].var = 5; PartSys->sources[0].v = -(8 + (SEGMENT.speed >> 2)); //speed + var must be < 128, inverted speed (=down) // lifetime in frames PartSys->sources[0].minLife = 30; PartSys->sources[0].maxLife = 200; PartSys->sources[0].source.x = hw_random(PartSys->maxX); //random emit position } else { //drip PartSys->sources[0].var = 0; PartSys->sources[0].v = -(SEGMENT.speed >> 1); //speed + var must be < 128, inverted speed (=down) PartSys->sources[0].minLife = 3000; PartSys->sources[0].maxLife = 3000; PartSys->sources[0].source.x = PartSys->maxX - PS_P_RADIUS_1D; } if (SEGENV.aux1 != SEGMENT.intensity) //slider changed SEGENV.aux0 = 1; //must not be zero or "% 0" happens below which crashes on ESP32 SEGENV.aux1 = SEGMENT.intensity; // save state // every nth frame emit a particle if (SEGMENT.call % SEGENV.aux0 == 0) { int32_t interval = 300 / ((SEGMENT.intensity) + 1); SEGENV.aux0 = interval + hw_random(interval + 5); // if (SEGMENT.check1) // rain mode // PartSys->sources[0].source.hue = 0; // else PartSys->sources[0].source.hue = hw_random8(); //set random color TODO: maybe also not random but color cycling? need another slider or checkmark for this. PartSys->sprayEmit(PartSys->sources[0]); } for (uint32_t i = 0; i < PartSys->usedParticles; i++) { //check all particles if (PartSys->particles[i].ttl && PartSys->particleFlags[i].collide == false) { // use collision flag to identify splash particles if (SEGMENT.custom1 > 0 && PartSys->particles[i].x < (PS_P_RADIUS_1D << 1)) { //splash enabled and reached bottom PartSys->particles[i].ttl = 0; //kill origin particle PartSys->sources[0].maxLife = 80; PartSys->sources[0].minLife = 20; PartSys->sources[0].var = 10 + (SEGMENT.custom1 >> 3); PartSys->sources[0].v = 0; PartSys->sources[0].source.hue = PartSys->particles[i].hue; PartSys->sources[0].source.x = PS_P_RADIUS_1D; PartSys->sources[0].sourceFlags.collide = true; //splashes do collide if enabled for (int j = 0; j < 2 + (SEGMENT.custom1 >> 2); j++) { PartSys->sprayEmit(PartSys->sources[0]); } } } if (SEGMENT.check1) { //rain mode, fade hue to max if (PartSys->particles[i].hue < 245) PartSys->particles[i].hue += 8; } //increase speed on high settings by calling the move function twice note: this can lead to missed collisions if (SEGMENT.speed > 200) PartSys->particleMoveUpdate(PartSys->particles[i], PartSys->particleFlags[i]); } PartSys->update(); // update and render } static const char _data_FX_MODE_PARTICLEDRIP[] PROGMEM = "PS DripDrop@Speed,!,Splash,Blur,Gravity,Rain,PushSplash,Smooth;,!;!;1;pal=0,sx=150,ix=25,c1=220,c2=30,c3=21"; /* Particle Version of "Bouncing Balls by Aircoookie" Also does rolling balls and juggle (and popcorn) Uses palette for particle color by DedeHai (Damian Schneider) */ void mode_particlePinball(void) { ParticleSystem1D *PartSys = nullptr; if (SEGMENT.call == 0) { // initialization if (!initParticleSystem1D(PartSys, 1, 128, 0, true)) // init FX_FALLBACK_STATIC; // allocation failed or is single pixel PartSys->sources[0].sourceFlags.collide = true; // seeded particles will collide (if enabled) PartSys->sources[0].source.x = -1000; // shoot up from below //PartSys->setKillOutOfBounds(true); // out of bounds particles dont return (except on top, taken care of by gravity setting) SEGENV.aux0 = 1; SEGENV.aux1 = 5000; // set settings out of range to ensure uptate on first call } else PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS if (PartSys == nullptr) FX_FALLBACK_STATIC; // something went wrong, no data! // Particle System settings //uint32_t hardness = 240 + (SEGMENT.custom1>>4); PartSys->updateSystem(); // update system properties (dimensions and data pointers) PartSys->setGravity(map(SEGMENT.custom3, 0 , 31, 0 , 8)); // set gravity (8 is default strength) PartSys->setBounce(SEGMENT.custom3); // disables bounce if no gravity is used PartSys->setMotionBlur(SEGMENT.custom2); // anable motion blur PartSys->enableParticleCollisions(SEGMENT.check1, 255); // enable collisions and set particle collision to high hardness PartSys->setColorByPosition(SEGMENT.check3); uint32_t maxParticles = max(20, SEGMENT.intensity / (1 + (SEGMENT.check2 * (SEGMENT.custom1 >> 5)))); // max particles depends on intensity and rolling balls mode + size if (SEGMENT.custom1 < 255) PartSys->setParticleSize(SEGMENT.custom1); // set size globally else { PartSys->perParticleSize = true; // use random individual particle size (see below) maxParticles *= 2; // use more particles if individual s ize is used as there is more space } PartSys->setUsedParticles(maxParticles); // reduce if using larger size and rolling balls mode bool updateballs = false; if (SEGENV.aux1 != SEGMENT.speed + SEGMENT.intensity + SEGMENT.check2 + SEGMENT.custom1 + PartSys->usedParticles) { // user settings change or more particles are available SEGENV.step = SEGMENT.call; // reset delay updateballs = true; PartSys->sources[0].maxLife = SEGMENT.custom3 ? 1000 : 0xFFFF; // maximum lifetime in frames/2 (very long if not using gravity, this is enough to travel 4000 pixels at min speed) PartSys->sources[0].minLife = PartSys->sources[0].maxLife >> 1; } if (SEGMENT.check2) { // rolling balls PartSys->setGravity(0); PartSys->setWallHardness(255); int speedsum = 0; for (uint32_t i = 0; i < PartSys->usedParticles; i++) { PartSys->particles[i].ttl = 500; // keep particles alive if (updateballs) { // speed changed or particle is dead, set particle properties PartSys->particleFlags[i].collide = true; if (PartSys->particles[i].x == 0) { // still at initial position PartSys->particles[i].x = hw_random16(PartSys->maxX); // random initial position for all particles PartSys->particles[i].vx = (hw_random16() & 0x01) ? 1 : -1; // random initial direction } PartSys->particles[i].hue = hw_random8(); //set ball colors to random PartSys->advPartProps[i].sat = 255; PartSys->advPartProps[i].size = hw_random8(); // set ball size for individual size mode } speedsum += abs(PartSys->particles[i].vx); } int32_t avgSpeed = speedsum / PartSys->usedParticles; int32_t setSpeed = 2 + (SEGMENT.speed >> 2); if (avgSpeed < setSpeed) { // if balls are slow, speed up some of them at random to keep the animation going for (int i = 0; i < setSpeed - avgSpeed; i++) { int idx = hw_random16(PartSys->usedParticles); if (abs(PartSys->particles[idx].vx) < PS_P_MAXSPEED) PartSys->particles[idx].vx += PartSys->particles[idx].vx >= 0 ? 1 : -1; // add 1, keep direction } } else if (avgSpeed > setSpeed + 8) // if avg speed is too high, apply friction to slow them down PartSys->applyFriction(1); } else { // bouncing balls PartSys->setWallHardness(220); PartSys->sources[0].var = SEGMENT.speed >> 3; int32_t newspeed = 2 + (SEGMENT.speed >> 1) - (SEGMENT.speed >> 3); PartSys->sources[0].v = newspeed; //check for balls that are 'laying on the ground' and remove them for (uint32_t i = 0; i < PartSys->usedParticles; i++) { if (PartSys->particles[i].ttl < 50) PartSys->particles[i].ttl = 0; // no dark particles else if (PartSys->particles[i].vx == 0 && PartSys->particles[i].x < (PS_P_RADIUS_1D + SEGMENT.custom1)) PartSys->particles[i].ttl -= 50; // age fast if (updateballs) { if (SEGMENT.custom3 == 0) // gravity off, update speed PartSys->particles[i].vx = PartSys->particles[i].vx > 0 ? newspeed : -newspeed; //keep the direction } } // every nth frame emit a ball if (SEGMENT.call > SEGENV.step) { int interval = 260 - ((int)SEGMENT.intensity); SEGENV.step += interval + hw_random16(interval); PartSys->sources[0].source.hue = hw_random16(); //set ball color PartSys->sources[0].sat = 255; PartSys->sources[0].size = hw_random8(); //set ball size PartSys->sprayEmit(PartSys->sources[0]); } } SEGENV.aux1 = SEGMENT.speed + SEGMENT.intensity + SEGMENT.check2 + SEGMENT.custom1 + PartSys->usedParticles; //for (uint32_t i = 0; i < PartSys->usedParticles; i++) { // PartSys->particleMoveUpdate(PartSys->particles[i], PartSys->particleFlags[i]); // double the speed note: this leads to bad collisions, also need to run collision detection before //} PartSys->update(); // update and render } static const char _data_FX_MODE_PSPINBALL[] PROGMEM = "PS Pinball@Speed,!,Size,Blur,Gravity,Collide,Rolling,Position Color;,!;!;1;pal=0,ix=220,c2=0,c3=8,o1=1"; /* Particle Replacement for original Dancing Shadows: "Spotlights moving back and forth that cast dancing shadows. Shine this through tree branches/leaves or other close-up objects that cast interesting shadows onto a ceiling or tarp. By Steve Pomeroy @xxv" Uses palette for particle color by DedeHai (Damian Schneider) */ void mode_particleDancingShadows(void) { ParticleSystem1D *PartSys = nullptr; if (SEGMENT.call == 0) { // initialization if (!initParticleSystem1D(PartSys, 1)) // init, one source FX_FALLBACK_STATIC; // allocation failed or is single pixel PartSys->sources[0].maxLife = 1000; //set long life (kill out of bounds is done in custom way) PartSys->sources[0].minLife = PartSys->sources[0].maxLife; } else { PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS } if (PartSys == nullptr) FX_FALLBACK_STATIC; // something went wrong, no data! // Particle System settings PartSys->updateSystem(); // update system properties (dimensions and data pointers) PartSys->setMotionBlur(SEGMENT.custom1); if (SEGMENT.check1) PartSys->setSmearBlur(120); // enable smear blur else PartSys->setSmearBlur(0); // disable smear blur PartSys->setParticleSize(SEGMENT.check3); // 1 or 2 pixel rendering PartSys->setColorByPosition(SEGMENT.check2); // color fixed by position PartSys->setUsedParticles(map(SEGMENT.intensity, 0, 255, 10, 255)); // set percentage of particles to use uint32_t deadparticles = 0; //kill out of bounds and moving away plus change color for (uint32_t i = 0; i < PartSys->usedParticles; i++) { if (((SEGMENT.call & 0x07) == 0) && PartSys->particleFlags[i].outofbounds) { //check if out of bounds particle move away from strip, only update every 8th frame if ((int32_t)PartSys->particles[i].vx * PartSys->particles[i].x > 0) PartSys->particles[i].ttl = 0; //particle is moving away, kill it } PartSys->particleFlags[i].perpetual = true; //particles do not age if (SEGMENT.call % (32 / (1 + (SEGMENT.custom2 >> 3))) == 0) PartSys->particles[i].hue += 2 + (SEGMENT.custom2 >> 5); //note: updating speed on the fly is not accurately possible, since it is unknown which particles are assigned to which spot if (SEGENV.aux0 != SEGMENT.speed) { //speed changed //update all particle speed by setting them to current value PartSys->particles[i].vx = PartSys->particles[i].vx > 0 ? SEGMENT.speed >> 3 : -SEGMENT.speed >> 3; } if (PartSys->particles[i].ttl == 0) deadparticles++; // count dead particles } SEGENV.aux0 = SEGMENT.speed; //generate a spotlight: generates particles just outside of view if (deadparticles > 5 && (SEGMENT.call & 0x03) == 0) { //random color, random type uint32_t type = hw_random16(SPOT_TYPES_COUNT); int8_t speed = 2 + hw_random16(2 + (SEGMENT.speed >> 1)) + (SEGMENT.speed >> 4); int32_t width = hw_random16(1, 10); uint32_t ttl = 300; //ttl is particle brightness (below perpetual is set so it does not age, i.e. ttl stays at this value) int32_t position; //choose random start position, left and right from the segment if (hw_random() & 0x01) { position = PartSys->maxXpixel; speed = -speed; } else position = -width; PartSys->sources[0].v = speed; //emitted particle speed PartSys->sources[0].source.hue = hw_random8(); //random spotlight color for (int32_t i = 0; i < width; i++) { if (width > 1) { switch (type) { case SPOT_TYPE_SOLID: //nothing to do break; case SPOT_TYPE_GRADIENT: ttl = cubicwave8(map(i, 0, width - 1, 0, 255)); ttl = ttl*ttl >> 8; //make gradient more pronounced break; case SPOT_TYPE_2X_GRADIENT: ttl = cubicwave8(2 * map(i, 0, width - 1, 0, 255)); ttl = ttl*ttl >> 8; break; case SPOT_TYPE_2X_DOT: if (i > 0) position++; //skip one pixel i++; break; case SPOT_TYPE_3X_DOT: if (i > 0) position += 2; //skip two pixels i+=2; break; case SPOT_TYPE_4X_DOT: if (i > 0) position += 3; //skip three pixels i+=3; break; } } //emit particle //set the particle source position: PartSys->sources[0].source.x = position * PS_P_RADIUS_1D; uint32_t partidx = PartSys->sprayEmit(PartSys->sources[0]); PartSys->particles[partidx].ttl = ttl; position++; //do the next pixel } } PartSys->update(); // update and render } static const char _data_FX_MODE_PARTICLEDANCINGSHADOWS[] PROGMEM = "PS Dancing Shadows@Speed,!,Blur,Color Cycle,,Smear,Position Color,Smooth;,!;!;1;sx=100,ix=180,c1=0,c2=0"; /* Particle Fireworks 1D replacement Uses palette for particle color by DedeHai (Damian Schneider) */ void mode_particleFireworks1D(void) { ParticleSystem1D *PartSys = nullptr; uint8_t *forcecounter; if (SEGMENT.call == 0) { // initialization if (!initParticleSystem1D(PartSys, 4, 150, 4, true)) // init advanced particle system FX_FALLBACK_STATIC; // allocation failed or is single pixel PartSys->setKillOutOfBounds(true); PartSys->sources[0].sourceFlags.custom1 = 1; // set rocket state to standby } else PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS if (PartSys == nullptr) FX_FALLBACK_STATIC; // something went wrong, no data! // Particle System settings PartSys->updateSystem(); // update system properties (dimensions and data pointers) forcecounter = PartSys->PSdataEnd; PartSys->setMotionBlur(SEGMENT.custom2); // anable motion blur int32_t gravity = (1 + (SEGMENT.speed >> 3)); // gravity value used for rocket speed calculation PartSys->setGravity(SEGMENT.speed ? gravity : 0); // set gravity PartSys->setParticleSize(SEGMENT.check3); // 1 or 2 pixel rendering (global size, disables per particle size) if (PartSys->sources[0].sourceFlags.custom1 == 1) { // rocket is on standby PartSys->sources[0].source.ttl--; if (PartSys->sources[0].source.ttl == 0) { // time is up, relaunch if (hw_random8() < SEGMENT.custom1) // randomly choose direction according to slider, fire at start of segment if true SEGENV.aux0 = 1; else SEGENV.aux0 = 0; PartSys->sources[0].sourceFlags.custom1 = 0; //flag used for rocket state PartSys->sources[0].source.hue = hw_random16(); // different color for each launch PartSys->sources[0].var = 10 * SEGMENT.check2; // emit variation, 0 if trail mode is off PartSys->sources[0].v = -10 * SEGMENT.check2; // emit speed, 0 if trail mode is off PartSys->sources[0].minLife = 180; PartSys->sources[0].maxLife = SEGMENT.check2 ? 700 : 240; // exhaust particle life PartSys->sources[0].source.x = SEGENV.aux0 * PartSys->maxX; // start from bottom or top uint32_t speed = sqrt((gravity * ((PartSys->maxX >> 2) + hw_random16(PartSys->maxX >> 1))) >> 4); // set speed such that rocket explods in frame PartSys->sources[0].source.vx = min(speed, (uint32_t)127); PartSys->sources[0].source.ttl = 4000; PartSys->sources[0].sat = 30; // low saturation exhaust PartSys->sources[0].sourceFlags.reversegrav = false ; // normal gravity if (SEGENV.aux0) { // inverted rockets launch from end PartSys->sources[0].sourceFlags.reversegrav = true; //PartSys->sources[0].source.x = PartSys->maxX; // start from top PartSys->sources[0].source.vx = -PartSys->sources[0].source.vx; // revert direction PartSys->sources[0].v = -PartSys->sources[0].v; // invert exhaust emit speed } } } else { // rocket is launched int32_t rocketgravity = -gravity; int32_t currentspeed = PartSys->sources[0].source.vx; if (SEGENV.aux0) { // negative speed rocket rocketgravity = -rocketgravity; currentspeed = -currentspeed; } PartSys->applyForce(PartSys->sources[0].source, rocketgravity, forcecounter[0]); PartSys->particleMoveUpdate(PartSys->sources[0].source, PartSys->sources[0].sourceFlags); PartSys->particleMoveUpdate(PartSys->sources[0].source, PartSys->sources[0].sourceFlags); // increase rocket speed by calling the move function twice, also ages twice uint32_t rocketheight = SEGENV.aux0 ? PartSys->maxX - PartSys->sources[0].source.x : PartSys->sources[0].source.x; if (currentspeed < 0 && PartSys->sources[0].source.ttl > 50) // reached apogee PartSys->sources[0].source.ttl = 50 - gravity;// min((uint32_t)50, 15 + (rocketheight >> (PS_P_RADIUS_SHIFT_1D + 3))); // alive for a few more frames if (PartSys->sources[0].source.ttl < 2) { // explode PartSys->sources[0].sourceFlags.custom1 = 1; // set standby state PartSys->sources[0].var = 5 + ((((PartSys->maxX >> 1) + rocketheight) * (20 + (SEGMENT.intensity << 1))) / (PartSys->maxX << 2)); // set explosion particle speed PartSys->sources[0].minLife = 1200; PartSys->sources[0].maxLife = 2600; PartSys->sources[0].source.ttl = 100 + hw_random16(64 - (SEGMENT.speed >> 2)); // standby time til next launch PartSys->sources[0].sat = SEGMENT.custom3 < 16 ? 10 + (SEGMENT.custom3 << 4) : 255; //color saturation PartSys->sources[0].size = SEGMENT.check3 ? hw_random16(SEGMENT.intensity) : 0; // random particle size in explosion uint32_t explosionsize = 8 + (PartSys->maxXpixel >> 2) + (PartSys->sources[0].source.x >> (PS_P_RADIUS_SHIFT_1D - 1)); explosionsize += hw_random16((explosionsize * SEGMENT.intensity) >> 8); PartSys->setColorByAge(false); // disable PartSys->setColorByPosition(false); // disable for (uint32_t e = 0; e < explosionsize; e++) { // emit explosion particles int idx = PartSys->sprayEmit(PartSys->sources[0]); // emit a particle if(SEGMENT.custom3 > 23) { if(SEGMENT.custom3 == 31) { // highest slider value PartSys->setColorByAge(SEGMENT.check1); // color by age if colorful mode is enabled PartSys->setColorByPosition(!SEGMENT.check1); // color by position otherwise } else { // if custom3 is set to high value (but not highest), set particle color by initial speed PartSys->particles[idx].hue = map(abs(PartSys->particles[idx].vx), 0, PartSys->sources[0].var, 0, 16 + hw_random16(200)); // set hue according to speed, use random amount of palette width PartSys->particles[idx].hue += PartSys->sources[0].source.hue; // add hue offset of the rocket (random starting color) } } else { if (SEGMENT.check1) // colorful mode PartSys->sources[0].source.hue = hw_random16(); //random color for each particle } } } } if ((SEGMENT.call & 0x01) == 0 && PartSys->sources[0].sourceFlags.custom1 == false) // every second frame and not in standby PartSys->sprayEmit(PartSys->sources[0]); // emit exhaust particle if ((SEGMENT.call & 0x03) == 0) // every fourth frame PartSys->applyFriction(1); // apply friction to all particles PartSys->update(); // update and render for (uint32_t i = 0; i < PartSys->usedParticles; i++) { if (PartSys->particles[i].ttl > 20) PartSys->particles[i].ttl -= 20; //ttl is linked to brightness, this allows to use higher brightness but still a short spark lifespan else PartSys->particles[i].ttl = 0; } } static const char _data_FX_MODE_PS_FIREWORKS1D[] PROGMEM = "PS Fireworks 1D@Gravity,Explosion,Firing side,Blur,Color,Colorful,Trail,Smooth;,!;!;1;c2=30,o1=1"; /* Particle based Sparkle effect Uses palette for particle color by DedeHai (Damian Schneider) */ void mode_particleSparkler(void) { ParticleSystem1D *PartSys = nullptr; uint32_t numSparklers; PSsettings1D sparklersettings; sparklersettings.asByte = 0; // PS settings for sparkler (set below) if (SEGMENT.call == 0) { // initialization if (!initParticleSystem1D(PartSys, 16, 128 ,0, true)) // init, no additional data needed FX_FALLBACK_STATIC; // allocation failed or is single pixel } else PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS if (PartSys == nullptr) FX_FALLBACK_STATIC; // something went wrong, no data! // Particle System settings PartSys->updateSystem(); // update system properties (dimensions and data pointers) sparklersettings.wrap = !SEGMENT.check2; sparklersettings.bounce = SEGMENT.check2; // note: bounce always takes priority over wrap numSparklers = PartSys->numSources; PartSys->setMotionBlur(SEGMENT.custom2); // anable motion blur/overlay //PartSys->setSmearBlur(SEGMENT.custom2); // anable smearing blur PartSys->setParticleSize( SEGMENT.check3 ? 60 : 0); // single pixel or large particle rendering for (uint32_t i = 0; i < numSparklers; i++) { PartSys->sources[i].source.hue = hw_random16(); PartSys->sources[i].var = 0; // sparks stationary PartSys->sources[i].minLife = 150 + SEGMENT.intensity; PartSys->sources[i].maxLife = 250 + (SEGMENT.intensity << 1); int32_t speed = SEGMENT.speed >> 1; if (SEGMENT.check1) // sparks move (slide option) PartSys->sources[i].var = SEGMENT.intensity >> 3; PartSys->sources[i].source.vx = PartSys->sources[i].source.vx > 0 ? speed : -speed; // update speed, do not change direction PartSys->sources[i].source.ttl = 400; // replenish its life (setting it perpetual uses more code) PartSys->sources[i].sat = SEGMENT.custom1; // color saturation if (SEGMENT.speed == 255) // random position at highest speed setting PartSys->sources[i].source.x = hw_random16(PartSys->maxX); else PartSys->particleMoveUpdate(PartSys->sources[i].source, PartSys->sources[i].sourceFlags, &sparklersettings); //move sparkler } numSparklers = min(1 + (SEGMENT.custom3 >> 1), (int)numSparklers); // set used sparklers, 1 to 16 if (SEGENV.aux0 != SEGMENT.custom3) { //number of used sparklers changed, redistribute for (uint32_t i = 1; i < numSparklers; i++) { PartSys->sources[i].source.x = (PartSys->sources[0].source.x + (PartSys->maxX / numSparklers) * i ) % PartSys->maxX; //distribute evenly } } SEGENV.aux0 = SEGMENT.custom3; for (uint32_t i = 0; i < numSparklers; i++) { if (hw_random() % (((271 - SEGMENT.intensity) >> 4)) == 0) PartSys->sprayEmit(PartSys->sources[i]); //emit a particle } PartSys->update(); // update and render for (uint32_t i = 0; i < PartSys->usedParticles; i++) { if (PartSys->particles[i].ttl > (64 - (SEGMENT.intensity >> 2))) PartSys->particles[i].ttl -= (64 - (SEGMENT.intensity >> 2)); //ttl is linked to brightness, this allows to use higher brightness but still a short spark lifespan else PartSys->particles[i].ttl = 0; } } static const char _data_FX_MODE_PS_SPARKLER[] PROGMEM = "PS Sparkler@Move,!,Saturation,Blur,Sparklers,Slide,Bounce,Large;,!;!;1;pal=0,sx=255,c1=0,c2=0,c3=6"; /* Particle based Hourglass, particles falling at defined intervals Uses palette for particle color by DedeHai (Damian Schneider) */ void mode_particleHourglass(void) { ParticleSystem1D *PartSys = nullptr; constexpr int positionOffset = PS_P_RADIUS_1D / 2;; // resting position offset bool* direction; uint32_t* settingTracker; if (SEGMENT.call == 0) { // initialization if (!initParticleSystem1D(PartSys, 0, 255, 8, false)) // init FX_FALLBACK_STATIC; // allocation failed or is single pixel PartSys->setBounce(true); PartSys->setWallHardness(100); } else PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS if (PartSys == nullptr) FX_FALLBACK_STATIC; // something went wrong, no data! // Particle System settings PartSys->updateSystem(); // update system properties (dimensions and data pointers) settingTracker = reinterpret_cast(PartSys->PSdataEnd); //assign data pointer direction = reinterpret_cast(PartSys->PSdataEnd + 4); //assign data pointer PartSys->setUsedParticles(1 + ((SEGMENT.intensity * 255) >> 8)); PartSys->setMotionBlur(SEGMENT.custom2); // anable motion blur PartSys->setGravity(map(SEGMENT.custom3, 0, 31, 1, 30)); PartSys->enableParticleCollisions(true, 64); // hardness value (found by experimentation on different settings) uint32_t colormode = SEGMENT.custom1 >> 5; // 0-7 if (SEGMENT.intensity != *settingTracker) { // initialize *settingTracker = SEGMENT.intensity; for (uint32_t i = 0; i < PartSys->usedParticles; i++) { PartSys->particleFlags[i].reversegrav = true; // resting particles dont fall *direction = 0; // down SEGENV.aux1 = 1; // initialize below } SEGENV.aux0 = PartSys->usedParticles - 1; // initial state, start with highest number particle } // re-order particles in case heavy collisions flipped particles (highest number index particle is on the "bottom") for (uint32_t i = 0; i < PartSys->usedParticles - 1; i++) { if (PartSys->particles[i].x < PartSys->particles[i+1].x && PartSys->particleFlags[i].fixed == false && PartSys->particleFlags[i+1].fixed == false) { std::swap(PartSys->particles[i].x, PartSys->particles[i+1].x); } } // calculate target position depending on direction auto calcTargetPos = [&](size_t i) { return PartSys->particleFlags[i].reversegrav ? PartSys->maxX - i * PS_P_RADIUS_1D - positionOffset : (PartSys->usedParticles - i) * PS_P_RADIUS_1D - positionOffset; }; for (uint32_t i = 0; i < PartSys->usedParticles; i++) { // check if particle reached target position after falling if (PartSys->particleFlags[i].fixed == false && abs(PartSys->particles[i].vx) < 5) { int32_t targetposition = calcTargetPos(i); bool belowtarget = PartSys->particleFlags[i].reversegrav ? (PartSys->particles[i].x > targetposition) : (PartSys->particles[i].x < targetposition); bool closeToTarget = abs(targetposition - PartSys->particles[i].x) < PS_P_RADIUS_1D; if (belowtarget || closeToTarget) { // overshot target or close to target and slow speed PartSys->particles[i].x = targetposition; // set exact position PartSys->particleFlags[i].fixed = true; // pin particle } } if (colormode == 7) PartSys->setColorByPosition(true); // color fixed by position else { PartSys->setColorByPosition(false); uint8_t basehue = ((SEGMENT.custom1 & 0x1F) << 3); // use 5 LSBs to select color switch(colormode) { case 0: PartSys->particles[i].hue = 120; break; // fixed at 120, if flip is activated, this can make red and green (use palette 34) case 1: PartSys->particles[i].hue = basehue; break; // fixed selectable color case 2: // 2 colors inverleaved (same code as 3) case 3: PartSys->particles[i].hue = ((SEGMENT.custom1 & 0x1F) << 1) + (i % 3)*74; break; // 3 interleved colors case 4: PartSys->particles[i].hue = basehue + (i * 255) / PartSys->usedParticles; break; // gradient palette colors case 5: PartSys->particles[i].hue = basehue + (i * 1024) / PartSys->usedParticles; break; // multi gradient palette colors case 6: PartSys->particles[i].hue = i + (strip.now >> 3); break; // disco! moving color gradient default: break; // use color by position } } if (SEGMENT.check1 && !PartSys->particleFlags[i].reversegrav) // flip color when fallen PartSys->particles[i].hue += 120; } if (SEGENV.aux1 == 1) { // last countdown call before dropping starts, reset all particles for (uint32_t i = 0; i < PartSys->usedParticles; i++) { PartSys->particleFlags[i].collide = true; PartSys->particleFlags[i].perpetual = true; PartSys->particles[i].ttl = 260; PartSys->particles[i].x = calcTargetPos(i); PartSys->particleFlags[i].fixed = true; } } if (SEGENV.aux1 == 0) { // countdown passed, run if (strip.now >= SEGENV.step) { // drop a particle // set next drop time if (SEGMENT.check3 && *direction) // fast reset SEGENV.step = strip.now + 100; // drop one particle every 100ms else // normal interval SEGENV.step = strip.now + max(100, SEGMENT.speed * 100); // map speed slider from 0.1s to 25.5s if (SEGENV.aux0 < PartSys->usedParticles) { PartSys->particleFlags[SEGENV.aux0].reversegrav = *direction; // let this particle fall or rise PartSys->particleFlags[SEGENV.aux0].fixed = false; // unpin } else { // overflow *direction = !(*direction); // flip direction SEGENV.aux1 = (SEGMENT.check2) * SEGMENT.vLength() + 100; // set restart countdown, make it short if auto start is unchecked } if (*direction == 0) // down, start dropping the highest number particle SEGENV.aux0--; // next particle else SEGENV.aux0++; } } else if (SEGMENT.check2) // auto start/reset SEGENV.aux1--; // countdown PartSys->update(); // update and render } static const char _data_FX_MODE_PS_HOURGLASS[] PROGMEM = "PS Hourglass@Interval,!,Color,Blur,Gravity,Colorflip,Start,Fast Reset;,!;!;1;pal=34,sx=5,ix=200,c1=140,c2=80,c3=4,o1=1,o2=1,o3=1"; /* Particle based Spray effect (like a volcano, possible replacement for popcorn) Uses palette for particle color by DedeHai (Damian Schneider) */ void mode_particle1Dspray(void) { ParticleSystem1D *PartSys = nullptr; if (SEGMENT.call == 0) { // initialization if (!initParticleSystem1D(PartSys, 1)) FX_FALLBACK_STATIC; // allocation failed or is single pixel PartSys->setKillOutOfBounds(true); PartSys->setWallHardness(150); PartSys->setParticleSize(1); } else PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS if (PartSys == nullptr) FX_FALLBACK_STATIC; // something went wrong, no data! // Particle System settings PartSys->updateSystem(); // update system properties (dimensions and data pointers) PartSys->setBounce(SEGMENT.check2); PartSys->setMotionBlur(SEGMENT.custom2); // anable motion blur int32_t gravity = -((int32_t)SEGMENT.custom3 - 16); // gravity setting, 0-15 is positive (down), 17 - 31 is negative (up) PartSys->setGravity(abs(gravity)); // use reversgrav setting to invert gravity (for proper 'floor' and out of bounce handling) PartSys->sources[0].source.hue = SEGMENT.aux0; // hw_random16(); PartSys->sources[0].var = 20; PartSys->sources[0].minLife = 200; PartSys->sources[0].maxLife = 400; PartSys->sources[0].source.x = map(SEGMENT.custom1, 0 , 255, 0, PartSys->maxX); // spray position PartSys->sources[0].v = map(SEGMENT.speed, 0 , 255, -127 + PartSys->sources[0].var, 127 - PartSys->sources[0].var); // particle emit speed PartSys->sources[0].sourceFlags.reversegrav = gravity < 0 ? true : false; if (hw_random() % (1 + ((255 - SEGMENT.intensity) >> 3)) == 0) { PartSys->sprayEmit(PartSys->sources[0]); // emit a particle SEGMENT.aux0++; // increment hue } //update color settings PartSys->setColorByAge(SEGMENT.check1); // overruled by 'color by position' PartSys->setColorByPosition(SEGMENT.check3); for (uint i = 0; i < PartSys->usedParticles; i++) { PartSys->particleFlags[i].reversegrav = PartSys->sources[0].sourceFlags.reversegrav; // update gravity direction } PartSys->update(); // update and render } static const char _data_FX_MODE_PS_1DSPRAY[] PROGMEM = "PS Spray 1D@Speed(+/-),!,Position,Blur,Gravity(+/-),AgeColor,Bounce,Position Color;,!;!;1;sx=200,ix=220,c1=0,c2=0"; /* Particle based balance: particles move back and forth (1D pendent to 2D particle box) Uses palette for particle color by DedeHai (Damian Schneider) */ void mode_particleBalance(void) { ParticleSystem1D *PartSys = nullptr; uint32_t i; if (SEGMENT.call == 0) { // initialization if (!initParticleSystem1D(PartSys, 1, 128)) // init, no additional data needed, use half of max particles FX_FALLBACK_STATIC; // allocation failed or is single pixel PartSys->setParticleSize(1); } else PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS if (PartSys == nullptr) FX_FALLBACK_STATIC; // something went wrong, no data! // Particle System settings PartSys->updateSystem(); // update system properties (dimensions and data pointers) PartSys->setMotionBlur(SEGMENT.custom2); // enable motion blur PartSys->setBounce(!SEGMENT.check2); PartSys->setWrap(SEGMENT.check2); uint8_t hardness = SEGMENT.custom1 > 0 ? map(SEGMENT.custom1, 0, 255, 50, 250) : 200; // set hardness, make the walls hard if collisions are disabled PartSys->enableParticleCollisions(SEGMENT.custom1, hardness); // enable collisions if custom1 > 0 PartSys->setWallHardness(200); PartSys->setUsedParticles(map(SEGMENT.intensity, 0, 255, 10, 255)); if (PartSys->usedParticles > SEGENV.aux1) { // more particles, reinitialize for (i = 0; i < PartSys->usedParticles; i++) { PartSys->particles[i].x = i * PS_P_RADIUS_1D; PartSys->particles[i].ttl = 300; PartSys->particleFlags[i].perpetual = true; PartSys->particleFlags[i].collide = true; } } SEGENV.aux1 = PartSys->usedParticles; // re-order particles in case collisions flipped particles for (i = 0; i < PartSys->usedParticles - 1; i++) { if (PartSys->particles[i].x > PartSys->particles[i+1].x) { if (SEGMENT.check2) { // check for wrap around if (PartSys->particles[i].x - PartSys->particles[i+1].x > 3 * PS_P_RADIUS_1D) continue; } std::swap(PartSys->particles[i].x, PartSys->particles[i+1].x); } } if (SEGMENT.call % (((255 - SEGMENT.speed) >> 6) + 1) == 0) { // how often the force is applied depends on speed setting int32_t xgravity; int32_t increment = (SEGMENT.speed >> 6) + 1; SEGENV.aux0 += increment; if (SEGMENT.check3) // random, use perlin noise xgravity = ((int16_t)perlin8(SEGENV.aux0) - 128); else // sinusoidal xgravity = (int16_t)cos8_t(SEGENV.aux0) - 128;//((int32_t)(SEGMENT.custom3 << 2) * cos8(SEGENV.aux0) // scale the force xgravity = (xgravity * ((SEGMENT.custom3+1) << 2)) / 128; // xgravity: -127 to +127 PartSys->applyForce(xgravity); } uint32_t randomindex = hw_random16(PartSys->usedParticles); PartSys->particles[randomindex].vx = ((int32_t)PartSys->particles[randomindex].vx * 200) / 255; // apply friction to random particle to reduce clumping //if (SEGMENT.check2 && (SEGMENT.call & 0x07) == 0) // no walls, apply friction to smooth things out if ((SEGMENT.call & 0x0F) == 0 && SEGMENT.custom3 > 4) // apply friction every 16th frame to smooth things out (except for low tilt) PartSys->applyFriction(1); // apply friction to all particles //update colors PartSys->setColorByPosition(SEGMENT.check1); if (!SEGMENT.check1) { for (i = 0; i < PartSys->usedParticles; i++) { PartSys->particles[i].hue = (1024 * i) / PartSys->usedParticles; // color by particle index } } PartSys->update(); // update and render } static const char _data_FX_MODE_PS_BALANCE[] PROGMEM = "PS 1D Balance@!,!,Hardness,Blur,Tilt,Position Color,Wrap,Random;,!;!;1;pal=18,c2=0,c3=4,o1=1"; /* Particle based Chase effect Uses palette for particle color by DedeHai (Damian Schneider) */ void mode_particleChase(void) { ParticleSystem1D *PartSys = nullptr; if (SEGMENT.call == 0) { // initialization if (!initParticleSystem1D(PartSys, 1, 191, 2, true)) // init FX_FALLBACK_STATIC; // allocation failed or is single pixel SEGENV.aux0 = 0xFFFF; // invalidate *PartSys->PSdataEnd = 1; // huedir *(PartSys->PSdataEnd + 1) = 1; // sizedir } else PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS if (PartSys == nullptr) FX_FALLBACK_STATIC; // something went wrong, no data! // Particle System settings PartSys->updateSystem(); // update system properties (dimensions and data pointers) PartSys->setColorByPosition(SEGMENT.check3); PartSys->setMotionBlur(7 + ((SEGMENT.custom3) << 3)); // anable motion blur uint32_t numParticles = 1 + map(SEGMENT.intensity, 0, 255, 0, PartSys->usedParticles / (1 + (SEGMENT.custom1 >> 5))); // depends on intensity and particle size (custom1), minimum 1 numParticles = min(numParticles, PartSys->usedParticles); // limit to available particles int32_t huestep = 1 + ((((uint32_t)SEGMENT.custom2 << 19) / numParticles) >> 16); // hue increment uint32_t settingssum = SEGMENT.speed + SEGMENT.intensity + SEGMENT.custom1 + SEGMENT.custom2 + SEGMENT.check1 + SEGMENT.check2 + SEGMENT.check3; if (SEGENV.aux0 != settingssum) { // settings changed changed, update if (SEGMENT.check1) SEGENV.step = PartSys->advPartProps[0].size / 2 + (PartSys->maxX / numParticles); else { SEGENV.step = (PartSys->maxX + (PS_P_RADIUS_1D << 6)) / numParticles; // spacing between particles SEGENV.step = (SEGENV.step / PS_P_RADIUS_1D) * PS_P_RADIUS_1D; // round down to nearest multiple of particle subpixel unit to align to pixel grid (makes them move in union) } for (int32_t i = 0; i < (int32_t)PartSys->usedParticles; i++) { PartSys->advPartProps[i].sat = 255; PartSys->particles[i].x = (i - 1) * SEGENV.step; // distribute evenly (starts out of frame for i=0) PartSys->particles[i].vx = SEGMENT.speed >> 2; PartSys->advPartProps[i].size = SEGMENT.custom1; if (SEGMENT.custom2 < 255) PartSys->particles[i].hue = i * huestep; // gradient distribution else PartSys->particles[i].hue = hw_random16(); } SEGENV.aux0 = settingssum; } if(SEGMENT.check1) { huestep = 1 + (max((int)huestep, 3) * ((int(sin16_t(strip.now * 3) + 32767))) >> 15); // changes gradient spread (scale hue step) } // wrap around (cannot use particle system wrap if distributing colors manually, it also wraps rendering which does not look good) for (int32_t i = (int32_t)PartSys->usedParticles - 1; i >= 0; i--) { // check from the back, last particle wraps first, multiple particles can overrun per frame if (PartSys->particles[i].x > PartSys->maxX + PS_P_RADIUS_1D + PartSys->advPartProps[i].size) { // wrap it around uint32_t nextindex = (i + 1) % PartSys->usedParticles; PartSys->particles[i].x = PartSys->particles[nextindex].x - (int)SEGENV.step; if(SEGMENT.check1) // playful mode, vary size PartSys->advPartProps[i].size = max(1 + (SEGMENT.custom1 >> 1), ((int(sin16_t(strip.now << 1) + 32767)) >> 8)); // cycle size if (SEGMENT.custom2 < 255) PartSys->particles[i].hue = PartSys->particles[nextindex].hue - huestep; else PartSys->particles[i].hue = hw_random16(); } PartSys->particles[i].ttl = 300; // reset ttl, cannot use perpetual because memmanager can change pointer at any time } if (SEGMENT.check1) { // playful mode, changes hue, size, speed, density dynamically int8_t* huedir = reinterpret_cast(PartSys->PSdataEnd); //assign data pointer int8_t* stepdir = reinterpret_cast(PartSys->PSdataEnd + 1); if(*stepdir == 0) *stepdir = 1; // initialize directions if(*huedir == 0) *huedir = 1; if (SEGENV.step >= (PartSys->advPartProps[0].size + PS_P_RADIUS_1D * 4) + PartSys->maxX / numParticles) *stepdir = -1; // increase density (decrease space between particles) else if (SEGENV.step <= (PartSys->advPartProps[0].size >> 1) + ((PartSys->maxX / numParticles))) *stepdir = 1; // decrease density if (SEGENV.aux1 > 512) *huedir = -1; else if (SEGENV.aux1 < 50) *huedir = 1; if (SEGMENT.call % (1024 / (1 + (SEGMENT.speed >> 2))) == 0) SEGENV.aux1 += *huedir; int8_t globalhuestep = 0; // global hue increment if (SEGMENT.call % (1 + (int(sin16_t(strip.now) + 32767) >> 12)) == 0) globalhuestep = 2; // global hue change to add some color variation if ((SEGMENT.call & 0x1F) == 0) SEGENV.step += *stepdir; // change density for(uint32_t i = 0; i < PartSys->usedParticles; i++) { PartSys->particles[i].hue -= globalhuestep; // shift global hue (both directions) PartSys->particles[i].vx = 1 + (SEGMENT.speed >> 2) + ((int32_t(sin16_t(strip.now >> 1) + 32767) * (SEGMENT.speed >> 2)) >> 16); } } PartSys->update(); // update and render } static const char _data_FX_MODE_PS_CHASE[] PROGMEM = "PS Chase@!,Density,Size,Hue,Blur,Playful,,Position Color;,!;!;1;pal=11,sx=50,c2=5,c3=0"; /* Particle Fireworks Starburst replacement (smoother rendering, more settings) Uses palette for particle color by DedeHai (Damian Schneider) */ void mode_particleStarburst(void) { ParticleSystem1D *PartSys = nullptr; if (SEGMENT.call == 0) { // initialization if (!initParticleSystem1D(PartSys, 1, 200, 0, true)) // init FX_FALLBACK_STATIC; // allocation failed or is single pixel PartSys->setKillOutOfBounds(true); PartSys->enableParticleCollisions(true, 200); PartSys->sources[0].source.ttl = 1; // set initial standby time PartSys->sources[0].sat = 0; // emitted particles start out white } else PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS if (PartSys == nullptr) FX_FALLBACK_STATIC; // something went wrong, no data! // Particle System settings PartSys->updateSystem(); // update system properties (dimensions and data pointers) PartSys->setMotionBlur(SEGMENT.custom2); // anable motion blur PartSys->setGravity(SEGMENT.check1 * 8); // enable gravity if (PartSys->sources[0].source.ttl-- == 0) { // stanby time elapsed TODO: make it a timer? uint32_t explosionsize = 4 + hw_random16(SEGMENT.intensity >> 2); PartSys->sources[0].source.hue = hw_random16(); PartSys->sources[0].var = 10 + (explosionsize << 1); PartSys->sources[0].minLife = 150; PartSys->sources[0].maxLife = 300; PartSys->sources[0].source.x = hw_random(PartSys->maxX); //random explosion position PartSys->sources[0].source.ttl = 10 + hw_random16(255 - SEGMENT.speed); PartSys->sources[0].size = SEGMENT.custom1; // Fragment size PartSys->sources[0].sourceFlags.collide = SEGMENT.check3; for (uint32_t e = 0; e < explosionsize; e++) { // emit particles if (SEGMENT.check2) PartSys->sources[0].source.hue = hw_random16(); //random color for each particle PartSys->sprayEmit(PartSys->sources[0]); //emit a particle } } //shrink all particles for (uint32_t i = 0; i < PartSys->usedParticles; i++) { if (PartSys->advPartProps[i].size) PartSys->advPartProps[i].size --; if (PartSys->advPartProps[i].sat < 250) PartSys->advPartProps[i].sat += 2 + (SEGMENT.custom3 >> 3); } if (SEGMENT.call % 5 == 0) { PartSys->applyFriction(1); //slow down particles } PartSys->update(); // update and render } static const char _data_FX_MODE_PS_STARBURST[] PROGMEM = "PS Starburst@Chance,Fragments,Size,Blur,Cooling,Gravity,Colorful,Push;,!;!;1;pal=52,sx=150,ix=150,c1=120,c2=0,c3=21"; /* Particle based 1D GEQ effect, each frequency bin gets an emitter, distributed over the strip Uses palette for particle color by DedeHai (Damian Schneider) */ void mode_particle1DGEQ(void) { ParticleSystem1D *PartSys = nullptr; uint32_t numSources; uint32_t i; if (SEGMENT.call == 0) { // initialization if (!initParticleSystem1D(PartSys, 16, 255, 0, true)) // init, no additional data needed FX_FALLBACK_STATIC; // allocation failed or is single pixel } else PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS if (PartSys == nullptr) FX_FALLBACK_STATIC; // something went wrong, no data! // Particle System settings PartSys->updateSystem(); // update system properties (dimensions and data pointers) numSources = PartSys->numSources; PartSys->setMotionBlur(SEGMENT.custom2); // anable motion blur uint32_t spacing = PartSys->maxX / numSources; for (i = 0; i < numSources; i++) { PartSys->sources[i].source.hue = i * 16; // hw_random16(); //TODO: make adjustable, maybe even colorcycle? PartSys->sources[i].var = SEGMENT.speed >> 2; PartSys->sources[i].minLife = 180 + (SEGMENT.intensity >> 1); PartSys->sources[i].maxLife = 240 + SEGMENT.intensity; PartSys->sources[i].sat = 255; PartSys->sources[i].size = SEGMENT.custom1; PartSys->sources[i].source.x = (spacing >> 1) + spacing * i; //distribute evenly } for (i = 0; i < PartSys->usedParticles; i++) { if (PartSys->particles[i].ttl > 20) PartSys->particles[i].ttl -= 20; //ttl is linked to brightness, this allows to use higher brightness but still a short lifespan else PartSys->particles[i].ttl = 0; } um_data_t *um_data = getAudioData(); uint8_t *fftResult = (uint8_t *)um_data->u_data[2]; // 16 bins with FFT data, log mapped already, each band contains frequency amplitude 0-255 //map the bands into 16 positions on x axis, emit some particles according to frequency loudness i = 0; uint32_t bin = hw_random16(numSources); //current bin , start with random one to distribute available particles fairly uint32_t threshold = 300 - SEGMENT.intensity; for (i = 0; i < numSources; i++) { bin++; bin = bin % numSources; uint32_t emitparticle = 0; // uint8_t emitspeed = ((uint32_t)fftResult[bin] * (uint32_t)SEGMENT.speed) >> 10; // emit speed according to loudness of band (127 max!) if (fftResult[bin] > threshold) { emitparticle = 1; } else if (fftResult[bin] > 0) { // band has low volue uint32_t restvolume = ((threshold - fftResult[bin]) >> 2) + 2; if (hw_random() % restvolume == 0) { emitparticle = 1; } } if (emitparticle) PartSys->sprayEmit(PartSys->sources[bin]); } //TODO: add color control? PartSys->update(); // update and render } static const char _data_FX_MODE_PS_1D_GEQ[] PROGMEM = "PS GEQ 1D@Speed,!,Size,Blur,,,,;,!;!;1f;pal=0,sx=50,ix=200,c1=0,c2=0,c3=0,o1=1,o2=1"; /* Particle based Fire effect Uses palette for particle color by DedeHai (Damian Schneider) */ void mode_particleFire1D(void) { ParticleSystem1D *PartSys = nullptr; if (SEGMENT.call == 0) { // initialization if (!initParticleSystem1D(PartSys, 5)) // init FX_FALLBACK_STATIC; // allocation failed or is single pixel PartSys->setKillOutOfBounds(true); PartSys->setParticleSize(1); } else PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS if (PartSys == nullptr) FX_FALLBACK_STATIC; // something went wrong, no data! // Particle System settings PartSys->updateSystem(); // update system properties (dimensions and data pointers) PartSys->setMotionBlur(128 + (SEGMENT.custom2 >> 1)); // enable motion blur PartSys->setColorByAge(true); uint32_t emitparticles = 1; uint32_t j = hw_random16(); for (uint i = 0; i < 3; i++) { // 3 base flames if (PartSys->sources[i].source.ttl > 50) PartSys->sources[i].source.ttl -= 10; // TODO: in 2D making the source fade out slow results in much smoother flames, need to check if it can be done the same else PartSys->sources[i].source.ttl = 100 + hw_random16(200); } for (uint i = 0; i < PartSys->numSources; i++) { j = (j + 1) % PartSys->numSources; PartSys->sources[j].source.x = 0; PartSys->sources[j].var = 2 + (SEGMENT.speed >> 4); // base flames if (j > 2) { PartSys->sources[j].minLife = 150 + SEGMENT.intensity + (j << 2); // TODO: in 2D, min life is maxlife/2 and that looks very nice PartSys->sources[j].maxLife = 200 + SEGMENT.intensity + (j << 3); PartSys->sources[j].v = (SEGMENT.speed >> (2 + (j << 1))); if (emitparticles) { emitparticles--; PartSys->sprayEmit(PartSys->sources[j]); // emit a particle } } else { PartSys->sources[j].minLife = PartSys->sources[j].source.ttl + SEGMENT.intensity; PartSys->sources[j].maxLife = PartSys->sources[j].minLife + 50; PartSys->sources[j].v = SEGMENT.speed >> 2; if (SEGENV.call & 0x01) // every second frame PartSys->sprayEmit(PartSys->sources[j]); // emit a particle } } for (uint i = 0; i < PartSys->usedParticles; i++) { PartSys->particles[i].x += PartSys->particles[i].ttl >> 7; // 'hot' particles are faster, apply some extra velocity if (PartSys->particles[i].ttl > 3 + ((255 - SEGMENT.custom1) >> 1)) PartSys->particles[i].ttl -= map(SEGMENT.custom1, 0, 255, 1, 3); // age faster } PartSys->update(); // update and render } static const char _data_FX_MODE_PS_FIRE1D[] PROGMEM = "PS Fire 1D@!,!,Cooling,Blur;,!;!;1;pal=35,sx=100,ix=50,c1=80,c2=100,c3=28,o1=1,o2=1"; /* Particle based AR effect, swoop particles along the strip with selected frequency loudness by DedeHai (Damian Schneider) */ void mode_particle1DsonicStream(void) { ParticleSystem1D *PartSys = nullptr; if (SEGMENT.call == 0) { // initialization if (!initParticleSystem1D(PartSys, 1, 255, 0, true)) // init, no additional data needed FX_FALLBACK_STATIC; // allocation failed or is single pixel PartSys->setKillOutOfBounds(true); PartSys->sources[0].source.x = 0; // at start //PartSys->sources[1].source.x = PartSys->maxX; // at end PartSys->sources[0].var = 0;//SEGMENT.custom1 >> 3; } else PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS if (PartSys == nullptr) FX_FALLBACK_STATIC; // something went wrong, no data! // Particle System settings PartSys->updateSystem(); // update system properties (dimensions and data pointers) PartSys->setMotionBlur(20 + (SEGMENT.custom2 >> 1)); // anable motion blur PartSys->setSmearBlur(200); // smooth out the edges PartSys->sources[0].v = 5 + (SEGMENT.speed >> 2); // FFT processing um_data_t *um_data = getAudioData(); uint8_t *fftResult = (uint8_t *)um_data->u_data[2]; // 16 bins with FFT data, log mapped already, each band contains frequency amplitude 0-255 uint32_t loudness; uint32_t baseBin = SEGMENT.custom3 >> 1; // 0 - 15 map(SEGMENT.custom3, 0, 31, 0, 14); loudness = fftResult[baseBin];// + fftResult[baseBin + 1]; if (baseBin > 12) loudness = loudness << 2; // double loudness for high frequencies (better detecion) uint32_t threshold = 140 - (SEGMENT.intensity >> 1); if (SEGMENT.check2) { // enable low pass filter for dynamic threshold SEGMENT.step = (SEGMENT.step * 31500 + loudness * (32768 - 31500)) >> 15; // low pass filter for simple beat detection: add average to base threshold threshold = 20 + (threshold >> 1) + SEGMENT.step; // add average to threshold } // color uint32_t hueincrement = (SEGMENT.custom1 >> 3); // 0-31 PartSys->sources[0].sat = SEGMENT.custom1 > 0 ? 255 : 0; // color slider at zero: set to white PartSys->setColorByPosition(SEGMENT.custom1 == 255); // particle manipulation for (uint32_t i = 0; i < PartSys->usedParticles; i++) { if (PartSys->sources[0].sourceFlags.perpetual == false) { // age faster if not perpetual if (PartSys->particles[i].ttl > 2) { PartSys->particles[i].ttl -= 2; //ttl is linked to brightness, this allows to use higher brightness but still a short lifespan } else PartSys->particles[i].ttl = 0; } if (SEGMENT.check1) { // modulate colors by mid frequencies int mids = sqrt32_bw((int)fftResult[5] + (int)fftResult[6] + (int)fftResult[7] + (int)fftResult[8] + (int)fftResult[9] + (int)fftResult[10]); // average the mids, bin 5 is ~500Hz, bin 10 is ~2kHz (see audio_reactive.h) PartSys->particles[i].hue += (mids * perlin8(PartSys->particles[i].x << 2, SEGMENT.step << 2)) >> 9; // color by perlin noise from mid frequencies } } if (loudness > threshold) { SEGMENT.aux0 += hueincrement; // change color PartSys->sources[0].minLife = 100 + (((unsigned)SEGMENT.intensity * loudness * loudness) >> 13); PartSys->sources[0].maxLife = PartSys->sources[0].minLife; PartSys->sources[0].source.hue = SEGMENT.aux0; PartSys->sources[0].size = SEGMENT.speed; if (PartSys->particles[SEGMENT.aux1].x > 3 * PS_P_RADIUS_1D || PartSys->particles[SEGMENT.aux1].ttl == 0) { // only emit if last particle is far enough away or dead int partindex = PartSys->sprayEmit(PartSys->sources[0]); // emit a particle if (partindex >= 0) SEGMENT.aux1 = partindex; // track last emitted particle } } else loudness = 0; // required for push mode PartSys->update(); // update and render (needs to be done before manipulation for initial particle spacing to be right) if (SEGMENT.check3) { // push mode PartSys->sources[0].sourceFlags.perpetual = true; // emitted particles dont age PartSys->applyFriction(1); //slow down particles int32_t movestep = (((int)SEGMENT.speed + 2) * loudness) >> 10; if (movestep) { for (uint32_t i = 0; i < PartSys->usedParticles; i++) { if (PartSys->particles[i].ttl) { PartSys->particles[i].x += movestep; // push particles PartSys->particles[i].vx = 10 + (SEGMENT.speed >> 4) ; // give particles some speed for smooth movement (friction will slow them down) } } } } else { PartSys->sources[0].sourceFlags.perpetual = false; // emitted particles age // move all particles (again) to allow faster speeds for (uint32_t i = 0; i < PartSys->usedParticles; i++) { if (PartSys->particles[i].vx == 0) PartSys->particles[i].vx = PartSys->sources[0].v; // move static particles (after disabling push mode) PartSys->particleMoveUpdate(PartSys->particles[i], PartSys->particleFlags[i], nullptr, &PartSys->advPartProps[i]); } } } static const char _data_FX_MODE_PS_SONICSTREAM[] PROGMEM = "PS Sonic Stream@!,!,Color,Blur,Bin,Mod,Filter,Push;,!;!;1f;c3=0,o2=1"; /* Particle based AR effect, creates exploding particles on beats by DedeHai (Damian Schneider) */ void mode_particle1DsonicBoom(void) { ParticleSystem1D *PartSys = nullptr; if (SEGMENT.call == 0) { // initialization if (!initParticleSystem1D(PartSys, 1, 255, 0, true)) // init, no additional data needed FX_FALLBACK_STATIC; // allocation failed or is single pixel PartSys->setKillOutOfBounds(true); } else PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS if (PartSys == nullptr) FX_FALLBACK_STATIC; // something went wrong, no data! // Particle System settings PartSys->updateSystem(); // update system properties (dimensions and data pointers) PartSys->setMotionBlur(180 * SEGMENT.check3); PartSys->setSmearBlur(64 * SEGMENT.check3); PartSys->sources[0].var = map(SEGMENT.speed, 0, 255, 10, 127); // FFT processing um_data_t *um_data = getAudioData(); uint8_t *fftResult = (uint8_t *)um_data->u_data[2]; // 16 bins with FFT data, log mapped already, each band contains frequency amplitude 0-255 uint32_t loudness; uint32_t baseBin = SEGMENT.custom3 >> 1; // 0 - 15 map(SEGMENT.custom3, 0, 31, 0, 14); loudness = fftResult[baseBin];// + fftResult[baseBin + 1]; if (baseBin > 12) loudness = loudness << 2; // double loudness for high frequencies (better detecion) uint32_t threshold = 150 - (SEGMENT.intensity >> 1); if (SEGMENT.check2) { // enable low pass filter for dynamic threshold SEGMENT.step = (SEGMENT.step * 31500 + loudness * (32768 - 31500)) >> 15; // low pass filter for simple beat detection: add average to base threshold threshold = 20 + (threshold >> 1) + SEGMENT.step; // add average to threshold } // particle manipulation for (uint32_t i = 0; i < PartSys->usedParticles; i++) { if (SEGMENT.check1) { // modulate colors by mid frequencies int mids = sqrt32_bw((int)fftResult[5] + (int)fftResult[6] + (int)fftResult[7] + (int)fftResult[8] + (int)fftResult[9] + (int)fftResult[10]); // average the mids, bin 5 is ~500Hz, bin 10 is ~2kHz (see audio_reactive.h) PartSys->particles[i].hue += (mids * perlin8(PartSys->particles[i].x << 2, SEGMENT.step << 2)) >> 9; // color by perlin noise from mid frequencies } if (PartSys->particles[i].ttl > 16) { PartSys->particles[i].ttl -= 16; //ttl is linked to brightness, this allows to use higher brightness but still a (very) short lifespan } } if (loudness > threshold) { if (SEGMENT.aux1 == 0) { // edge detected, code only runs once per "beat" // update position if (SEGMENT.custom2 < 128) // fixed position PartSys->sources[0].source.x = map(SEGMENT.custom2, 0, 127, 0, PartSys->maxX); else if (SEGMENT.custom2 < 255) { // advances on each "beat" int32_t step = PartSys->maxX / (((270 - SEGMENT.custom2) >> 3)); // step: 2 - 33 steps for full segment width PartSys->sources[0].source.x = (PartSys->sources[0].source.x + step) % PartSys->maxX; if (PartSys->sources[0].source.x < step) // align to be symmetrical by making the first position half a step from start PartSys->sources[0].source.x = step >> 1; } else // position set to max, use random postion per beat PartSys->sources[0].source.x = hw_random(PartSys->maxX); // update color //PartSys->setColorByPosition(SEGMENT.custom1 == 255); // color slider at max: particle color by position PartSys->sources[0].sat = SEGMENT.custom1 > 0 ? 255 : 0; // color slider at zero: set to white if (SEGMENT.custom1 == 255) // emit color by position SEGMENT.aux0 = map(PartSys->sources[0].source.x , 0, PartSys->maxX, 0, 255); else if (SEGMENT.custom1 > 0) SEGMENT.aux0 += (SEGMENT.custom1 >> 1); // change emit color per "beat" } SEGMENT.aux1 = 1; // track edge detection PartSys->sources[0].minLife = 200; PartSys->sources[0].maxLife = PartSys->sources[0].minLife + (((unsigned)SEGMENT.intensity * loudness * loudness) >> 13); PartSys->sources[0].source.hue = SEGMENT.aux0; uint32_t explosionsize = 4 + (PartSys->maxXpixel >> 2); explosionsize = hw_random16((explosionsize * loudness) >> 10); for (uint32_t e = 0; e < explosionsize; e++) { // emit explosion particles PartSys->sprayEmit(PartSys->sources[0]); // emit a particle } } else SEGMENT.aux1 = 0; // reset edge detection PartSys->update(); // update and render (needs to be done before manipulation for initial particle spacing to be right) } static const char _data_FX_MODE_PS_SONICBOOM[] PROGMEM = "PS Sonic Boom@!,!,Color,Position,Bin,Mod,Filter,Blur;,!;!;1f;c2=63,c3=0,o2=1"; /* Particles bound by springs by DedeHai (Damian Schneider) */ void mode_particleSpringy(void) { ParticleSystem1D *PartSys = nullptr; if (SEGMENT.call == 0) { // initialization if (!initParticleSystem1D(PartSys, 1, 128, 0, true)) // init with advanced properties (used for spring forces) FX_FALLBACK_STATIC; // allocation failed or is single pixel SEGENV.aux0 = SEGENV.aux1 = 0xFFFF; // invalidate settings } else PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS if (PartSys == nullptr) FX_FALLBACK_STATIC; // something went wrong, no data! // Particle System settings PartSys->updateSystem(); // update system properties (dimensions and data pointers) PartSys->setMotionBlur(220 * SEGMENT.check1); // anable motion blur PartSys->setSmearBlur(50); // smear a little PartSys->setUsedParticles(map(SEGMENT.custom1, 0, 255, 30 >> SEGMENT.check2, 255 >> (SEGMENT.check2*2))); // depends on density and particle size //PartSys->enableParticleCollisions(true, 140); // enable particle collisions, can not be set too hard or impulses will not strech the springs if soft. int32_t springlength = PartSys->maxX / (PartSys->usedParticles); // spring length (spacing between particles) int32_t springK = map(SEGMENT.speed, 0, 255, 5, 35); // spring constant (stiffness) uint32_t settingssum = SEGMENT.custom1 + SEGMENT.check2; PartSys->setParticleSize(SEGMENT.check2 ? 120 : 1); // large or small particles if (SEGENV.aux0 != settingssum) { // number of particles changed, update distribution for (int32_t i = 0; i < (int32_t)PartSys->usedParticles; i++) { PartSys->advPartProps[i].sat = 255; // full saturation //PartSys->particleFlags[i].collide = true; // enable collision for particles -> results in chaos, removed for now PartSys->particles[i].x = (i+1) * ((PartSys->maxX) / (PartSys->usedParticles)); // distribute //PartSys->particles[i].vx = 0; //reset speed //PartSys->advPartProps[i].size = SEGMENT.check2 ? 190 : 2; // set size, small or big -> use global size } SEGENV.aux0 = settingssum; } int dxlimit = (2 + ((255 - SEGMENT.speed) >> 5)) * springlength; // limit for spring length to avoid overstretching int springforce[PartSys->usedParticles]; // spring forces memset(springforce, 0, PartSys->usedParticles * sizeof(int32_t)); // reset spring forces // calculate spring forces and limit particle positions if (PartSys->particles[0].x < -springlength) PartSys->particles[0].x = -springlength; // limit the spring length else if (PartSys->particles[0].x > dxlimit) PartSys->particles[0].x = dxlimit; // limit the spring length springforce[0] += ((springlength >> 1) - (PartSys->particles[0].x)) * springK; // first particle anchors to x=0 for (uint32_t i = 1; i < PartSys->usedParticles; i++) { // reorder particles if they are out of order to prevent chaos if (PartSys->particles[i].x < PartSys->particles[i-1].x) std::swap(PartSys->particles[i].x, PartSys->particles[i-1].x); // swap particle positions to maintain order int dx = PartSys->particles[i].x - PartSys->particles[i-1].x; // distance, always positive if (dx > dxlimit) { // limit the spring length PartSys->particles[i].x = PartSys->particles[i-1].x + dxlimit; dx = dxlimit; } int dxleft = (springlength - dx); // offset from spring resting position springforce[i] += dxleft * springK; springforce[i-1] -= dxleft * springK; if (i == (PartSys->usedParticles - 1)) { if (PartSys->particles[i].x >= PartSys->maxX + springlength) PartSys->particles[i].x = PartSys->maxX + springlength; int dxright = (springlength >> 1) - (PartSys->maxX - PartSys->particles[i].x); // last particle anchors to x=maxX springforce[i] -= dxright * springK; } } // apply spring forces to particles bool dampenoscillations = (SEGMENT.call % (9 - (SEGMENT.speed >> 5))) == 0; // dampen oscillation if particles are slow, more damping on stiffer springs for (uint32_t i = 0; i < PartSys->usedParticles; i++) { springforce[i] = springforce[i] / 64; // scale spring force (cannot use shifts because of negative values) int maxforce = 120; // limit spring force springforce[i] = springforce[i] > maxforce ? maxforce : springforce[i] < -maxforce ? -maxforce : springforce[i]; // limit spring force PartSys->applyForce(PartSys->particles[i], springforce[i], PartSys->advPartProps[i].forcecounter); //dampen slow particles to avoid persisting oscillations on higher stiffness if (dampenoscillations) { if (abs(PartSys->particles[i].vx) < 3 && abs(springforce[i]) < (springK >> 2)) PartSys->particles[i].vx = (PartSys->particles[i].vx * 254) / 256; // take out some energy } PartSys->particles[i].ttl = 300; // reset ttl, cannot use perpetual } if (SEGMENT.call % ((65 - ((SEGMENT.intensity * (1 + (SEGMENT.speed>>3))) >> 7))) == 0) // more damping for higher stiffness PartSys->applyFriction((SEGMENT.intensity >> 2)); // add a small resetting force so particles return to resting position even under high damping for (uint32_t i = 1; i < PartSys->usedParticles - 1; i++) { int restposition = (springlength >> 1) + i * springlength; // resting position int dx = restposition - PartSys->particles[i].x; // distance, always positive PartSys->applyForce(PartSys->particles[i], dx > 0 ? 1 : (dx < 0 ? -1 : 0), PartSys->advPartProps[i].forcecounter); } // Modes if (SEGMENT.check3) { // use AR, custom 3 becomes frequency band to use, applies velocity to center particle according to loudness um_data_t *um_data = getAudioData(); uint8_t *fftResult = (uint8_t *)um_data->u_data[2]; // 16 bins with FFT data, log mapped already, each band contains frequency amplitude 0-255 uint32_t baseBin = map(SEGMENT.custom3, 0, 31, 0, 14); uint32_t loudness = fftResult[baseBin] + fftResult[baseBin+1]; uint32_t threshold = 80; //150 - (SEGMENT.intensity >> 1); if (loudness > threshold) { int offset = (PartSys->maxX >> 1) - PartSys->particles[PartSys->usedParticles>>1].x; // offset from center if (abs(offset) < PartSys->maxX >> 5) // push particle around in center sector PartSys->particles[PartSys->usedParticles>>1].vx = ((PartSys->particles[PartSys->usedParticles>>1].vx > 0 ? 1 : -1)) * (loudness >> 3); } } else{ if (SEGMENT.custom3 <= 10) { // periodic pulse: 0-5 apply at start, 6-10 apply at center if (strip.now > SEGMENT.step) { int speed = (SEGMENT.custom3 > 5) ? (SEGMENT.custom3 - 6) : SEGMENT.custom3; SEGMENT.step = strip.now + 7500 - ((SEGMENT.speed << 3) + (speed << 10)); int amplitude = 40 + (SEGMENT.custom1 >> 2); int index = (SEGMENT.custom3 > 5) ? (PartSys->usedParticles / 2) : 0; // center or start particle PartSys->particles[index].vx += amplitude; } } else if (SEGMENT.custom3 <= 30) { // sinusoidal wave: 11-20 apply at start, 21-30 apply at center int index = (SEGMENT.custom3 > 20) ? (PartSys->usedParticles / 2) : 0; // center or start particle int restposition = 0; if (index > 0) restposition = PartSys->maxX >> 1; // center //int amplitude = 5 + (SEGMENT.speed >> 3) + (SEGMENT.custom1 >> 2); // amplitude depends on density int amplitude = 5 + (SEGMENT.custom1 >> 2); // amplitude depends on density int speed = SEGMENT.custom3 - 10 - (index ? 10 : 0); // map 11-20 and 21-30 to 1-10 int phase = strip.now * ((1 + (SEGMENT.speed >> 4)) * speed); if (SEGMENT.check2) amplitude <<= 1; // double amplitude for XL particles PartSys->particles[index].x = restposition + ((sin16_t(phase) * amplitude) >> 12); // apply position } else { if (hw_random16() < 656) { // ~1% chance to add a pulse int amplitude = 60; if (SEGMENT.check2) amplitude <<= 1; // double amplitude for XL particles PartSys->particles[PartSys->usedParticles >> 1].vx += hw_random16(amplitude << 1) - amplitude; // apply acceleration } } } for (uint32_t i = 0; i < PartSys->usedParticles; i++) { if (SEGMENT.custom2 == 255) { // map speed to hue int speedclr = ((int8_t(abs(PartSys->particles[i].vx))) >> 2) << 4; // scale for greater color variation, dump small values to avoid flickering //int speed = PartSys->particles[i].vx << 2; // +/- 512 if (speedclr > 240) speedclr = 240; // limit color to non-wrapping part of palette PartSys->particles[i].hue = speedclr; } else if (SEGMENT.custom2 > 0) PartSys->particles[i].hue = i * (SEGMENT.custom2 >> 2); // gradient distribution else { // map hue to particle density int deviation; if (i == 0) // First particle: measure density based on distance to anchor point deviation = springlength/2 - PartSys->particles[i].x; else if (i == PartSys->usedParticles - 1) // Last particle: measure density based on distance to right boundary deviation = springlength/2 - (PartSys->maxX - PartSys->particles[i].x); else { // Middle particles: average of compression/expansion from both sides int leftDx = PartSys->particles[i].x - PartSys->particles[i-1].x; int rightDx = PartSys->particles[i+1].x - PartSys->particles[i].x; int avgDistance = (leftDx + rightDx) >> 1; if (avgDistance < 0) avgDistance = 0; // avoid negative distances (not sure why this happens) deviation = (springlength - avgDistance); } deviation = constrain(deviation, -127, 112); // limit deviation to -127..112 (do not go intwo wrapping part of palette) PartSys->particles[i].hue = 127 + deviation; // map density to hue } } PartSys->update(); // update and render } static const char _data_FX_MODE_PS_SPRINGY[] PROGMEM = "PS Springy@Stiffness,Damping,Density,Hue,Mode,Smear,XL,AR;,!;!;1f;pal=54,c2=0,c3=23"; #endif // WLED_DISABLE_PARTICLESYSTEM1D ////////////////////////////////////////////////////////////////////////////////////////// // mode data static const char _data_RESERVED[] PROGMEM = "RSVD"; // add (or replace reserved) effect mode and data into vector // use id==255 to find unallocated gaps (with "Reserved" data string) // if vector size() is smaller than id (single) data is appended at the end (regardless of id) // return the actual id used for the effect or 255 if the add failed. uint8_t WS2812FX::addEffect(uint8_t id, mode_ptr mode_fn, const char *mode_name) { if (id == 255) { // find empty slot for (size_t i=1; i<_mode.size(); i++) if (_modeData[i] == _data_RESERVED) { id = i; break; } } if (id < _mode.size()) { if (_modeData[id] != _data_RESERVED) return 255; // do not overwrite an already added effect _mode[id] = mode_fn; _modeData[id] = mode_name; return id; } else if (_mode.size() < 255) { // 255 is reserved for indicating the effect wasn't added _mode.push_back(mode_fn); _modeData.push_back(mode_name); if (_modeCount < _mode.size()) _modeCount++; return _mode.size() - 1; } else { return 255; // The vector is full so return 255 } } void WS2812FX::setupEffectData() { // Solid must be first! (assuming vector is empty upon call to setup) _mode.push_back(&mode_static); _modeData.push_back(_data_FX_MODE_STATIC); // fill reserved word in case there will be any gaps in the array for (size_t i=1; i<_modeCount; i++) { _mode.push_back(&mode_static); _modeData.push_back(_data_RESERVED); } // now replace all pre-allocated effects addEffect(FX_MODE_COPY, &mode_copy_segment, _data_FX_MODE_COPY); // --- 1D non-audio effects --- addEffect(FX_MODE_BLINK, &mode_blink, _data_FX_MODE_BLINK); addEffect(FX_MODE_BREATH, &mode_breath, _data_FX_MODE_BREATH); addEffect(FX_MODE_COLOR_WIPE, &mode_color_wipe, _data_FX_MODE_COLOR_WIPE); addEffect(FX_MODE_COLOR_WIPE_RANDOM, &mode_color_wipe_random, _data_FX_MODE_COLOR_WIPE_RANDOM); addEffect(FX_MODE_RANDOM_COLOR, &mode_random_color, _data_FX_MODE_RANDOM_COLOR); addEffect(FX_MODE_COLOR_SWEEP, &mode_color_sweep, _data_FX_MODE_COLOR_SWEEP); addEffect(FX_MODE_DYNAMIC, &mode_dynamic, _data_FX_MODE_DYNAMIC); addEffect(FX_MODE_RAINBOW, &mode_rainbow, _data_FX_MODE_RAINBOW); addEffect(FX_MODE_RAINBOW_CYCLE, &mode_rainbow_cycle, _data_FX_MODE_RAINBOW_CYCLE); addEffect(FX_MODE_SCAN, &mode_scan, _data_FX_MODE_SCAN); addEffect(FX_MODE_DUAL_SCAN, &mode_dual_scan, _data_FX_MODE_DUAL_SCAN); addEffect(FX_MODE_FADE, &mode_fade, _data_FX_MODE_FADE); addEffect(FX_MODE_THEATER_CHASE, &mode_theater_chase, _data_FX_MODE_THEATER_CHASE); addEffect(FX_MODE_THEATER_CHASE_RAINBOW, &mode_theater_chase_rainbow, _data_FX_MODE_THEATER_CHASE_RAINBOW); addEffect(FX_MODE_RUNNING_LIGHTS, &mode_running_lights, _data_FX_MODE_RUNNING_LIGHTS); addEffect(FX_MODE_SAW, &mode_saw, _data_FX_MODE_SAW); addEffect(FX_MODE_TWINKLE, &mode_twinkle, _data_FX_MODE_TWINKLE); addEffect(FX_MODE_DISSOLVE, &mode_dissolve, _data_FX_MODE_DISSOLVE); addEffect(FX_MODE_DISSOLVE_RANDOM, &mode_dissolve_random, _data_FX_MODE_DISSOLVE_RANDOM); addEffect(FX_MODE_FLASH_SPARKLE, &mode_flash_sparkle, _data_FX_MODE_FLASH_SPARKLE); addEffect(FX_MODE_HYPER_SPARKLE, &mode_hyper_sparkle, _data_FX_MODE_HYPER_SPARKLE); addEffect(FX_MODE_STROBE, &mode_strobe, _data_FX_MODE_STROBE); addEffect(FX_MODE_STROBE_RAINBOW, &mode_strobe_rainbow, _data_FX_MODE_STROBE_RAINBOW); addEffect(FX_MODE_MULTI_STROBE, &mode_multi_strobe, _data_FX_MODE_MULTI_STROBE); addEffect(FX_MODE_BLINK_RAINBOW, &mode_blink_rainbow, _data_FX_MODE_BLINK_RAINBOW); addEffect(FX_MODE_ANDROID, &mode_android, _data_FX_MODE_ANDROID); addEffect(FX_MODE_CHASE_COLOR, &mode_chase_color, _data_FX_MODE_CHASE_COLOR); addEffect(FX_MODE_CHASE_RANDOM, &mode_chase_random, _data_FX_MODE_CHASE_RANDOM); addEffect(FX_MODE_CHASE_RAINBOW, &mode_chase_rainbow, _data_FX_MODE_CHASE_RAINBOW); addEffect(FX_MODE_CHASE_FLASH, &mode_chase_flash, _data_FX_MODE_CHASE_FLASH); addEffect(FX_MODE_CHASE_FLASH_RANDOM, &mode_chase_flash_random, _data_FX_MODE_CHASE_FLASH_RANDOM); addEffect(FX_MODE_CHASE_RAINBOW_WHITE, &mode_chase_rainbow_white, _data_FX_MODE_CHASE_RAINBOW_WHITE); addEffect(FX_MODE_COLORFUL, &mode_colorful, _data_FX_MODE_COLORFUL); addEffect(FX_MODE_TRAFFIC_LIGHT, &mode_traffic_light, _data_FX_MODE_TRAFFIC_LIGHT); addEffect(FX_MODE_COLOR_SWEEP_RANDOM, &mode_color_sweep_random, _data_FX_MODE_COLOR_SWEEP_RANDOM); addEffect(FX_MODE_RUNNING_COLOR, &mode_running_color, _data_FX_MODE_RUNNING_COLOR); addEffect(FX_MODE_AURORA, &mode_aurora, _data_FX_MODE_AURORA); addEffect(FX_MODE_COLORCLOUDS, &mode_ColorClouds, _data_FX_MODE_COLORCLOUDS); addEffect(FX_MODE_RUNNING_RANDOM, &mode_running_random, _data_FX_MODE_RUNNING_RANDOM); addEffect(FX_MODE_LARSON_SCANNER, &mode_larson_scanner, _data_FX_MODE_LARSON_SCANNER); addEffect(FX_MODE_RAIN, &mode_rain, _data_FX_MODE_RAIN); addEffect(FX_MODE_PRIDE_2015, &mode_pride_2015, _data_FX_MODE_PRIDE_2015); addEffect(FX_MODE_COLORWAVES, &mode_colorwaves, _data_FX_MODE_COLORWAVES); addEffect(FX_MODE_FIREWORKS, &mode_fireworks, _data_FX_MODE_FIREWORKS); addEffect(FX_MODE_TETRIX, &mode_tetrix, _data_FX_MODE_TETRIX); addEffect(FX_MODE_FIRE_FLICKER, &mode_fire_flicker, _data_FX_MODE_FIRE_FLICKER); addEffect(FX_MODE_GRADIENT, &mode_gradient, _data_FX_MODE_GRADIENT); addEffect(FX_MODE_LOADING, &mode_loading, _data_FX_MODE_LOADING); addEffect(FX_MODE_FAIRY, &mode_fairy, _data_FX_MODE_FAIRY); addEffect(FX_MODE_TWO_DOTS, &mode_two_dots, _data_FX_MODE_TWO_DOTS); addEffect(FX_MODE_FAIRYTWINKLE, &mode_fairytwinkle, _data_FX_MODE_FAIRYTWINKLE); addEffect(FX_MODE_RUNNING_DUAL, &mode_running_dual, _data_FX_MODE_RUNNING_DUAL); #ifdef WLED_ENABLE_GIF addEffect(FX_MODE_IMAGE, &mode_image, _data_FX_MODE_IMAGE); #endif addEffect(FX_MODE_TRICOLOR_CHASE, &mode_tricolor_chase, _data_FX_MODE_TRICOLOR_CHASE); addEffect(FX_MODE_TRICOLOR_WIPE, &mode_tricolor_wipe, _data_FX_MODE_TRICOLOR_WIPE); addEffect(FX_MODE_TRICOLOR_FADE, &mode_tricolor_fade, _data_FX_MODE_TRICOLOR_FADE); addEffect(FX_MODE_LIGHTNING, &mode_lightning, _data_FX_MODE_LIGHTNING); addEffect(FX_MODE_ICU, &mode_icu, _data_FX_MODE_ICU); addEffect(FX_MODE_DUAL_LARSON_SCANNER, &mode_dual_larson_scanner, _data_FX_MODE_DUAL_LARSON_SCANNER); addEffect(FX_MODE_RANDOM_CHASE, &mode_random_chase, _data_FX_MODE_RANDOM_CHASE); addEffect(FX_MODE_OSCILLATE, &mode_oscillate, _data_FX_MODE_OSCILLATE); addEffect(FX_MODE_JUGGLE, &mode_juggle, _data_FX_MODE_JUGGLE); addEffect(FX_MODE_PALETTE, &mode_palette, _data_FX_MODE_PALETTE); addEffect(FX_MODE_BPM, &mode_bpm, _data_FX_MODE_BPM); addEffect(FX_MODE_FILLNOISE8, &mode_fillnoise8, _data_FX_MODE_FILLNOISE8); addEffect(FX_MODE_NOISE16_1, &mode_noise16_1, _data_FX_MODE_NOISE16_1); addEffect(FX_MODE_NOISE16_2, &mode_noise16_2, _data_FX_MODE_NOISE16_2); addEffect(FX_MODE_NOISE16_3, &mode_noise16_3, _data_FX_MODE_NOISE16_3); addEffect(FX_MODE_NOISE16_4, &mode_noise16_4, _data_FX_MODE_NOISE16_4); addEffect(FX_MODE_COLORTWINKLE, &mode_colortwinkle, _data_FX_MODE_COLORTWINKLE); addEffect(FX_MODE_LAKE, &mode_lake, _data_FX_MODE_LAKE); addEffect(FX_MODE_METEOR, &mode_meteor, _data_FX_MODE_METEOR); //addEffect(FX_MODE_METEOR_SMOOTH, &mode_meteor_smooth, _data_FX_MODE_METEOR_SMOOTH); // merged with mode_meteor addEffect(FX_MODE_RAILWAY, &mode_railway, _data_FX_MODE_RAILWAY); addEffect(FX_MODE_RIPPLE, &mode_ripple, _data_FX_MODE_RIPPLE); addEffect(FX_MODE_TWINKLEFOX, &mode_twinklefox, _data_FX_MODE_TWINKLEFOX); addEffect(FX_MODE_TWINKLECAT, &mode_twinklecat, _data_FX_MODE_TWINKLECAT); addEffect(FX_MODE_HALLOWEEN_EYES, &mode_halloween_eyes, _data_FX_MODE_HALLOWEEN_EYES); addEffect(FX_MODE_STATIC_PATTERN, &mode_static_pattern, _data_FX_MODE_STATIC_PATTERN); addEffect(FX_MODE_TRI_STATIC_PATTERN, &mode_tri_static_pattern, _data_FX_MODE_TRI_STATIC_PATTERN); addEffect(FX_MODE_SPOTS, &mode_spots, _data_FX_MODE_SPOTS); addEffect(FX_MODE_SPOTS_FADE, &mode_spots_fade, _data_FX_MODE_SPOTS_FADE); addEffect(FX_MODE_COMET, &mode_comet, _data_FX_MODE_COMET); #if defined(WLED_PS_DONT_REPLACE_1D_FX) || defined(WLED_PS_DONT_REPLACE_2D_FX) addEffect(FX_MODE_FIRE_2012, &mode_fire_2012, _data_FX_MODE_FIRE_2012); addEffect(FX_MODE_EXPLODING_FIREWORKS, &mode_exploding_fireworks, _data_FX_MODE_EXPLODING_FIREWORKS); #endif addEffect(FX_MODE_SPARKLE, &mode_sparkle, _data_FX_MODE_SPARKLE); addEffect(FX_MODE_GLITTER, &mode_glitter, _data_FX_MODE_GLITTER); addEffect(FX_MODE_SOLID_GLITTER, &mode_solid_glitter, _data_FX_MODE_SOLID_GLITTER); addEffect(FX_MODE_MULTI_COMET, &mode_multi_comet, _data_FX_MODE_MULTI_COMET); #ifdef WLED_PS_DONT_REPLACE_1D_FX addEffect(FX_MODE_ROLLINGBALLS, &mode_rolling_balls, _data_FX_MODE_ROLLINGBALLS); addEffect(FX_MODE_STARBURST, &mode_starburst, _data_FX_MODE_STARBURST); addEffect(FX_MODE_DANCING_SHADOWS, &mode_dancing_shadows, _data_FX_MODE_DANCING_SHADOWS); #endif addEffect(FX_MODE_CANDLE, &mode_candle, _data_FX_MODE_CANDLE); addEffect(FX_MODE_BOUNCINGBALLS, &mode_bouncing_balls, _data_FX_MODE_BOUNCINGBALLS); addEffect(FX_MODE_POPCORN, &mode_popcorn, _data_FX_MODE_POPCORN); addEffect(FX_MODE_DRIP, &mode_drip, _data_FX_MODE_DRIP); addEffect(FX_MODE_SINELON, &mode_sinelon, _data_FX_MODE_SINELON); addEffect(FX_MODE_SINELON_DUAL, &mode_sinelon_dual, _data_FX_MODE_SINELON_DUAL); addEffect(FX_MODE_SINELON_RAINBOW, &mode_sinelon_rainbow, _data_FX_MODE_SINELON_RAINBOW); addEffect(FX_MODE_PLASMA, &mode_plasma, _data_FX_MODE_PLASMA); addEffect(FX_MODE_PERCENT, &mode_percent, _data_FX_MODE_PERCENT); addEffect(FX_MODE_RIPPLE_RAINBOW, &mode_ripple_rainbow, _data_FX_MODE_RIPPLE_RAINBOW); addEffect(FX_MODE_HEARTBEAT, &mode_heartbeat, _data_FX_MODE_HEARTBEAT); addEffect(FX_MODE_PACIFICA, &mode_pacifica, _data_FX_MODE_PACIFICA); addEffect(FX_MODE_CANDLE_MULTI, &mode_candle_multi, _data_FX_MODE_CANDLE_MULTI); addEffect(FX_MODE_SUNRISE, &mode_sunrise, _data_FX_MODE_SUNRISE); addEffect(FX_MODE_PHASED, &mode_phased, _data_FX_MODE_PHASED); addEffect(FX_MODE_TWINKLEUP, &mode_twinkleup, _data_FX_MODE_TWINKLEUP); addEffect(FX_MODE_NOISEPAL, &mode_noisepal, _data_FX_MODE_NOISEPAL); addEffect(FX_MODE_SINEWAVE, &mode_sinewave, _data_FX_MODE_SINEWAVE); addEffect(FX_MODE_PHASEDNOISE, &mode_phased_noise, _data_FX_MODE_PHASEDNOISE); addEffect(FX_MODE_FLOW, &mode_flow, _data_FX_MODE_FLOW); addEffect(FX_MODE_CHUNCHUN, &mode_chunchun, _data_FX_MODE_CHUNCHUN); addEffect(FX_MODE_WASHING_MACHINE, &mode_washing_machine, _data_FX_MODE_WASHING_MACHINE); addEffect(FX_MODE_BLENDS, &mode_blends, _data_FX_MODE_BLENDS); addEffect(FX_MODE_TV_SIMULATOR, &mode_tv_simulator, _data_FX_MODE_TV_SIMULATOR); addEffect(FX_MODE_DYNAMIC_SMOOTH, &mode_dynamic_smooth, _data_FX_MODE_DYNAMIC_SMOOTH); addEffect(FX_MODE_PACMAN, &mode_pacman, _data_FX_MODE_PACMAN); // --- 1D audio effects --- addEffect(FX_MODE_PIXELS, &mode_pixels, _data_FX_MODE_PIXELS); addEffect(FX_MODE_PIXELWAVE, &mode_pixelwave, _data_FX_MODE_PIXELWAVE); addEffect(FX_MODE_JUGGLES, &mode_juggles, _data_FX_MODE_JUGGLES); addEffect(FX_MODE_MATRIPIX, &mode_matripix, _data_FX_MODE_MATRIPIX); addEffect(FX_MODE_GRAVIMETER, &mode_gravimeter, _data_FX_MODE_GRAVIMETER); addEffect(FX_MODE_PLASMOID, &mode_plasmoid, _data_FX_MODE_PLASMOID); addEffect(FX_MODE_PUDDLES, &mode_puddles, _data_FX_MODE_PUDDLES); addEffect(FX_MODE_MIDNOISE, &mode_midnoise, _data_FX_MODE_MIDNOISE); addEffect(FX_MODE_NOISEMETER, &mode_noisemeter, _data_FX_MODE_NOISEMETER); addEffect(FX_MODE_FREQWAVE, &mode_freqwave, _data_FX_MODE_FREQWAVE); addEffect(FX_MODE_FREQMATRIX, &mode_freqmatrix, _data_FX_MODE_FREQMATRIX); addEffect(FX_MODE_WATERFALL, &mode_waterfall, _data_FX_MODE_WATERFALL); addEffect(FX_MODE_FREQPIXELS, &mode_freqpixels, _data_FX_MODE_FREQPIXELS); addEffect(FX_MODE_NOISEFIRE, &mode_noisefire, _data_FX_MODE_NOISEFIRE); addEffect(FX_MODE_PUDDLEPEAK, &mode_puddlepeak, _data_FX_MODE_PUDDLEPEAK); addEffect(FX_MODE_NOISEMOVE, &mode_noisemove, _data_FX_MODE_NOISEMOVE); addEffect(FX_MODE_PERLINMOVE, &mode_perlinmove, _data_FX_MODE_PERLINMOVE); addEffect(FX_MODE_RIPPLEPEAK, &mode_ripplepeak, _data_FX_MODE_RIPPLEPEAK); addEffect(FX_MODE_FREQMAP, &mode_freqmap, _data_FX_MODE_FREQMAP); addEffect(FX_MODE_GRAVCENTER, &mode_gravcenter, _data_FX_MODE_GRAVCENTER); addEffect(FX_MODE_GRAVCENTRIC, &mode_gravcentric, _data_FX_MODE_GRAVCENTRIC); addEffect(FX_MODE_GRAVFREQ, &mode_gravfreq, _data_FX_MODE_GRAVFREQ); addEffect(FX_MODE_DJLIGHT, &mode_DJLight, _data_FX_MODE_DJLIGHT); addEffect(FX_MODE_BLURZ, &mode_blurz, _data_FX_MODE_BLURZ); addEffect(FX_MODE_FLOWSTRIPE, &mode_FlowStripe, _data_FX_MODE_FLOWSTRIPE); addEffect(FX_MODE_WAVESINS, &mode_wavesins, _data_FX_MODE_WAVESINS); addEffect(FX_MODE_ROCKTAVES, &mode_rocktaves, _data_FX_MODE_ROCKTAVES); addEffect(FX_MODE_SHIMMER, &mode_shimmer, _data_FX_MODE_SHIMMER); // --- 2D effects --- #ifndef WLED_DISABLE_2D addEffect(FX_MODE_2DPLASMAROTOZOOM, &mode_2Dplasmarotozoom, _data_FX_MODE_2DPLASMAROTOZOOM); addEffect(FX_MODE_2DSPACESHIPS, &mode_2Dspaceships, _data_FX_MODE_2DSPACESHIPS); addEffect(FX_MODE_2DCRAZYBEES, &mode_2Dcrazybees, _data_FX_MODE_2DCRAZYBEES); #ifdef WLED_PS_DONT_REPLACE_2D_FX addEffect(FX_MODE_2DGHOSTRIDER, &mode_2Dghostrider, _data_FX_MODE_2DGHOSTRIDER); addEffect(FX_MODE_2DBLOBS, &mode_2Dfloatingblobs, _data_FX_MODE_2DBLOBS); #endif addEffect(FX_MODE_2DSCROLLTEXT, &mode_2Dscrollingtext, _data_FX_MODE_2DSCROLLTEXT); addEffect(FX_MODE_2DDRIFTROSE, &mode_2Ddriftrose, _data_FX_MODE_2DDRIFTROSE); addEffect(FX_MODE_2DDISTORTIONWAVES, &mode_2Ddistortionwaves, _data_FX_MODE_2DDISTORTIONWAVES); addEffect(FX_MODE_2DGEQ, &mode_2DGEQ, _data_FX_MODE_2DGEQ); // audio addEffect(FX_MODE_2DNOISE, &mode_2Dnoise, _data_FX_MODE_2DNOISE); addEffect(FX_MODE_2DFIRENOISE, &mode_2Dfirenoise, _data_FX_MODE_2DFIRENOISE); addEffect(FX_MODE_2DSQUAREDSWIRL, &mode_2Dsquaredswirl, _data_FX_MODE_2DSQUAREDSWIRL); //non audio addEffect(FX_MODE_2DDNA, &mode_2Ddna, _data_FX_MODE_2DDNA); addEffect(FX_MODE_2DMATRIX, &mode_2Dmatrix, _data_FX_MODE_2DMATRIX); addEffect(FX_MODE_2DMETABALLS, &mode_2Dmetaballs, _data_FX_MODE_2DMETABALLS); addEffect(FX_MODE_2DFUNKYPLANK, &mode_2DFunkyPlank, _data_FX_MODE_2DFUNKYPLANK); // audio addEffect(FX_MODE_2DPULSER, &mode_2DPulser, _data_FX_MODE_2DPULSER); addEffect(FX_MODE_2DDRIFT, &mode_2DDrift, _data_FX_MODE_2DDRIFT); addEffect(FX_MODE_2DWAVERLY, &mode_2DWaverly, _data_FX_MODE_2DWAVERLY); // audio addEffect(FX_MODE_2DSUNRADIATION, &mode_2DSunradiation, _data_FX_MODE_2DSUNRADIATION); addEffect(FX_MODE_2DCOLOREDBURSTS, &mode_2DColoredBursts, _data_FX_MODE_2DCOLOREDBURSTS); addEffect(FX_MODE_2DJULIA, &mode_2DJulia, _data_FX_MODE_2DJULIA); addEffect(FX_MODE_2DGAMEOFLIFE, &mode_2Dgameoflife, _data_FX_MODE_2DGAMEOFLIFE); addEffect(FX_MODE_2DTARTAN, &mode_2Dtartan, _data_FX_MODE_2DTARTAN); addEffect(FX_MODE_2DPOLARLIGHTS, &mode_2DPolarLights, _data_FX_MODE_2DPOLARLIGHTS); addEffect(FX_MODE_2DSWIRL, &mode_2DSwirl, _data_FX_MODE_2DSWIRL); // audio addEffect(FX_MODE_2DLISSAJOUS, &mode_2DLissajous, _data_FX_MODE_2DLISSAJOUS); addEffect(FX_MODE_2DFRIZZLES, &mode_2DFrizzles, _data_FX_MODE_2DFRIZZLES); addEffect(FX_MODE_2DPLASMABALL, &mode_2DPlasmaball, _data_FX_MODE_2DPLASMABALL); addEffect(FX_MODE_2DHIPHOTIC, &mode_2DHiphotic, _data_FX_MODE_2DHIPHOTIC); addEffect(FX_MODE_2DSINDOTS, &mode_2DSindots, _data_FX_MODE_2DSINDOTS); addEffect(FX_MODE_2DDNASPIRAL, &mode_2DDNASpiral, _data_FX_MODE_2DDNASPIRAL); addEffect(FX_MODE_2DBLACKHOLE, &mode_2DBlackHole, _data_FX_MODE_2DBLACKHOLE); addEffect(FX_MODE_2DSOAP, &mode_2Dsoap, _data_FX_MODE_2DSOAP); addEffect(FX_MODE_2DOCTOPUS, &mode_2Doctopus, _data_FX_MODE_2DOCTOPUS); addEffect(FX_MODE_2DWAVINGCELL, &mode_2Dwavingcell, _data_FX_MODE_2DWAVINGCELL); addEffect(FX_MODE_2DAKEMI, &mode_2DAkemi, _data_FX_MODE_2DAKEMI); // audio #ifndef WLED_DISABLE_PARTICLESYSTEM2D addEffect(FX_MODE_PARTICLEVOLCANO, &mode_particlevolcano, _data_FX_MODE_PARTICLEVOLCANO); addEffect(FX_MODE_PARTICLEFIRE, &mode_particlefire, _data_FX_MODE_PARTICLEFIRE); addEffect(FX_MODE_PARTICLEFIREWORKS, &mode_particlefireworks, _data_FX_MODE_PARTICLEFIREWORKS); addEffect(FX_MODE_PARTICLEVORTEX, &mode_particlevortex, _data_FX_MODE_PARTICLEVORTEX); addEffect(FX_MODE_PARTICLEPERLIN, &mode_particleperlin, _data_FX_MODE_PARTICLEPERLIN); addEffect(FX_MODE_PARTICLEPIT, &mode_particlepit, _data_FX_MODE_PARTICLEPIT); addEffect(FX_MODE_PARTICLEBOX, &mode_particlebox, _data_FX_MODE_PARTICLEBOX); addEffect(FX_MODE_PARTICLEATTRACTOR, &mode_particleattractor, _data_FX_MODE_PARTICLEATTRACTOR); // 872 bytes addEffect(FX_MODE_PARTICLEIMPACT, &mode_particleimpact, _data_FX_MODE_PARTICLEIMPACT); addEffect(FX_MODE_PARTICLEWATERFALL, &mode_particlewaterfall, _data_FX_MODE_PARTICLEWATERFALL); addEffect(FX_MODE_PARTICLESPRAY, &mode_particlespray, _data_FX_MODE_PARTICLESPRAY); addEffect(FX_MODE_PARTICLESGEQ, &mode_particleGEQ, _data_FX_MODE_PARTICLEGEQ); addEffect(FX_MODE_PARTICLECENTERGEQ, &mode_particlecenterGEQ, _data_FX_MODE_PARTICLECIRCULARGEQ); addEffect(FX_MODE_PARTICLEGHOSTRIDER, &mode_particleghostrider, _data_FX_MODE_PARTICLEGHOSTRIDER); addEffect(FX_MODE_PARTICLEBLOBS, &mode_particleblobs, _data_FX_MODE_PARTICLEBLOBS); addEffect(FX_MODE_PARTICLEGALAXY, &mode_particlegalaxy, _data_FX_MODE_PARTICLEGALAXY); #endif // WLED_DISABLE_PARTICLESYSTEM2D #endif // WLED_DISABLE_2D #ifndef WLED_DISABLE_PARTICLESYSTEM1D addEffect(FX_MODE_PSDRIP, &mode_particleDrip, _data_FX_MODE_PARTICLEDRIP); addEffect(FX_MODE_PSPINBALL, &mode_particlePinball, _data_FX_MODE_PSPINBALL); //potential replacement for: bouncing balls, rollingballs, popcorn addEffect(FX_MODE_PSDANCINGSHADOWS, &mode_particleDancingShadows, _data_FX_MODE_PARTICLEDANCINGSHADOWS); addEffect(FX_MODE_PSFIREWORKS1D, &mode_particleFireworks1D, _data_FX_MODE_PS_FIREWORKS1D); addEffect(FX_MODE_PSSPARKLER, &mode_particleSparkler, _data_FX_MODE_PS_SPARKLER); addEffect(FX_MODE_PSHOURGLASS, &mode_particleHourglass, _data_FX_MODE_PS_HOURGLASS); addEffect(FX_MODE_PS1DSPRAY, &mode_particle1Dspray, _data_FX_MODE_PS_1DSPRAY); addEffect(FX_MODE_PSBALANCE, &mode_particleBalance, _data_FX_MODE_PS_BALANCE); addEffect(FX_MODE_PSCHASE, &mode_particleChase, _data_FX_MODE_PS_CHASE); addEffect(FX_MODE_PSSTARBURST, &mode_particleStarburst, _data_FX_MODE_PS_STARBURST); addEffect(FX_MODE_PS1DGEQ, &mode_particle1DGEQ, _data_FX_MODE_PS_1D_GEQ); addEffect(FX_MODE_PSFIRE1D, &mode_particleFire1D, _data_FX_MODE_PS_FIRE1D); addEffect(FX_MODE_PS1DSONICSTREAM, &mode_particle1DsonicStream, _data_FX_MODE_PS_SONICSTREAM); addEffect(FX_MODE_PS1DSONICBOOM, &mode_particle1DsonicBoom, _data_FX_MODE_PS_SONICBOOM); addEffect(FX_MODE_PS1DSPRINGY, &mode_particleSpringy, _data_FX_MODE_PS_SPRINGY); #endif // WLED_DISABLE_PARTICLESYSTEM1D } ================================================ FILE: wled00/FX.h ================================================ #pragma once /* WS2812FX.h - Library for WS2812 LED effects. Harm Aldick - 2016 www.aldick.org Copyright (c) 2016 Harm Aldick Licensed under the EUPL v. 1.2 or later Adapted from code originally licensed under the MIT license Modified for WLED Segment class/struct (c) 2022 Blaz Kristan (@blazoncek) */ #ifndef WS2812FX_h #define WS2812FX_h #include #include "wled.h" #ifdef WLED_DEBUG // enable additional debug output #if defined(WLED_DEBUG_HOST) #include "net_debug.h" #define DEBUGOUT NetDebug #else #define DEBUGOUT Serial #endif #define DEBUGFX_PRINT(x) DEBUGOUT.print(x) #define DEBUGFX_PRINTLN(x) DEBUGOUT.println(x) #define DEBUGFX_PRINTF(x...) DEBUGOUT.printf(x) #define DEBUGFX_PRINTF_P(x...) DEBUGOUT.printf_P(x) #else #define DEBUGFX_PRINT(x) #define DEBUGFX_PRINTLN(x) #define DEBUGFX_PRINTF(x...) #define DEBUGFX_PRINTF_P(x...) #endif #define FASTLED_INTERNAL //remove annoying pragma messages #define USE_GET_MILLISECOND_TIMER #include "FastLED.h" #define DEFAULT_BRIGHTNESS (uint8_t)127 #define DEFAULT_MODE (uint8_t)0 #define DEFAULT_SPEED (uint8_t)128 #define DEFAULT_INTENSITY (uint8_t)128 #define DEFAULT_COLOR (uint32_t)0xFFAA00 #define DEFAULT_C1 (uint8_t)128 #define DEFAULT_C2 (uint8_t)128 #define DEFAULT_C3 (uint8_t)16 #ifndef MIN #define MIN(a,b) ((a)<(b)?(a):(b)) #endif #ifndef MAX #define MAX(a,b) ((a)>(b)?(a):(b)) #endif //color mangling macros #ifndef RGBW32 #define RGBW32(r,g,b,w) (uint32_t((byte(w) << 24) | (byte(r) << 16) | (byte(g) << 8) | (byte(b)))) #endif extern bool realtimeRespectLedMaps; // used in getMappedPixelIndex() extern byte realtimeMode; // used in getMappedPixelIndex() /* Not used in all effects yet */ #define WLED_FPS 42 #define FRAMETIME_FIXED (1000/WLED_FPS) #define FRAMETIME strip.getFrameTime() #if defined(ARDUINO_ARCH_ESP32) && !defined(CONFIG_IDF_TARGET_ESP32C3) && !defined(CONFIG_IDF_TARGET_ESP32S2) #define MIN_FRAME_DELAY 2 // minimum wait between repaints, to keep other functions like WiFi alive #elif defined(CONFIG_IDF_TARGET_ESP32S2) || defined(CONFIG_IDF_TARGET_ESP32C3) #define MIN_FRAME_DELAY 3 // S2/C3 are slower than normal esp32, and only have one core #else #define MIN_FRAME_DELAY 8 // 8266 legacy MIN_SHOW_DELAY #endif #define FPS_UNLIMITED 0 // FPS calculation (can be defined as compile flag for debugging) #ifndef FPS_CALC_AVG #define FPS_CALC_AVG 7 // average FPS calculation over this many frames (moving average) #endif #ifndef FPS_MULTIPLIER #define FPS_MULTIPLIER 1 // dev option: multiplier to get sub-frame FPS without floats #endif #define FPS_CALC_SHIFT 7 // bit shift for fixed point math // heap memory limit for effects data, pixel buffers try to reserve it if PSRAM is available #ifdef ESP8266 #define MAX_NUM_SEGMENTS 16 /* How much data bytes all segments combined may allocate */ #define MAX_SEGMENT_DATA (6*1024) // 6k by default #elif defined(CONFIG_IDF_TARGET_ESP32S2) #define MAX_NUM_SEGMENTS 32 #define MAX_SEGMENT_DATA (20*1024) // 20k by default (S2 is short on free RAM), limit does not apply if PSRAM is available #else #ifdef BOARD_HAS_PSRAM #define MAX_NUM_SEGMENTS 64 #else #define MAX_NUM_SEGMENTS 32 #endif #define MAX_SEGMENT_DATA (64*1024) // 64k by default, limit does not apply if PSRAM is available #endif /* How much data bytes each segment should max allocate to leave enough space for other segments, assuming each segment uses the same amount of data. 256 for ESP8266, 640 for ESP32. */ #define FAIR_DATA_PER_SEG (MAX_SEGMENT_DATA / MAX_NUM_SEGMENTS) #define MIN_SHOW_DELAY (_frametime < 16 ? 8 : 15) #define NUM_COLORS 3 /* number of colors per segment */ #define SEGMENT (*strip._currentSegment) #define SEGENV (*strip._currentSegment) #define SEGCOLOR(x) Segment::getCurrentColor(x) #define SEGPALETTE Segment::getCurrentPalette() #define SEGLEN Segment::vLength() #define SEG_W Segment::vWidth() #define SEG_H Segment::vHeight() #define SPEED_FORMULA_L (5U + (50U*(255U - SEGMENT.speed))/SEGLEN) // some common colors #define RED (uint32_t)0xFF0000 #define GREEN (uint32_t)0x00FF00 #define BLUE (uint32_t)0x0000FF #define WHITE (uint32_t)0xFFFFFF #define BLACK (uint32_t)0x000000 #define YELLOW (uint32_t)0xFFFF00 #define CYAN (uint32_t)0x00FFFF #define MAGENTA (uint32_t)0xFF00FF #define PURPLE (uint32_t)0x400080 #define ORANGE (uint32_t)0xFF3000 #define PINK (uint32_t)0xFF1493 #define GREY (uint32_t)0x808080 #define GRAY GREY #define DARKGREY (uint32_t)0x333333 #define DARKGRAY DARKGREY #define ULTRAWHITE (uint32_t)0xFFFFFFFF #define DARKSLATEGRAY (uint32_t)0x2F4F4F #define DARKSLATEGREY DARKSLATEGRAY // segment options #define NO_OPTIONS (uint16_t)0x0000 #define TRANSPOSED (uint16_t)0x0100 // rotated 90deg & reversed #define MIRROR_Y_2D (uint16_t)0x0080 #define REVERSE_Y_2D (uint16_t)0x0040 #define RESET_REQ (uint16_t)0x0020 #define FROZEN (uint16_t)0x0010 #define MIRROR (uint16_t)0x0008 #define SEGMENT_ON (uint16_t)0x0004 #define REVERSE (uint16_t)0x0002 #define SELECTED (uint16_t)0x0001 #define FX_MODE_STATIC 0 #define FX_MODE_BLINK 1 #define FX_MODE_BREATH 2 #define FX_MODE_COLOR_WIPE 3 #define FX_MODE_COLOR_WIPE_RANDOM 4 #define FX_MODE_RANDOM_COLOR 5 #define FX_MODE_COLOR_SWEEP 6 #define FX_MODE_DYNAMIC 7 #define FX_MODE_RAINBOW 8 #define FX_MODE_RAINBOW_CYCLE 9 #define FX_MODE_SCAN 10 #define FX_MODE_DUAL_SCAN 11 // candidate for removal (use Scan) #define FX_MODE_FADE 12 #define FX_MODE_THEATER_CHASE 13 #define FX_MODE_THEATER_CHASE_RAINBOW 14 // candidate for removal (use Theater) #define FX_MODE_RUNNING_LIGHTS 15 #define FX_MODE_SAW 16 #define FX_MODE_TWINKLE 17 #define FX_MODE_DISSOLVE 18 #define FX_MODE_DISSOLVE_RANDOM 19 // candidate for removal (use Dissolve with with check 3) #define FX_MODE_SPARKLE 20 #define FX_MODE_FLASH_SPARKLE 21 #define FX_MODE_HYPER_SPARKLE 22 #define FX_MODE_STROBE 23 #define FX_MODE_STROBE_RAINBOW 24 #define FX_MODE_MULTI_STROBE 25 #define FX_MODE_BLINK_RAINBOW 26 #define FX_MODE_ANDROID 27 #define FX_MODE_CHASE_COLOR 28 #define FX_MODE_CHASE_RANDOM 29 #define FX_MODE_CHASE_RAINBOW 30 #define FX_MODE_CHASE_FLASH 31 #define FX_MODE_CHASE_FLASH_RANDOM 32 #define FX_MODE_CHASE_RAINBOW_WHITE 33 #define FX_MODE_COLORFUL 34 #define FX_MODE_TRAFFIC_LIGHT 35 #define FX_MODE_COLOR_SWEEP_RANDOM 36 #define FX_MODE_RUNNING_COLOR 37 // candidate for removal (use Theater) #define FX_MODE_AURORA 38 #define FX_MODE_RUNNING_RANDOM 39 #define FX_MODE_LARSON_SCANNER 40 #define FX_MODE_COMET 41 #define FX_MODE_FIREWORKS 42 #define FX_MODE_RAIN 43 #define FX_MODE_TETRIX 44 //was Merry Christmas prior to 0.12.0 (use "Chase 2" with Red/Green) #define FX_MODE_FIRE_FLICKER 45 #define FX_MODE_GRADIENT 46 #define FX_MODE_LOADING 47 #define FX_MODE_ROLLINGBALLS 48 //was Police before 0.14 #define FX_MODE_FAIRY 49 //was Police All prior to 0.13.0-b6 (use "Two Dots" with Red/Blue and full intensity) #define FX_MODE_TWO_DOTS 50 #define FX_MODE_FAIRYTWINKLE 51 //was Two Areas prior to 0.13.0-b6 (use "Two Dots" with full intensity) #define FX_MODE_RUNNING_DUAL 52 // candidate for removal (use Running) #define FX_MODE_IMAGE 53 #define FX_MODE_TRICOLOR_CHASE 54 #define FX_MODE_TRICOLOR_WIPE 55 #define FX_MODE_TRICOLOR_FADE 56 #define FX_MODE_LIGHTNING 57 #define FX_MODE_ICU 58 #define FX_MODE_MULTI_COMET 59 #define FX_MODE_DUAL_LARSON_SCANNER 60 // candidate for removal (use Scanner with with check 1) #define FX_MODE_RANDOM_CHASE 61 #define FX_MODE_OSCILLATE 62 #define FX_MODE_PRIDE_2015 63 #define FX_MODE_JUGGLE 64 #define FX_MODE_PALETTE 65 #define FX_MODE_FIRE_2012 66 #define FX_MODE_COLORWAVES 67 #define FX_MODE_BPM 68 #define FX_MODE_FILLNOISE8 69 #define FX_MODE_NOISE16_1 70 #define FX_MODE_NOISE16_2 71 #define FX_MODE_NOISE16_3 72 #define FX_MODE_NOISE16_4 73 #define FX_MODE_COLORTWINKLE 74 #define FX_MODE_LAKE 75 #define FX_MODE_METEOR 76 //#define FX_MODE_METEOR_SMOOTH 77 // replaced by Meteor #define FX_MODE_COPY 77 #define FX_MODE_RAILWAY 78 #define FX_MODE_RIPPLE 79 #define FX_MODE_TWINKLEFOX 80 #define FX_MODE_TWINKLECAT 81 #define FX_MODE_HALLOWEEN_EYES 82 #define FX_MODE_STATIC_PATTERN 83 #define FX_MODE_TRI_STATIC_PATTERN 84 #define FX_MODE_SPOTS 85 #define FX_MODE_SPOTS_FADE 86 #define FX_MODE_GLITTER 87 #define FX_MODE_CANDLE 88 #define FX_MODE_STARBURST 89 #define FX_MODE_EXPLODING_FIREWORKS 90 #define FX_MODE_BOUNCINGBALLS 91 #define FX_MODE_SINELON 92 #define FX_MODE_SINELON_DUAL 93 // candidate for removal (use sinelon) #define FX_MODE_SINELON_RAINBOW 94 // candidate for removal (use sinelon) #define FX_MODE_POPCORN 95 #define FX_MODE_DRIP 96 #define FX_MODE_PLASMA 97 #define FX_MODE_PERCENT 98 #define FX_MODE_RIPPLE_RAINBOW 99 // candidate for removal (use ripple) #define FX_MODE_HEARTBEAT 100 #define FX_MODE_PACIFICA 101 #define FX_MODE_CANDLE_MULTI 102 // candidate for removal (use candle with multi select) #define FX_MODE_SOLID_GLITTER 103 // candidate for removal (use glitter) #define FX_MODE_SUNRISE 104 #define FX_MODE_PHASED 105 #define FX_MODE_TWINKLEUP 106 #define FX_MODE_NOISEPAL 107 #define FX_MODE_SINEWAVE 108 #define FX_MODE_PHASEDNOISE 109 #define FX_MODE_FLOW 110 #define FX_MODE_CHUNCHUN 111 #define FX_MODE_DANCING_SHADOWS 112 #define FX_MODE_WASHING_MACHINE 113 #define FX_MODE_2DPLASMAROTOZOOM 114 // was Candy Cane prior to 0.14 (use Chase 2) #define FX_MODE_BLENDS 115 #define FX_MODE_TV_SIMULATOR 116 #define FX_MODE_DYNAMIC_SMOOTH 117 // candidate for removal (check3 in dynamic) // new 0.14 2D effects #define FX_MODE_2DSPACESHIPS 118 //gap fill #define FX_MODE_2DCRAZYBEES 119 //gap fill #define FX_MODE_2DGHOSTRIDER 120 //gap fill #define FX_MODE_2DBLOBS 121 //gap fill #define FX_MODE_2DSCROLLTEXT 122 //gap fill #define FX_MODE_2DDRIFTROSE 123 //gap fill #define FX_MODE_2DDISTORTIONWAVES 124 //gap fill #define FX_MODE_2DSOAP 125 //gap fill #define FX_MODE_2DOCTOPUS 126 //gap fill #define FX_MODE_2DWAVINGCELL 127 //gap fill // WLED-SR effects (SR compatible IDs !!!) #define FX_MODE_PIXELS 128 #define FX_MODE_PIXELWAVE 129 #define FX_MODE_JUGGLES 130 #define FX_MODE_MATRIPIX 131 #define FX_MODE_GRAVIMETER 132 #define FX_MODE_PLASMOID 133 #define FX_MODE_PUDDLES 134 #define FX_MODE_MIDNOISE 135 #define FX_MODE_NOISEMETER 136 #define FX_MODE_FREQWAVE 137 #define FX_MODE_FREQMATRIX 138 #define FX_MODE_2DGEQ 139 #define FX_MODE_WATERFALL 140 #define FX_MODE_FREQPIXELS 141 #define FX_MODE_BINMAP 142 #define FX_MODE_NOISEFIRE 143 #define FX_MODE_PUDDLEPEAK 144 #define FX_MODE_NOISEMOVE 145 #define FX_MODE_2DNOISE 146 #define FX_MODE_PERLINMOVE 147 #define FX_MODE_RIPPLEPEAK 148 #define FX_MODE_2DFIRENOISE 149 #define FX_MODE_2DSQUAREDSWIRL 150 // #define FX_MODE_2DFIRE2012 151 #define FX_MODE_PACMAN 151 // gap fill (non-SR). Do NOT renumber; SR-ID range must remain stable. #define FX_MODE_2DDNA 152 #define FX_MODE_2DMATRIX 153 #define FX_MODE_2DMETABALLS 154 #define FX_MODE_FREQMAP 155 #define FX_MODE_GRAVCENTER 156 #define FX_MODE_GRAVCENTRIC 157 #define FX_MODE_GRAVFREQ 158 #define FX_MODE_DJLIGHT 159 #define FX_MODE_2DFUNKYPLANK 160 //#define FX_MODE_2DCENTERBARS 161 #define FX_MODE_SHIMMER 161 // gap fill, non SR 1D effect #define FX_MODE_2DPULSER 162 #define FX_MODE_BLURZ 163 #define FX_MODE_2DDRIFT 164 #define FX_MODE_2DWAVERLY 165 #define FX_MODE_2DSUNRADIATION 166 #define FX_MODE_2DCOLOREDBURSTS 167 #define FX_MODE_2DJULIA 168 // #define FX_MODE_2DPOOLNOISE 169 //have been removed in WLED SR in the past because of low mem but should be added back // #define FX_MODE_2DTWISTER 170 //have been removed in WLED SR in the past because of low mem but should be added back // #define FX_MODE_2DCAELEMENTATY 171 //have been removed in WLED SR in the past because of low mem but should be added back #define FX_MODE_2DGAMEOFLIFE 172 #define FX_MODE_2DTARTAN 173 #define FX_MODE_2DPOLARLIGHTS 174 #define FX_MODE_2DSWIRL 175 #define FX_MODE_2DLISSAJOUS 176 #define FX_MODE_2DFRIZZLES 177 #define FX_MODE_2DPLASMABALL 178 #define FX_MODE_FLOWSTRIPE 179 #define FX_MODE_2DHIPHOTIC 180 #define FX_MODE_2DSINDOTS 181 #define FX_MODE_2DDNASPIRAL 182 #define FX_MODE_2DBLACKHOLE 183 #define FX_MODE_WAVESINS 184 #define FX_MODE_ROCKTAVES 185 #define FX_MODE_2DAKEMI 186 #define FX_MODE_PARTICLEVOLCANO 187 #define FX_MODE_PARTICLEFIRE 188 #define FX_MODE_PARTICLEFIREWORKS 189 #define FX_MODE_PARTICLEVORTEX 190 #define FX_MODE_PARTICLEPERLIN 191 #define FX_MODE_PARTICLEPIT 192 #define FX_MODE_PARTICLEBOX 193 #define FX_MODE_PARTICLEATTRACTOR 194 #define FX_MODE_PARTICLEIMPACT 195 #define FX_MODE_PARTICLEWATERFALL 196 #define FX_MODE_PARTICLESPRAY 197 #define FX_MODE_PARTICLESGEQ 198 #define FX_MODE_PARTICLECENTERGEQ 199 #define FX_MODE_PARTICLEGHOSTRIDER 200 #define FX_MODE_PARTICLEBLOBS 201 #define FX_MODE_PSDRIP 202 #define FX_MODE_PSPINBALL 203 #define FX_MODE_PSDANCINGSHADOWS 204 #define FX_MODE_PSFIREWORKS1D 205 #define FX_MODE_PSSPARKLER 206 #define FX_MODE_PSHOURGLASS 207 #define FX_MODE_PS1DSPRAY 208 #define FX_MODE_PSBALANCE 209 #define FX_MODE_PSCHASE 210 #define FX_MODE_PSSTARBURST 211 #define FX_MODE_PS1DGEQ 212 #define FX_MODE_PSFIRE1D 213 #define FX_MODE_PS1DSONICSTREAM 214 #define FX_MODE_PS1DSONICBOOM 215 #define FX_MODE_PS1DSPRINGY 216 #define FX_MODE_PARTICLEGALAXY 217 #define FX_MODE_COLORCLOUDS 218 #define MODE_COUNT 219 #define BLEND_STYLE_FADE 0x00 // universal #define BLEND_STYLE_FAIRY_DUST 0x01 // universal #define BLEND_STYLE_SWIPE_RIGHT 0x02 // 1D or 2D #define BLEND_STYLE_SWIPE_LEFT 0x03 // 1D or 2D #define BLEND_STYLE_OUTSIDE_IN 0x04 // 1D or 2D #define BLEND_STYLE_INSIDE_OUT 0x05 // 1D or 2D #define BLEND_STYLE_SWIPE_UP 0x06 // 2D #define BLEND_STYLE_SWIPE_DOWN 0x07 // 2D #define BLEND_STYLE_OPEN_H 0x08 // 2D #define BLEND_STYLE_OPEN_V 0x09 // 2D #define BLEND_STYLE_SWIPE_TL 0x0A // 2D #define BLEND_STYLE_SWIPE_TR 0x0B // 2D #define BLEND_STYLE_SWIPE_BR 0x0C // 2D #define BLEND_STYLE_SWIPE_BL 0x0D // 2D #define BLEND_STYLE_CIRCULAR_OUT 0x0E // 2D #define BLEND_STYLE_CIRCULAR_IN 0x0F // 2D // as there are many push variants to optimise if statements they are groupped together #define BLEND_STYLE_PUSH_RIGHT 0x10 // 1D or 2D (& 0b00010000) #define BLEND_STYLE_PUSH_LEFT 0x11 // 1D or 2D (& 0b00010000) #define BLEND_STYLE_PUSH_UP 0x12 // 2D (& 0b00010000) #define BLEND_STYLE_PUSH_DOWN 0x13 // 2D (& 0b00010000) #define BLEND_STYLE_PUSH_TL 0x14 // 2D (& 0b00010000) #define BLEND_STYLE_PUSH_TR 0x15 // 2D (& 0b00010000) #define BLEND_STYLE_PUSH_BR 0x16 // 2D (& 0b00010000) #define BLEND_STYLE_PUSH_BL 0x17 // 2D (& 0b00010000) #define BLEND_STYLE_PUSH_MASK 0x10 #define BLEND_STYLE_COUNT 18 typedef enum mapping1D2D { M12_Pixels = 0, M12_pBar = 1, M12_pArc = 2, M12_pCorner = 3, M12_sPinwheel = 4 } mapping1D2D_t; class WS2812FX; // segment, 76 bytes class Segment { public: uint32_t colors[NUM_COLORS]; uint16_t start; // start index / start X coordinate 2D (left) uint16_t stop; // stop index / stop X coordinate 2D (right); segment is invalid if stop == 0 uint16_t startY; // start Y coodrinate 2D (top); there should be no more than 255 rows uint16_t stopY; // stop Y coordinate 2D (bottom); there should be no more than 255 rows uint16_t offset; // offset for 1D effects (effect will wrap around) union { mutable uint16_t options; //bit pattern: msb first: [transposed mirrorY reverseY] transitional (tbd) paused needspixelstate mirrored on reverse selected struct { mutable bool selected : 1; // 0 : selected bool reverse : 1; // 1 : reversed mutable bool on : 1; // 2 : is On bool mirror : 1; // 3 : mirrored mutable bool freeze : 1; // 4 : paused/frozen mutable bool reset : 1; // 5 : indicates that Segment runtime requires reset bool reverse_y : 1; // 6 : reversed Y (2D) bool mirror_y : 1; // 7 : mirrored Y (2D) bool transpose : 1; // 8 : transposed (2D, swapped X & Y) uint8_t map1D2D : 3; // 9-11 : mapping for 1D effect on 2D (0-use as strip, 1-expand vertically, 2-circular/arc, 3-rectangular/corner, ...) uint8_t soundSim : 2; // 12-13 : 0-3 sound simulation types ("soft" & "hard" or "on"/"off") mutable uint8_t set : 2; // 14-15 : 0-3 UI segment sets/groups }; }; uint8_t grouping, spacing; uint8_t opacity, cct; // 0==1900K, 255==10091K // effect data uint8_t mode; uint8_t palette; uint8_t speed; uint8_t intensity; uint8_t custom1, custom2; // custom FX parameters/sliders struct { uint8_t custom3 : 5; // reduced range slider (0-31) bool check1 : 1; // checkmark 1 bool check2 : 1; // checkmark 2 bool check3 : 1; // checkmark 3 //uint8_t blendMode : 4; // segment blending modes: top, bottom, add, subtract, difference, multiply, divide, lighten, darken, screen, overlay, hardlight, softlight, dodge, burn }; uint8_t blendMode; // segment blending modes: top, bottom, add, subtract, difference, multiply, divide, lighten, darken, screen, overlay, hardlight, softlight, dodge, burn char *name; // segment name // runtime data mutable uint32_t step; // custom "step" var mutable uint32_t call; // call counter mutable uint16_t aux0; // custom var mutable uint16_t aux1; // custom var byte *data; // effect data pointer static uint16_t maxWidth, maxHeight; // these define matrix width & height (max. segment dimensions) private: uint32_t *pixels; // pixel data unsigned _dataLen; uint8_t _default_palette; // palette number that gets assigned to pal0 union { mutable uint8_t _capabilities; // determines segment capabilities in terms of what is available: RGB, W, CCT, manual W, etc. struct { bool _isRGB : 1; bool _hasW : 1; bool _isCCT : 1; bool _manualW : 1; }; }; // static variables are use to speed up effect calculations by stashing common pre-calculated values static unsigned _usedSegmentData; // amount of data used by all segments static unsigned _vLength; // 1D dimension used for current effect static unsigned _vWidth, _vHeight; // 2D dimensions used for current effect static uint32_t _currentColors[NUM_COLORS]; // colors used for current effect (faster access from effect functions) static CRGBPalette16 _currentPalette; // palette used for current effect (includes transition, used in color_from_palette()) static CRGBPalette16 _randomPalette; // actual random palette static CRGBPalette16 _newRandomPalette; // target random palette static uint16_t _lastPaletteChange; // last random palette change time (in seconds) static uint16_t _nextPaletteBlend; // next due time for random palette morph (in millis()) static bool _modeBlend; // mode/effect blending semaphore // clipping rectangle used for blending static uint16_t _clipStart, _clipStop; static uint8_t _clipStartY, _clipStopY; // transition data, holds values during transition (76 bytes/28 bytes) struct Transition { Segment *_oldSegment; // previous segment environment (may be nullptr if effect did not change) unsigned long _start; // must accommodate millis() uint32_t _colors[NUM_COLORS]; // current colors #ifndef WLED_SAVE_RAM CRGBPalette16 _palT; // temporary palette (slowly being morphed from old to new) #endif uint16_t _dur; // duration of transition in ms uint16_t _progress; // transition progress (0-65535); pre-calculated from _start & _dur in updateTransitionProgress() uint8_t _prevPaletteBlends; // number of previous palette blends (there are max 255 blends possible) uint8_t _palette, _bri, _cct; // palette ID, brightness and CCT at the start of transition (brightness will be 0 if segment was off) Transition(uint16_t dur=750) : _oldSegment(nullptr) , _start(millis()) , _colors{0,0,0} #ifndef WLED_SAVE_RAM , _palT(CRGBPalette16(CRGB::Black)) #endif , _dur(dur) , _progress(0) , _prevPaletteBlends(0) , _palette(0) , _bri(0) , _cct(0) {} ~Transition() { //DEBUGFX_PRINTF_P(PSTR("-- Destroying transition: %p\n"), this); if (_oldSegment) delete _oldSegment; } } *_t; protected: inline static void addUsedSegmentData(int len) { Segment::_usedSegmentData += len; } inline uint32_t *getPixels() const { return pixels; } inline void setPixelColorRaw(unsigned i, uint32_t c) const { pixels[i] = c; } inline uint32_t getPixelColorRaw(unsigned i) const { return pixels[i]; }; #ifndef WLED_DISABLE_2D inline void setPixelColorXYRaw(unsigned x, unsigned y, uint32_t c) const { auto XY = [](unsigned X, unsigned Y){ return X + Y*Segment::vWidth(); }; pixels[XY(x,y)] = c; } inline uint32_t getPixelColorXYRaw(unsigned x, unsigned y) const { auto XY = [](unsigned X, unsigned Y){ return X + Y*Segment::vWidth(); }; return pixels[XY(x,y)]; }; #endif void resetIfRequired(); // sets all SEGENV variables to 0 and clears data buffer CRGBPalette16 &loadPalette(CRGBPalette16 &tgt, uint8_t pal); // transition functions void stopTransition(); // ends transition mode by destroying transition structure (does nothing if not in transition) void updateTransitionProgress() const; // sets transition progress (0-65535) based on time passed since transition start inline void handleTransition() { updateTransitionProgress(); if (isInTransition() && progress() == 0xFFFFU) stopTransition(); } inline uint16_t progress() const { return isInTransition() ? _t->_progress : 0xFFFFU; } // relies on handleTransition()/updateTransitionProgress() to update progression variable inline Segment *getOldSegment() const { return isInTransition() ? _t->_oldSegment : nullptr; } inline static void modeBlend(bool blend) { Segment::_modeBlend = blend; } inline static void setClippingRect(int startX, int stopX, int startY = 0, int stopY = 1) { _clipStart = startX; _clipStop = stopX; _clipStartY = startY; _clipStopY = stopY; }; inline static bool isPreviousMode() { return Segment::_modeBlend; } // needed for determining CCT/opacity during non-BLEND_STYLE_FADE transition static void handleRandomPalette(); public: Segment(uint16_t sStart=0, uint16_t sStop=30, uint16_t sStartY = 0, uint16_t sStopY = 1) : colors{DEFAULT_COLOR,BLACK,BLACK} , start(sStart) , stop(sStop > sStart ? sStop : sStart+1) // minimum length is 1 , startY(sStartY) , stopY(sStopY > sStartY ? sStopY : sStartY+1) // minimum height is 1 , offset(0) , options(SELECTED | SEGMENT_ON) , grouping(1) , spacing(0) , opacity(255) , cct(127) , mode(DEFAULT_MODE) , palette(0) , speed(DEFAULT_SPEED) , intensity(DEFAULT_INTENSITY) , custom1(DEFAULT_C1) , custom2(DEFAULT_C2) , custom3(DEFAULT_C3) , check1(false) , check2(false) , check3(false) , blendMode(0) , name(nullptr) , step(0) , call(0) , aux0(0) , aux1(0) , data(nullptr) , _dataLen(0) , _default_palette(6) , _capabilities(0) , _t(nullptr) { DEBUGFX_PRINTF_P(PSTR("-- Creating segment: %p [%d,%d:%d,%d]\n"), this, (int)start, (int)stop, (int)startY, (int)stopY); // allocate render buffer (always entire segment), prefer PSRAM if DRAM is running low. Note: impact on FPS with PSRAM buffer is low (<2% with QSPI PSRAM) pixels = static_cast(allocate_buffer(length() * sizeof(uint32_t), BFRALLOC_PREFER_PSRAM | BFRALLOC_NOBYTEACCESS | BFRALLOC_CLEAR)); if (!pixels) { DEBUGFX_PRINTLN(F("!!! Not enough RAM for pixel buffer !!!")); extern byte errorFlag; errorFlag = ERR_NORAM_PX; stop = 0; // mark segment as inactive/invalid } } Segment(const Segment &orig); // copy constructor Segment(Segment &&orig) noexcept; // move constructor ~Segment() { #ifdef WLED_DEBUG DEBUGFX_PRINTF_P(PSTR("-- Destroying segment: %p [%d,%d:%d,%d]"), this, (int)start, (int)stop, (int)startY, (int)stopY); if (name) DEBUGFX_PRINTF_P(PSTR(" %s (%p)"), name, name); if (data) DEBUGFX_PRINTF_P(PSTR(" %u->(%p)"), _dataLen, data); DEBUGFX_PRINTF_P(PSTR(" T[%p]"), _t); DEBUGFX_PRINTLN(); #endif clearName(); #ifdef WLED_ENABLE_GIF endImagePlayback(this); #endif deallocateData(); p_free(pixels); } Segment& operator= (const Segment &orig); // copy assignment Segment& operator= (Segment &&orig) noexcept; // move assignment #ifdef WLED_DEBUG size_t getSize() const { return sizeof(Segment) + (data?_dataLen:0) + (name?strlen(name):0) + (_t?sizeof(Transition):0) + (pixels?length()*sizeof(uint32_t):0); } #endif inline bool getOption(uint8_t n) const { return ((options >> n) & 0x01); } inline bool isSelected() const { return selected; } inline bool isInTransition() const { return _t != nullptr; } inline bool isActive() const { return stop > start && pixels; } inline bool hasRGB() const { return _isRGB; } inline bool hasWhite() const { return _hasW; } inline bool isCCT() const { return _isCCT; } inline uint16_t width() const { return stop > start ? (stop - start) : 0; }// segment width in physical pixels (length if 1D) inline uint16_t height() const { return stopY - startY; } // segment height (if 2D) in physical pixels (it *is* always >=1) inline uint16_t length() const { return width() * height(); } // segment length (count) in physical pixels inline uint16_t groupLength() const { return grouping + spacing; } inline uint8_t getLightCapabilities() const { return _capabilities; } inline void deactivate() { setGeometry(0,0); } inline Segment &clearName() { p_free(name); name = nullptr; return *this; } inline Segment &setName(const String &name) { return setName(name.c_str()); } inline static unsigned vLength() { return Segment::_vLength; } inline static unsigned vWidth() { return Segment::_vWidth; } inline static unsigned vHeight() { return Segment::_vHeight; } inline static uint32_t getCurrentColor(unsigned i) { return Segment::_currentColors[i= 0 && i < length()) setPixelColorRaw(i,col); } #ifdef WLED_USE_AA_PIXELS void setPixelColor(float i, uint32_t c, bool aa = true) const; inline void setPixelColor(float i, uint8_t r, uint8_t g, uint8_t b, uint8_t w = 0, bool aa = true) const { setPixelColor(i, RGBW32(r,g,b,w), aa); } inline void setPixelColor(float i, CRGB c, bool aa = true) const { setPixelColor(i, RGBW32(c.r,c.g,c.b,0), aa); } #endif [[gnu::hot]] bool isPixelClipped(int i) const; [[gnu::hot]] uint32_t getPixelColor(int i) const; // 1D support functions (some implement 2D as well) void blur(uint8_t, bool smear = false) const; void clear() const { fill(BLACK); } // clear segment void fill(uint32_t c) const; void fade_out(uint8_t r) const; void fadeToSecondaryBy(uint8_t fadeBy) const; void fadeToBlackBy(uint8_t fadeBy) const; inline void blendPixelColor(int n, uint32_t color, uint8_t blend) const { setPixelColor(n, color_blend(getPixelColor(n), color, blend)); } inline void blendPixelColor(int n, CRGB c, uint8_t blend) const { blendPixelColor(n, RGBW32(c.r,c.g,c.b,0), blend); } inline void addPixelColor(int n, uint32_t color, bool preserveCR = true) const { setPixelColor(n, color_add(getPixelColor(n), color, preserveCR)); } inline void addPixelColor(int n, byte r, byte g, byte b, byte w = 0, bool preserveCR = true) const { addPixelColor(n, RGBW32(r,g,b,w), preserveCR); } inline void addPixelColor(int n, CRGB c, bool preserveCR = true) const { addPixelColor(n, RGBW32(c.r,c.g,c.b,0), preserveCR); } inline void fadePixelColor(uint16_t n, uint8_t fade) const { setPixelColor(n, color_fade(getPixelColor(n), fade, true)); } [[gnu::hot]] uint32_t color_from_palette(uint16_t, bool mapping, bool moving, uint8_t mcol, uint8_t pbri = 255) const; [[gnu::hot]] uint32_t color_wheel(uint8_t pos) const; // 2D matrix unsigned virtualWidth() const; // segment width in virtual pixels (accounts for groupping and spacing) unsigned virtualHeight() const; // segment height in virtual pixels (accounts for groupping and spacing) inline unsigned nrOfVStrips() const { // returns number of virtual vertical strips in 2D matrix (used to expand 1D effects into 2D) #ifndef WLED_DISABLE_2D return (is2D() && map1D2D == M12_pBar) ? virtualWidth() : 1; #else return 1; #endif } inline unsigned rawLength() const { // returns length of used raw pixel buffer (eg. get/setPixelColorRaw()) #ifndef WLED_DISABLE_2D if (is2D()) return virtualWidth() * virtualHeight(); #endif return virtualLength(); } #ifndef WLED_DISABLE_2D inline bool is2D() const { return (width()>1 && height()>1); } [[gnu::hot]] void setPixelColorXY(int x, int y, uint32_t c) const; // set relative pixel within segment with color inline void setPixelColorXY(unsigned x, unsigned y, uint32_t c) const { setPixelColorXY(int(x), int(y), c); } inline void setPixelColorXY(int x, int y, byte r, byte g, byte b, byte w = 0) const { setPixelColorXY(x, y, RGBW32(r,g,b,w)); } inline void setPixelColorXY(int x, int y, CRGB c) const { setPixelColorXY(x, y, RGBW32(c.r,c.g,c.b,0)); } inline void setPixelColorXY(unsigned x, unsigned y, CRGB c) const { setPixelColorXY(int(x), int(y), RGBW32(c.r,c.g,c.b,0)); } #ifdef WLED_USE_AA_PIXELS void setPixelColorXY(float x, float y, uint32_t c, bool aa = true) const; inline void setPixelColorXY(float x, float y, byte r, byte g, byte b, byte w = 0, bool aa = true) const { setPixelColorXY(x, y, RGBW32(r,g,b,w), aa); } inline void setPixelColorXY(float x, float y, CRGB c, bool aa = true) const { setPixelColorXY(x, y, RGBW32(c.r,c.g,c.b,0), aa); } #endif [[gnu::hot]] bool isPixelXYClipped(int x, int y) const; [[gnu::hot]] uint32_t getPixelColorXY(int x, int y) const; // 2D support functions inline void blendPixelColorXY(uint16_t x, uint16_t y, uint32_t color, uint8_t blend) const { setPixelColorXY(x, y, color_blend(getPixelColorXY(x,y), color, blend)); } inline void blendPixelColorXY(uint16_t x, uint16_t y, CRGB c, uint8_t blend) const { blendPixelColorXY(x, y, RGBW32(c.r,c.g,c.b,0), blend); } inline void addPixelColorXY(int x, int y, uint32_t color, bool preserveCR = true) const { setPixelColorXY(x, y, color_add(getPixelColorXY(x,y), color, preserveCR)); } inline void addPixelColorXY(int x, int y, byte r, byte g, byte b, byte w = 0, bool preserveCR = true) { addPixelColorXY(x, y, RGBW32(r,g,b,w), preserveCR); } inline void addPixelColorXY(int x, int y, CRGB c, bool preserveCR = true) const { addPixelColorXY(x, y, RGBW32(c.r,c.g,c.b,0), preserveCR); } inline void fadePixelColorXY(uint16_t x, uint16_t y, uint8_t fade) const { setPixelColorXY(x, y, color_fade(getPixelColorXY(x,y), fade, true)); } inline void blurCols(fract8 blur_amount, bool smear = false) const { blur2D(0, blur_amount, smear); } // blur all columns (50% faster than full 2D blur) inline void blurRows(fract8 blur_amount, bool smear = false) const { blur2D(blur_amount, 0, smear); } // blur all rows (50% faster than full 2D blur) //void box_blur(unsigned r = 1U, bool smear = false); // 2D box blur void blur2D(uint8_t blur_x, uint8_t blur_y, bool smear = false) const; void moveX(int delta, bool wrap = false) const; void moveY(int delta, bool wrap = false) const; void move(unsigned dir, unsigned delta, bool wrap = false) const; void drawCircle(uint16_t cx, uint16_t cy, uint8_t radius, uint32_t c, bool soft = false) const; void fillCircle(uint16_t cx, uint16_t cy, uint8_t radius, uint32_t c, bool soft = false) const; void drawLine(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1, uint32_t c, bool soft = false) const; void drawCharacter(unsigned char chr, int16_t x, int16_t y, uint8_t w, uint8_t h, uint32_t color, uint32_t col2 = 0, int8_t rotate = 0) const; void wu_pixel(uint32_t x, uint32_t y, CRGB c) const; inline void drawCircle(uint16_t cx, uint16_t cy, uint8_t radius, CRGB c, bool soft = false) const { drawCircle(cx, cy, radius, RGBW32(c.r,c.g,c.b,0), soft); } inline void fillCircle(uint16_t cx, uint16_t cy, uint8_t radius, CRGB c, bool soft = false) const { fillCircle(cx, cy, radius, RGBW32(c.r,c.g,c.b,0), soft); } inline void drawLine(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1, CRGB c, bool soft = false) const { drawLine(x0, y0, x1, y1, RGBW32(c.r,c.g,c.b,0), soft); } // automatic inline inline void drawCharacter(unsigned char chr, int16_t x, int16_t y, uint8_t w, uint8_t h, CRGB c, CRGB c2 = CRGB::Black, int8_t rotate = 0) const { drawCharacter(chr, x, y, w, h, RGBW32(c.r,c.g,c.b,0), RGBW32(c2.r,c2.g,c2.b,0), rotate); } // automatic inline inline void fill_solid(CRGB c) const { fill(RGBW32(c.r,c.g,c.b,0)); } #else inline bool is2D() const { return false; } inline void setPixelColorXY(int x, int y, uint32_t c) const { setPixelColor(x, c); } inline void setPixelColorXY(unsigned x, unsigned y, uint32_t c) const { setPixelColor(int(x), c); } inline void setPixelColorXY(int x, int y, byte r, byte g, byte b, byte w = 0) const { setPixelColor(x, RGBW32(r,g,b,w)); } inline void setPixelColorXY(int x, int y, CRGB c) const { setPixelColor(x, RGBW32(c.r,c.g,c.b,0)); } inline void setPixelColorXY(unsigned x, unsigned y, CRGB c) const { setPixelColor(int(x), RGBW32(c.r,c.g,c.b,0)); } #ifdef WLED_USE_AA_PIXELS inline void setPixelColorXY(float x, float y, uint32_t c, bool aa = true) const { setPixelColor(x, c, aa); } inline void setPixelColorXY(float x, float y, byte r, byte g, byte b, byte w = 0, bool aa = true) { setPixelColor(x, RGBW32(r,g,b,w), aa); } inline void setPixelColorXY(float x, float y, CRGB c, bool aa = true) const { setPixelColor(x, RGBW32(c.r,c.g,c.b,0), aa); } #endif inline bool isPixelXYClipped(int x, int y) const { return isPixelClipped(x); } inline uint32_t getPixelColorXY(int x, int y) const { return getPixelColor(x); } inline void blendPixelColorXY(uint16_t x, uint16_t y, uint32_t c, uint8_t blend) const { blendPixelColor(x, c, blend); } inline void blendPixelColorXY(uint16_t x, uint16_t y, CRGB c, uint8_t blend) const { blendPixelColor(x, RGBW32(c.r,c.g,c.b,0), blend); } inline void addPixelColorXY(int x, int y, uint32_t color, bool saturate = false) const { addPixelColor(x, color, saturate); } inline void addPixelColorXY(int x, int y, byte r, byte g, byte b, byte w = 0, bool saturate = false) const { addPixelColor(x, RGBW32(r,g,b,w), saturate); } inline void addPixelColorXY(int x, int y, CRGB c, bool saturate = false) const { addPixelColor(x, RGBW32(c.r,c.g,c.b,0), saturate); } inline void fadePixelColorXY(uint16_t x, uint16_t y, uint8_t fade) const { fadePixelColor(x, fade); } //inline void box_blur(unsigned i, bool vertical, fract8 blur_amount) {} inline void blur2D(uint8_t blur_x, uint8_t blur_y, bool smear = false) {} inline void blurCols(fract8 blur_amount, bool smear = false) { blur(blur_amount, smear); } // blur all columns (50% faster than full 2D blur) inline void blurRows(fract8 blur_amount, bool smear = false) {} inline void moveX(int delta, bool wrap = false) {} inline void moveY(int delta, bool wrap = false) {} inline void move(uint8_t dir, uint8_t delta, bool wrap = false) {} inline void drawCircle(uint16_t cx, uint16_t cy, uint8_t radius, uint32_t c, bool soft = false) {} inline void drawCircle(uint16_t cx, uint16_t cy, uint8_t radius, CRGB c, bool soft = false) {} inline void fillCircle(uint16_t cx, uint16_t cy, uint8_t radius, uint32_t c, bool soft = false) {} inline void fillCircle(uint16_t cx, uint16_t cy, uint8_t radius, CRGB c, bool soft = false) {} inline void drawLine(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1, uint32_t c, bool soft = false) {} inline void drawLine(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1, CRGB c, bool soft = false) {} inline void drawCharacter(unsigned char chr, int16_t x, int16_t y, uint8_t w, uint8_t h, uint32_t color, uint32_t = 0, int8_t = 0) {} inline void drawCharacter(unsigned char chr, int16_t x, int16_t y, uint8_t w, uint8_t h, CRGB c, CRGB c2, int8_t rotate = 0) {} inline void wu_pixel(uint32_t x, uint32_t y, CRGB c) {} #endif friend class WS2812FX; friend class ParticleSystem2D; friend class ParticleSystem1D; }; // main "strip" class (108 bytes) class WS2812FX { typedef void (*mode_ptr)(); // pointer to mode function typedef void (*show_callback)(); // pre show callback typedef struct ModeData { uint8_t _id; // mode (effect) id mode_ptr _fcn; // mode (effect) function const char *_data; // mode (effect) name and its UI control data ModeData(uint8_t id, void (*fcn)(void), const char *data) : _id(id), _fcn(fcn), _data(data) {} } mode_data_t; public: WS2812FX() : now(millis()), timebase(0), isMatrix(false), #ifdef WLED_AUTOSEGMENTS autoSegments(true), #else autoSegments(false), #endif correctWB(false), cctFromRgb(false), // true private variables _pixels(nullptr), _pixelCCT(nullptr), _suspend(false), _brightness(DEFAULT_BRIGHTNESS), _length(DEFAULT_LED_COUNT), _transitionDur(750), _frametime(FRAMETIME_FIXED), _cumulativeFps(WLED_FPS << FPS_CALC_SHIFT), _targetFps(WLED_FPS), _isServicing(false), _isOffRefreshRequired(false), _hasWhiteChannel(false), _triggered(false), _segment_index(0), _mainSegment(0), _modeCount(MODE_COUNT), _callback(nullptr), customMappingTable(nullptr), customMappingSize(0), _lastShow(0), _lastServiceShow(0) { _mode.reserve(_modeCount); // allocate memory to prevent initial fragmentation (does not increase size()) _modeData.reserve(_modeCount); // allocate memory to prevent initial fragmentation (does not increase size()) if (_mode.capacity() <= 1 || _modeData.capacity() <= 1) _modeCount = 1; // memory allocation failed only show Solid else setupEffectData(); } ~WS2812FX() { p_free(_pixels); p_free(_pixelCCT); // just in case d_free(customMappingTable); _mode.clear(); _modeData.clear(); _segments.clear(); #ifndef WLED_DISABLE_2D panel.clear(); #endif } void #ifdef WLED_DEBUG printSize(), // prints memory usage for strip components #endif finalizeInit(), // initialises strip components service(), // executes effect functions when due and calls strip.show() setCCT(uint16_t k), // sets global CCT (either in relative 0-255 value or in K) setBrightness(uint8_t b, bool direct = false), // sets strip brightness setRange(uint16_t i, uint16_t i2, uint32_t col), // used for clock overlay purgeSegments(), // removes inactive segments from RAM (may incure penalty and memory fragmentation but reduces vector footprint) setMainSegmentId(unsigned n = 0), resetSegments(), // marks all segments for reset makeAutoSegments(bool forceReset = false), // will create segments based on configured outputs fixInvalidSegments(), // fixes incorrect segment configuration blendSegment(const Segment &topSegment) const, // blends topSegment into pixels show(), // initiates LED output setTargetFps(unsigned fps), setupEffectData(), // add default effects to the list; defined in FX.cpp waitForIt(); // wait until frame is over (service() has finished or time for 1 frame has passed) void setRealtimePixelColor(unsigned i, uint32_t c); inline void setPixelColor(unsigned n, uint32_t c) const { if (n < getLengthTotal()) _pixels[n] = c; } // paints absolute strip pixel with index n and color c inline void resetTimebase() { timebase = 0UL - millis(); } inline void setPixelColor(unsigned n, uint8_t r, uint8_t g, uint8_t b, uint8_t w = 0) const { setPixelColor(n, RGBW32(r,g,b,w)); } inline void setPixelColor(unsigned n, CRGB c) const { setPixelColor(n, c.red, c.green, c.blue); } inline void fill(uint32_t c) const { for (size_t i = 0; i < getLengthTotal(); i++) setPixelColor(i, c); } // fill whole strip with color (inline) inline void trigger() { _triggered = true; } // Forces the next frame to be computed on all active segments. inline void setShowCallback(show_callback cb) { _callback = cb; } inline void setTransition(uint16_t t) { _transitionDur = t; } // sets transition time (in ms) inline void appendSegment(uint16_t sStart=0, uint16_t sStop=30, uint16_t sStartY = 0, uint16_t sStopY = 1) { if (_segments.size() < getMaxSegments()) _segments.emplace_back(sStart,sStop,sStartY,sStopY); } inline void suspend() { _suspend = true; } // will suspend (and canacel) strip.service() execution inline void resume() { _suspend = false; } // will resume strip.service() execution void restartRuntime(); void setTransitionMode(bool t); bool checkSegmentAlignment() const; bool hasRGBWBus() const; bool hasCCTBus() const; bool deserializeMap(unsigned n = 0); inline bool isUpdating() const { return !BusManager::canAllShow(); } // return true if the strip is being sent pixel updates inline bool isServicing() const { return _isServicing; } // returns true if strip.service() is executing inline bool hasWhiteChannel() const { return _hasWhiteChannel; } // returns true if strip contains separate white chanel inline bool isOffRefreshRequired() const { return _isOffRefreshRequired; } // returns true if strip requires regular updates (i.e. TM1814 chipset) inline bool isSuspended() const { return _suspend; } // returns true if strip.service() execution is suspended inline bool needsUpdate() const { return _triggered; } // returns true if strip received a trigger() request // uint8_t paletteBlend; // obsolete - use global paletteBlend instead of strip.paletteBlend uint8_t getActiveSegmentsNum() const; uint8_t getFirstSelectedSegId() const; uint8_t getLastActiveSegmentId() const; uint8_t getActiveSegsLightCapabilities(bool selectedOnly = false) const; uint8_t addEffect(uint8_t id, mode_ptr mode_fn, const char *mode_name); // add effect to the list; defined in FX.cpp; inline uint8_t getBrightness() const { return _brightness; } // returns current strip brightness inline static constexpr unsigned getMaxSegments() { return MAX_NUM_SEGMENTS; } // returns maximum number of supported segments (fixed value) inline uint8_t getSegmentsNum() const { return _segments.size(); } // returns currently present segments inline uint8_t getCurrSegmentId() const { return _segment_index; } // returns current segment index (only valid while strip.isServicing()) inline uint8_t getMainSegmentId() const { return _mainSegment; } // returns main segment index inline uint8_t getTargetFps() const { return _targetFps; } // returns rough FPS value for las 2s interval inline uint8_t getModeCount() const { return _modeCount; } // returns number of registered modes/effects uint16_t getLengthPhysical() const; uint16_t getLengthTotal() const; // will include virtual/nonexistent pixels in matrix inline uint16_t getFps() const { return (millis() - _lastShow > 2000) ? 0 : (FPS_MULTIPLIER * _cumulativeFps) >> FPS_CALC_SHIFT; } // Returns the refresh rate of the LED strip (_cumulativeFps is stored in fixed point) inline uint16_t getFrameTime() const { return _frametime; } // returns amount of time a frame should take (in ms) inline uint16_t getMinShowDelay() const { return MIN_FRAME_DELAY; } // returns minimum amount of time strip.service() can be delayed (constant) inline uint16_t getLength() const { return _length; } // returns actual amount of LEDs on a strip (2D matrix may have less LEDs than W*H) inline uint16_t getTransition() const { return _transitionDur; } // returns currently set transition time (in ms) inline uint16_t getMappedPixelIndex(uint16_t index) const { // convert logical address to physical if (index < customMappingSize && (realtimeMode == REALTIME_MODE_INACTIVE || realtimeRespectLedMaps)) index = customMappingTable[index]; return index; }; unsigned long now, timebase; inline uint32_t getPixelColor(unsigned n) const { return (getMappedPixelIndex(n) < getLengthTotal()) ? _pixels[n] : 0; } // returns color of pixel n, black if out of (mapped) bounds inline uint32_t getPixelColorNoMap(unsigned n) const { return (n < getLengthTotal()) ? _pixels[n] : 0; } // ignores mapping table inline uint32_t getLastShow() const { return _lastShow; } // returns millis() timestamp of last strip.show() call const char *getModeData(unsigned id = 0) const { return (id && id < _modeCount) ? _modeData[id] : PSTR("Solid"); } inline const char **getModeDataSrc() { return &(_modeData[0]); } // vectors use arrays for underlying data Segment& getSegment(unsigned id); inline Segment& getFirstSelectedSeg() { return _segments[getFirstSelectedSegId()]; } // returns reference to first segment that is "selected" inline Segment& getMainSegment() { return _segments[getMainSegmentId()]; } // returns reference to main segment inline Segment* getSegments() { return &(_segments[0]); } // returns pointer to segment vector structure (warning: use carefully) // 2D support (panels) #ifndef WLED_DISABLE_2D struct Panel { uint16_t xOffset; // x offset relative to the top left of matrix in LEDs uint16_t yOffset; // y offset relative to the top left of matrix in LEDs uint8_t width; // width of the panel uint8_t height; // height of the panel union { uint8_t options; struct { bool bottomStart : 1; // starts at bottom? bool rightStart : 1; // starts on right? bool vertical : 1; // is vertical? bool serpentine : 1; // is serpentine? }; }; Panel() : xOffset(0) , yOffset(0) , width(8) , height(8) , options(0) {} }; std::vector panel; #endif void setUpMatrix(); // sets up automatic matrix ledmap from panel configuration inline void setPixelColorXY(unsigned x, unsigned y, uint32_t c) const { setPixelColor(y * Segment::maxWidth + x, c); } inline void setPixelColorXY(unsigned x, unsigned y, byte r, byte g, byte b, byte w = 0) const { setPixelColorXY(x, y, RGBW32(r,g,b,w)); } inline void setPixelColorXY(unsigned x, unsigned y, CRGB c) const { setPixelColorXY(x, y, RGBW32(c.r,c.g,c.b,0)); } inline uint32_t getPixelColorXY(unsigned x, unsigned y) const { return getPixelColor(y * Segment::maxWidth + x); } // end 2D support bool isMatrix; struct { bool autoSegments : 1; bool correctWB : 1; bool cctFromRgb : 1; }; Segment *_currentSegment; private: uint32_t *_pixels; uint8_t *_pixelCCT; std::vector _segments; volatile bool _suspend; uint8_t _brightness; uint16_t _length; uint16_t _transitionDur; uint16_t _frametime; uint16_t _cumulativeFps; uint8_t _targetFps; // will require only 1 byte struct { bool _isServicing : 1; bool _isOffRefreshRequired : 1; //periodic refresh is required for the strip to remain off. bool _hasWhiteChannel : 1; bool _triggered : 1; }; uint8_t _segment_index; uint8_t _mainSegment; uint8_t _modeCount; std::vector _mode; // SRAM footprint: 4 bytes per element std::vector _modeData; // mode (effect) name and its slider control data array show_callback _callback; uint16_t* customMappingTable; uint16_t customMappingSize; unsigned long _lastShow; unsigned long _lastServiceShow; friend class Segment; }; extern const char JSON_mode_names[]; extern const char JSON_palette_names[]; #endif ================================================ FILE: wled00/FX_2Dfcn.cpp ================================================ /* FX_2Dfcn.cpp contains all 2D utility functions Copyright (c) 2022 Blaz Kristan (https://blaz.at/home) Licensed under the EUPL v. 1.2 or later Adapted from code originally licensed under the MIT license Parts of the code adapted from WLED Sound Reactive */ #include "wled.h" // setUpMatrix() - constructs ledmap array from matrix of panels with WxH pixels // this converts physical (possibly irregular) LED arrangement into well defined // array of logical pixels: fist entry corresponds to left-topmost logical pixel // followed by horizontal pixels, when Segment::maxWidth logical pixels are added they // are followed by next row (down) of Segment::maxWidth pixels (and so forth) // note: matrix may be comprised of multiple panels each with different orientation // but ledmap takes care of that. ledmap is constructed upon initialization // so matrix should disable regular ledmap processing // WARNING: effect drawing has to be suspended (strip.suspend()) or must be called from loop() context void WS2812FX::setUpMatrix() { #ifndef WLED_DISABLE_2D // isMatrix is set in cfg.cpp or set.cpp if (isMatrix) { // calculate width dynamically because it may have gaps Segment::maxWidth = 1; Segment::maxHeight = 1; for (const Panel &p : panel) { if (p.xOffset + p.width > Segment::maxWidth) { Segment::maxWidth = p.xOffset + p.width; } if (p.yOffset + p.height > Segment::maxHeight) { Segment::maxHeight = p.yOffset + p.height; } } // safety check if (Segment::maxWidth * Segment::maxHeight > MAX_LEDS || Segment::maxWidth > 255 || Segment::maxHeight > 255 || Segment::maxWidth <= 1 || Segment::maxHeight <= 1) { DEBUG_PRINTLN(F("2D Bounds error.")); isMatrix = false; Segment::maxWidth = _length; Segment::maxHeight = 1; panel.clear(); // release memory allocated by panels panel.shrink_to_fit(); // release memory if allocated resetSegments(); return; } customMappingSize = 0; // prevent use of mapping if anything goes wrong d_free(customMappingTable); // Segment::maxWidth and Segment::maxHeight are set according to panel layout // and the product will include at least all leds in matrix // if actual LEDs are more, getLengthTotal() will return correct number of LEDs customMappingTable = static_cast(d_malloc(sizeof(uint16_t)*getLengthTotal())); // prefer to not use SPI RAM if (customMappingTable) { customMappingSize = getLengthTotal(); // fill with empty in case we don't fill the entire matrix unsigned matrixSize = Segment::maxWidth * Segment::maxHeight; for (unsigned i = 0; ias(); gapSize = map.size(); if (!map.isNull() && gapSize >= matrixSize) { // not an empty map gapTable = static_cast(p_malloc(gapSize)); if (gapTable) for (size_t i = 0; i < gapSize; i++) { gapTable[i] = constrain(map[i], -1, 1); } } } DEBUG_PRINTLN(F("Gaps loaded.")); releaseJSONBufferLock(); } unsigned x, y, pix=0; //pixel for (const Panel &p : panel) { unsigned h = p.vertical ? p.height : p.width; unsigned v = p.vertical ? p.width : p.height; for (size_t j = 0; j < v; j++){ for(size_t i = 0; i < h; i++) { y = (p.vertical?p.rightStart:p.bottomStart) ? v-j-1 : j; x = (p.vertical?p.bottomStart:p.rightStart) ? h-i-1 : i; x = p.serpentine && j%2 ? h-x-1 : x; size_t index = (p.yOffset + (p.vertical?x:y)) * Segment::maxWidth + p.xOffset + (p.vertical?y:x); if (!gapTable || (gapTable && gapTable[index] > 0)) customMappingTable[index] = pix; // a useful pixel (otherwise -1 is retained) if (!gapTable || (gapTable && gapTable[index] >= 0)) pix++; // not a missing pixel } } } // delete gap array as we no longer need it p_free(gapTable); #ifdef WLED_DEBUG DEBUG_PRINT(F("Matrix ledmap:")); for (unsigned i=0; i stop the clipping range is inverted bool Segment::isPixelXYClipped(int x, int y) const { if (blendingStyle != BLEND_STYLE_FADE && isInTransition() && _clipStart != _clipStop) { const bool invertX = _clipStart > _clipStop; const bool invertY = _clipStartY > _clipStopY; const int cStartX = invertX ? _clipStop : _clipStart; const int cStopX = invertX ? _clipStart : _clipStop; const int cStartY = invertY ? _clipStopY : _clipStartY; const int cStopY = invertY ? _clipStartY : _clipStopY; if (blendingStyle == BLEND_STYLE_FAIRY_DUST) { const unsigned width = cStopX - cStartX; // assumes full segment width (faster than virtualWidth()) const unsigned len = width * (cStopY - cStartY); // assumes full segment height (faster than virtualHeight()) if (len < 2) return false; const unsigned shuffled = hashInt(x + y * width) % len; const unsigned pos = (shuffled * 0xFFFFU) / len; return progress() <= pos; } if (blendingStyle == BLEND_STYLE_CIRCULAR_IN || blendingStyle == BLEND_STYLE_CIRCULAR_OUT) { const int cx = (cStopX-cStartX+1) / 2; const int cy = (cStopY-cStartY+1) / 2; const bool out = (blendingStyle == BLEND_STYLE_CIRCULAR_OUT); const unsigned prog = out ? progress() : 0xFFFFU - progress(); int radius2 = max(cx, cy) * prog / 0xFFFF; radius2 = 2 * radius2 * radius2; if (radius2 == 0) return out; const int dx = x - cx; const int dy = y - cy; const bool outside = dx * dx + dy * dy > radius2; return out ? outside : !outside; } bool xInside = (x >= cStartX && x < cStopX); if (invertX) xInside = !xInside; bool yInside = (y >= cStartY && y < cStopY); if (invertY) yInside = !yInside; const bool clip = blendingStyle == BLEND_STYLE_OUTSIDE_IN ? xInside || yInside : xInside && yInside; return !clip; } return false; } void IRAM_ATTR_YN Segment::setPixelColorXY(int x, int y, uint32_t col) const { if (!isActive()) return; // not active if ((unsigned)x >= vWidth() || (unsigned)y >= vHeight()) return; // if pixel would fall out of virtual segment just exit setPixelColorXYRaw(x, y, col); } #ifdef WLED_USE_AA_PIXELS // anti-aliased version of setPixelColorXY() void Segment::setPixelColorXY(float x, float y, uint32_t col, bool aa) const { if (!isActive()) return; // not active if (x<0.0f || x>1.0f || y<0.0f || y>1.0f) return; // not normalized float fX = x * (vWidth()-1); float fY = y * (vHeight()-1); if (aa) { unsigned xL = roundf(fX-0.49f); unsigned xR = roundf(fX+0.49f); unsigned yT = roundf(fY-0.49f); unsigned yB = roundf(fY+0.49f); float dL = (fX - xL)*(fX - xL); float dR = (xR - fX)*(xR - fX); float dT = (fY - yT)*(fY - yT); float dB = (yB - fY)*(yB - fY); uint32_t cXLYT = getPixelColorXY(xL, yT); uint32_t cXRYT = getPixelColorXY(xR, yT); uint32_t cXLYB = getPixelColorXY(xL, yB); uint32_t cXRYB = getPixelColorXY(xR, yB); if (xL!=xR && yT!=yB) { setPixelColorXY(xL, yT, color_blend(col, cXLYT, uint8_t(sqrtf(dL*dT)*255.0f))); // blend TL pixel setPixelColorXY(xR, yT, color_blend(col, cXRYT, uint8_t(sqrtf(dR*dT)*255.0f))); // blend TR pixel setPixelColorXY(xL, yB, color_blend(col, cXLYB, uint8_t(sqrtf(dL*dB)*255.0f))); // blend BL pixel setPixelColorXY(xR, yB, color_blend(col, cXRYB, uint8_t(sqrtf(dR*dB)*255.0f))); // blend BR pixel } else if (xR!=xL && yT==yB) { setPixelColorXY(xR, yT, color_blend(col, cXLYT, uint8_t(dL*255.0f))); // blend L pixel setPixelColorXY(xR, yT, color_blend(col, cXRYT, uint8_t(dR*255.0f))); // blend R pixel } else if (xR==xL && yT!=yB) { setPixelColorXY(xR, yT, color_blend(col, cXLYT, uint8_t(dT*255.0f))); // blend T pixel setPixelColorXY(xL, yB, color_blend(col, cXLYB, uint8_t(dB*255.0f))); // blend B pixel } else { setPixelColorXY(xL, yT, col); // exact match (x & y land on a pixel) } } else { setPixelColorXY(uint16_t(roundf(fX)), uint16_t(roundf(fY)), col); } } #endif // returns RGBW values of pixel uint32_t IRAM_ATTR_YN Segment::getPixelColorXY(int x, int y) const { if (!isActive()) return 0; // not active if ((unsigned)x >= vWidth() || (unsigned)y >= vHeight()) return 0; // if pixel would fall out of virtual segment just exit return getPixelColorXYRaw(x,y); } // 2D blurring, can be asymmetrical void Segment::blur2D(uint8_t blur_x, uint8_t blur_y, bool smear) const { if (!isActive()) return; // not active const unsigned cols = vWidth(); const unsigned rows = vHeight(); const auto XY = [&](unsigned x, unsigned y){ return x + y*cols; }; if (blur_x) { const uint8_t keepx = smear ? 255 : 255 - blur_x; const uint8_t seepx = blur_x >> 1; for (unsigned row = 0; row < rows; row++) { // blur rows (x direction) // handle first pixel in row to avoid conditional in loop (faster) uint32_t cur = getPixelColorRaw(XY(0, row)); uint32_t carryover = fast_color_scale(cur, seepx); setPixelColorRaw(XY(0, row), fast_color_scale(cur, keepx)); for (unsigned x = 1; x < cols; x++) { cur = getPixelColorRaw(XY(x, row)); uint32_t part = fast_color_scale(cur, seepx); cur = fast_color_scale(cur, keepx); cur = color_add(cur, carryover); setPixelColorRaw(XY(x - 1, row), color_add(getPixelColorRaw(XY(x-1, row)), part)); // previous pixel setPixelColorRaw(XY(x, row), cur); // current pixel carryover = part; } } } if (blur_y) { const uint8_t keepy = smear ? 255 : 255 - blur_y; const uint8_t seepy = blur_y >> 1; for (unsigned col = 0; col < cols; col++) { // handle first pixel in column uint32_t cur = getPixelColorRaw(XY(col, 0)); uint32_t carryover = fast_color_scale(cur, seepy); setPixelColorRaw(XY(col, 0), fast_color_scale(cur, keepy)); for (unsigned y = 1; y < rows; y++) { cur = getPixelColorRaw(XY(col, y)); uint32_t part = fast_color_scale(cur, seepy); cur = fast_color_scale(cur, keepy); cur = color_add(cur, carryover); setPixelColorRaw(XY(col, y - 1), color_add(getPixelColorRaw(XY(col, y-1)), part)); // previous pixel setPixelColorRaw(XY(col, y), cur); // current pixel carryover = part; } } } } /* // 2D Box blur void Segment::box_blur(unsigned radius, bool smear) { if (!isActive() || radius == 0) return; // not active if (radius > 3) radius = 3; const unsigned d = (1 + 2*radius) * (1 + 2*radius); // averaging divisor const unsigned cols = vWidth(); const unsigned rows = vHeight(); uint16_t *tmpRSum = new uint16_t[cols*rows]; uint16_t *tmpGSum = new uint16_t[cols*rows]; uint16_t *tmpBSum = new uint16_t[cols*rows]; uint16_t *tmpWSum = new uint16_t[cols*rows]; // fill summed-area table (https://en.wikipedia.org/wiki/Summed-area_table) for (unsigned x = 0; x < cols; x++) { unsigned rS, gS, bS, wS; unsigned index; rS = gS = bS = wS = 0; for (unsigned y = 0; y < rows; y++) { index = x * cols + y; if (x > 0) { unsigned index2 = (x - 1) * cols + y; tmpRSum[index] = tmpRSum[index2]; tmpGSum[index] = tmpGSum[index2]; tmpBSum[index] = tmpBSum[index2]; tmpWSum[index] = tmpWSum[index2]; } else { tmpRSum[index] = 0; tmpGSum[index] = 0; tmpBSum[index] = 0; tmpWSum[index] = 0; } uint32_t c = getPixelColorXY(x, y); rS += R(c); gS += G(c); bS += B(c); wS += W(c); tmpRSum[index] += rS; tmpGSum[index] += gS; tmpBSum[index] += bS; tmpWSum[index] += wS; } } // do a box blur using pre-calculated sums for (unsigned x = 0; x < cols; x++) { for (unsigned y = 0; y < rows; y++) { // sum = D + A - B - C where k = (x,y) // +----+-+---- (x) // | | | // +----A-B // | |k| // +----C-D // | //(y) unsigned x0 = x < radius ? 0 : x - radius; unsigned y0 = y < radius ? 0 : y - radius; unsigned x1 = x >= cols - radius ? cols - 1 : x + radius; unsigned y1 = y >= rows - radius ? rows - 1 : y + radius; unsigned A = x0 * cols + y0; unsigned B = x1 * cols + y0; unsigned C = x0 * cols + y1; unsigned D = x1 * cols + y1; unsigned r = tmpRSum[D] + tmpRSum[A] - tmpRSum[C] - tmpRSum[B]; unsigned g = tmpGSum[D] + tmpGSum[A] - tmpGSum[C] - tmpGSum[B]; unsigned b = tmpBSum[D] + tmpBSum[A] - tmpBSum[C] - tmpBSum[B]; unsigned w = tmpWSum[D] + tmpWSum[A] - tmpWSum[C] - tmpWSum[B]; setPixelColorXY(x, y, RGBW32(r/d, g/d, b/d, w/d)); } } delete[] tmpRSum; delete[] tmpGSum; delete[] tmpBSum; delete[] tmpWSum; } */ void Segment::moveX(int delta, bool wrap) const { if (!isActive() || !delta) return; // not active const int vW = vWidth(); // segment width in logical pixels (can be 0 if segment is inactive) const int vH = vHeight(); // segment height in logical pixels (is always >= 1) const auto XY = [&](unsigned x, unsigned y){ return x + y*vW; }; int absDelta = abs(delta); if (absDelta >= vW) return; uint32_t newPxCol[vW]; int newDelta; int stop = vW; int start = 0; if (wrap) newDelta = (delta + vW) % vW; // +cols in case delta < 0 else { if (delta < 0) start = absDelta; stop = vW - absDelta; newDelta = delta > 0 ? delta : 0; } for (int y = 0; y < vH; y++) { for (int x = 0; x < stop; x++) { int srcX = x + newDelta; if (wrap) srcX %= vW; // Wrap using modulo when `wrap` is true newPxCol[x] = getPixelColorRaw(XY(srcX, y)); } for (int x = 0; x < stop; x++) setPixelColorRaw(XY(x + start, y), newPxCol[x]); } } void Segment::moveY(int delta, bool wrap) const { if (!isActive() || !delta) return; // not active const int vW = vWidth(); // segment width in logical pixels (can be 0 if segment is inactive) const int vH = vHeight(); // segment height in logical pixels (is always >= 1) const auto XY = [&](unsigned x, unsigned y){ return x + y*vW; }; int absDelta = abs(delta); if (absDelta >= vH) return; uint32_t newPxCol[vH]; int newDelta; int stop = vH; int start = 0; if (wrap) newDelta = (delta + vH) % vH; // +rows in case delta < 0 else { if (delta < 0) start = absDelta; stop = vH - absDelta; newDelta = delta > 0 ? delta : 0; } for (int x = 0; x < vW; x++) { for (int y = 0; y < stop; y++) { int srcY = y + newDelta; if (wrap) srcY %= vH; // Wrap using modulo when `wrap` is true newPxCol[y] = getPixelColorRaw(XY(x, srcY)); } for (int y = 0; y < stop; y++) setPixelColorRaw(XY(x, y + start), newPxCol[y]); } } // move() - move all pixels in desired direction delta number of pixels // @param dir direction: 0=left, 1=left-up, 2=up, 3=right-up, 4=right, 5=right-down, 6=down, 7=left-down // @param delta number of pixels to move // @param wrap around void Segment::move(unsigned dir, unsigned delta, bool wrap) const { if (delta==0) return; switch (dir) { case 0: moveX( delta, wrap); break; case 1: moveX( delta, wrap); moveY( delta, wrap); break; case 2: moveY( delta, wrap); break; case 3: moveX(-delta, wrap); moveY( delta, wrap); break; case 4: moveX(-delta, wrap); break; case 5: moveX(-delta, wrap); moveY(-delta, wrap); break; case 6: moveY(-delta, wrap); break; case 7: moveX( delta, wrap); moveY(-delta, wrap); break; } } void Segment::drawCircle(uint16_t cx, uint16_t cy, uint8_t radius, uint32_t col, bool soft) const { if (!isActive() || radius == 0) return; // not active if (soft) { // Xiaolin Wu’s algorithm const int rsq = radius*radius; int x = 0; int y = radius; unsigned oldFade = 0; while (x < y) { float yf = sqrtf(float(rsq - x*x)); // needs to be floating point uint8_t fade = float(0xFF) * (ceilf(yf) - yf); // how much color to keep if (oldFade > fade) y--; oldFade = fade; int px, py; for (uint8_t i = 0; i < 16; i++) { int swaps = (i & 0x4 ? 1 : 0); // 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1 int adj = (i < 8) ? 0 : 1; // 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1 int dx = (i & 1) ? -1 : 1; // 1, -1, 1, -1, 1, -1, 1, -1, 1, -1, 1, -1, 1, -1, 1, -1 int dy = (i & 2) ? -1 : 1; // 1, 1, -1, -1, 1, 1, -1, -1, 1, 1, -1, -1, 1, 1, -1, -1 if (swaps) { px = cx + (y - adj) * dx; py = cy + x * dy; } else { px = cx + x * dx; py = cy + (y - adj) * dy; } uint32_t pixCol = getPixelColorXY(px, py); setPixelColorXY(px, py, adj ? color_blend(pixCol, col, fade) : color_blend(col, pixCol, fade)); } x++; } } else { // Bresenham’s Algorithm int d = 3 - (2*radius); int y = radius, x = 0; while (y >= x) { for (int i = 0; i < 4; i++) { int dx = (i & 1) ? -x : x; int dy = (i & 2) ? -y : y; setPixelColorXY(cx + dx, cy + dy, col); setPixelColorXY(cx + dy, cy + dx, col); } x++; if (d > 0) { y--; d += 4 * (x - y) + 10; } else { d += 4 * x + 6; } } } } // by stepko, taken from https://editor.soulmatelights.com/gallery/573-blobs void Segment::fillCircle(uint16_t cx, uint16_t cy, uint8_t radius, uint32_t col, bool soft) const { if (!isActive() || radius == 0) return; // not active const int vW = vWidth(); // segment width in logical pixels (can be 0 if segment is inactive) const int vH = vHeight(); // segment height in logical pixels (is always >= 1) // draw soft bounding circle if (soft) drawCircle(cx, cy, radius, col, soft); // fill it for (int y = -radius; y <= radius; y++) { for (int x = -radius; x <= radius; x++) { if (x * x + y * y <= radius * radius && int(cx)+x >= 0 && int(cy)+y >= 0 && int(cx)+x < vW && int(cy)+y < vH) setPixelColorXY(cx + x, cy + y, col); } } } //line function void Segment::drawLine(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1, uint32_t c, bool soft) const { if (!isActive()) return; // not active const int vW = vWidth(); // segment width in logical pixels (can be 0 if segment is inactive) const int vH = vHeight(); // segment height in logical pixels (is always >= 1) if (x0 >= vW || x1 >= vW || y0 >= vH || y1 >= vH) return; const int dx = abs(x1-x0), sx = x0 dx; if (steep) { // we need to go along longest dimension std::swap(x0,y0); std::swap(x1,y1); } if (x0 > x1) { // we need to go in increasing fashion std::swap(x0,x1); std::swap(y0,y1); } float gradient = x1-x0 == 0 ? 1.0f : float(y1-y0) / float(x1-x0); float intersectY = y0; for (int x = x0; x <= x1; x++) { uint8_t keep = float(0xFF) * (intersectY-int(intersectY)); // how much color to keep uint8_t seep = 0xFF - keep; // how much background to keep int y = int(intersectY); if (steep) std::swap(x,y); // temporaryly swap if steep // pixel coverage is determined by fractional part of y co-ordinate blendPixelColorXY(x, y, c, seep); blendPixelColorXY(x+int(steep), y+int(!steep), c, keep); intersectY += gradient; if (steep) std::swap(x,y); // restore if steep } } else { // Bresenham's algorithm int err = (dx>dy ? dx : -dy)/2; // error direction for (;;) { setPixelColorXY(x0, y0, c); if (x0==x1 && y0==y1) break; int e2 = err; if (e2 >-dx) { err -= dy; x0 += sx; } if (e2 < dy) { err += dx; y0 += sy; } } } } #include "src/font/console_font_4x6.h" #include "src/font/console_font_5x8.h" #include "src/font/console_font_5x12.h" #include "src/font/console_font_6x8.h" #include "src/font/console_font_7x9.h" // draws a raster font character on canvas // only supports: 4x6=24, 5x8=40, 5x12=60, 6x8=48 and 7x9=63 fonts ATM void Segment::drawCharacter(unsigned char chr, int16_t x, int16_t y, uint8_t w, uint8_t h, uint32_t color, uint32_t col2, int8_t rotate) const { if (!isActive()) return; // not active if (chr < 32 || chr > 126) return; // only ASCII 32-126 supported chr -= 32; // align with font table entries const int font = w*h; // if col2 == BLACK then use currently selected palette for gradient otherwise create gradient from color and col2 CRGBPalette16 grad = col2 ? CRGBPalette16(CRGB(color), CRGB(col2)) : SEGPALETTE; // selected palette as gradient for (int i = 0; i= (int)vWidth() || y0 < 0 || y0 >= (int)vHeight()) continue; // drawing off-screen if (((bits>>(j+(8-w))) & 0x01)) { // bit set setPixelColorXYRaw(x0, y0, c.color32); } } } } #define WU_WEIGHT(a,b) ((uint8_t) (((a)*(b)+(a)+(b))>>8)) void Segment::wu_pixel(uint32_t x, uint32_t y, CRGB c) const { //awesome wu_pixel procedure by reddit u/sutaburosu if (!isActive()) return; // not active // extract the fractional parts and derive their inverses unsigned xx = x & 0xff, yy = y & 0xff, ix = 255 - xx, iy = 255 - yy; // calculate the intensities for each affected pixel uint8_t wu[4] = {WU_WEIGHT(ix, iy), WU_WEIGHT(xx, iy), WU_WEIGHT(ix, yy), WU_WEIGHT(xx, yy)}; // multiply the intensities by the colour, and saturating-add them to the pixels for (int i = 0; i < 4; i++) { int wu_x = (x >> 8) + (i & 1); // precalculate x int wu_y = (y >> 8) + ((i >> 1) & 1); // precalculate y CRGB led = getPixelColorXY(wu_x, wu_y); CRGB oldLed = led; led.r = qadd8(led.r, c.r * wu[i] >> 8); led.g = qadd8(led.g, c.g * wu[i] >> 8); led.b = qadd8(led.b, c.b * wu[i] >> 8); if (led != oldLed) setPixelColorXY(wu_x, wu_y, led); // don't repaint if same color } } #undef WU_WEIGHT #endif // WLED_DISABLE_2D ================================================ FILE: wled00/FX_fcn.cpp ================================================ /* WS2812FX_fcn.cpp contains all utility functions Harm Aldick - 2016 www.aldick.org Copyright (c) 2016 Harm Aldick Licensed under the EUPL v. 1.2 or later Adapted from code originally licensed under the MIT license Modified heavily for WLED */ #include "wled.h" #include "FXparticleSystem.h" // TODO: better define the required function (mem service) in FX.h? /* Custom per-LED mapping has moved! Create a file "ledmap.json" using the edit page. this is just an example (30 LEDs). It will first set all even, then all uneven LEDs. {"map":[ 0, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22, 24, 26, 28, 1, 3, 5, 7, 9, 11, 13, 15, 17, 19, 21, 23, 25, 27, 29]} another example. Switches direction every 5 LEDs. {"map":[ 0, 1, 2, 3, 4, 9, 8, 7, 6, 5, 10, 11, 12, 13, 14, 19, 18, 17, 16, 15, 20, 21, 22, 23, 24, 29, 28, 27, 26, 25]} */ #if MAX_NUM_SEGMENTS < WLED_MAX_BUSSES #error "Max segments must be at least max number of busses!" #endif /////////////////////////////////////////////////////////////////////////////// // Segment class implementation /////////////////////////////////////////////////////////////////////////////// unsigned Segment::_usedSegmentData = 0U; // amount of RAM all segments use for their data[] uint16_t Segment::maxWidth = DEFAULT_LED_COUNT; uint16_t Segment::maxHeight = 1; unsigned Segment::_vLength = 0; unsigned Segment::_vWidth = 0; unsigned Segment::_vHeight = 0; uint32_t Segment::_currentColors[NUM_COLORS] = {0,0,0}; CRGBPalette16 Segment::_currentPalette = CRGBPalette16(CRGB::Black); CRGBPalette16 Segment::_randomPalette = generateRandomPalette(); // was CRGBPalette16(DEFAULT_COLOR); CRGBPalette16 Segment::_newRandomPalette = generateRandomPalette(); // was CRGBPalette16(DEFAULT_COLOR); uint16_t Segment::_lastPaletteChange = 0; // in seconds; perhaps it should be per segment uint16_t Segment::_nextPaletteBlend = 0; // in millis bool Segment::_modeBlend = false; uint16_t Segment::_clipStart = 0; uint16_t Segment::_clipStop = 0; uint8_t Segment::_clipStartY = 0; uint8_t Segment::_clipStopY = 1; // copy constructor Segment::Segment(const Segment &orig) { //DEBUG_PRINTF_P(PSTR("-- Copy segment constructor: %p -> %p\n"), &orig, this); memcpy((void*)this, (void*)&orig, sizeof(Segment)); _t = nullptr; // copied segment cannot be in transition name = nullptr; data = nullptr; _dataLen = 0; pixels = nullptr; if (!stop) return; // nothing to do if segment is inactive/invalid if (orig.pixels) { // allocate pixel buffer: prefer IRAM/PSRAM pixels = static_cast(allocate_buffer(orig.length() * sizeof(uint32_t), BFRALLOC_PREFER_PSRAM | BFRALLOC_NOBYTEACCESS)); if (pixels) { memcpy(pixels, orig.pixels, sizeof(uint32_t) * orig.length()); if (orig.name) { name = static_cast(allocate_buffer(strlen(orig.name)+1, BFRALLOC_PREFER_PSRAM)); if (name) strcpy(name, orig.name); } if (orig.data) { if (allocateData(orig._dataLen)) memcpy(data, orig.data, orig._dataLen); } } else { DEBUGFX_PRINTLN(F("!!! Not enough RAM for pixel buffer !!!")); errorFlag = ERR_NORAM_PX; stop = 0; // mark segment as inactive/invalid } } else stop = 0; // mark segment as inactive/invalid } // move constructor Segment::Segment(Segment &&orig) noexcept { //DEBUG_PRINTF_P(PSTR("-- Move segment constructor: %p -> %p\n"), &orig, this); memcpy((void*)this, (void*)&orig, sizeof(Segment)); orig._t = nullptr; // old segment cannot be in transition any more orig.name = nullptr; orig.data = nullptr; orig._dataLen = 0; orig.pixels = nullptr; } // copy assignment Segment& Segment::operator= (const Segment &orig) { //DEBUG_PRINTF_P(PSTR("-- Copying segment: %p -> %p\n"), &orig, this); if (this != &orig) { // clean destination if (name) { p_free(name); name = nullptr; } if (_t) stopTransition(); // also erases _t deallocateData(); p_free(pixels); pixels = nullptr; // copy source memcpy((void*)this, (void*)&orig, sizeof(Segment)); // erase pointers to allocated data data = nullptr; _dataLen = 0; if (!stop) return *this; // nothing to do if segment is inactive/invalid // copy source data if (orig.pixels) { // allocate pixel buffer: prefer IRAM/PSRAM pixels = static_cast(allocate_buffer(orig.length() * sizeof(uint32_t), BFRALLOC_PREFER_PSRAM | BFRALLOC_NOBYTEACCESS)); if (pixels) { memcpy(pixels, orig.pixels, sizeof(uint32_t) * orig.length()); if (orig.name) { name = static_cast(allocate_buffer(strlen(orig.name)+1, BFRALLOC_PREFER_PSRAM)); if (name) strcpy(name, orig.name); } if (orig.data) { if (allocateData(orig._dataLen)) memcpy(data, orig.data, orig._dataLen); } } else { DEBUG_PRINTLN(F("!!! Not enough RAM for pixel buffer !!!")); errorFlag = ERR_NORAM_PX; stop = 0; // mark segment as inactive/invalid } } else stop = 0; // mark segment as inactive/invalid } return *this; } // move assignment Segment& Segment::operator= (Segment &&orig) noexcept { //DEBUG_PRINTF_P(PSTR("-- Moving segment: %p -> %p\n"), &orig, this); if (this != &orig) { if (name) { p_free(name); name = nullptr; } // free old name if (_t) stopTransition(); // also erases _t deallocateData(); // free old runtime data p_free(pixels); // free old pixel buffer // move source data memcpy((void*)this, (void*)&orig, sizeof(Segment)); orig.name = nullptr; orig.data = nullptr; orig._dataLen = 0; orig.pixels = nullptr; orig._t = nullptr; // old segment cannot be in transition } return *this; } // allocates effect data buffer on heap and initialises (erases) it bool Segment::allocateData(size_t len) { if (len == 0) return false; // nothing to do if (data && _dataLen >= len) { // already allocated enough (reduce fragmentation) if (call == 0) { if (_dataLen < FAIR_DATA_PER_SEG) { // segment data is small //DEBUG_PRINTF_P(PSTR("-- Clearing data (%d): %p\n"), len, this); memset(data, 0, len); // erase buffer if called during effect initialisation return true; // no need to reallocate } } else return true; } //DEBUG_PRINTF_P(PSTR("-- Allocating data (%d): %p\n"), len, this); // limit to MAX_SEGMENT_DATA if there is no PSRAM, otherwise prefer functionality over speed #ifndef BOARD_HAS_PSRAM if (Segment::getUsedSegmentData() + len - _dataLen > MAX_SEGMENT_DATA) { // not enough memory DEBUG_PRINTF_P(PSTR("SegmentData limit reached: %d/%d\n"), len, Segment::getUsedSegmentData()); errorFlag = ERR_NORAM; return false; } #endif if (data) { d_free(data); // free data and try to allocate again (segment buffer may be blocking contiguous heap) Segment::addUsedSegmentData(-_dataLen); // subtract buffer size } data = static_cast(allocate_buffer(len, BFRALLOC_PREFER_DRAM | BFRALLOC_CLEAR)); // prefer DRAM over PSRAM for speed if (data) { Segment::addUsedSegmentData(len); _dataLen = len; //DEBUG_PRINTF_P(PSTR("--- Allocated data (%p): %d/%d -> %p\n"), this, len, Segment::getUsedSegmentData(), data); return true; } // allocation failed DEBUG_PRINTLN(F("!!! Allocation failed. !!!")); errorFlag = ERR_NORAM; return false; } void Segment::deallocateData() { if (!data) { _dataLen = 0; return; } if ((Segment::getUsedSegmentData() > 0) && (_dataLen > 0)) { // check that we don't have a dangling / inconsistent data pointer //DEBUG_PRINTF_P(PSTR("--- Released data (%p): %d/%d -> %p\n"), this, _dataLen, Segment::getUsedSegmentData(), data); d_free(data); } else { DEBUG_PRINTF_P(PSTR("---- Released data (%p): inconsistent UsedSegmentData (%d/%d), cowardly refusing to free nothing.\n"), this, _dataLen, Segment::getUsedSegmentData()); } data = nullptr; Segment::addUsedSegmentData(_dataLen <= Segment::getUsedSegmentData() ? -_dataLen : -Segment::getUsedSegmentData()); _dataLen = 0; } /** * If reset of this segment was requested, clears runtime * settings of this segment. * Must not be called while an effect mode function is running * because it could access the data buffer and this method * may free that data buffer. */ void Segment::resetIfRequired() { if (!reset || !isActive()) return; //DEBUG_PRINTF_P(PSTR("-- Segment reset: %p\n"), this); if (data && _dataLen > 0) { if (_dataLen > FAIR_DATA_PER_SEG) deallocateData(); // do not keep large allocations else memset(data, 0, _dataLen); // can prevent heap fragmentation DEBUG_PRINTF_P(PSTR("-- Segment %p reset, data cleared\n"), this); } if (pixels) for (size_t i = 0; i < length(); i++) pixels[i] = BLACK; // clear pixel buffer step = 0; call = 0; aux0 = 0; aux1 = 0; reset = false; #ifdef WLED_ENABLE_GIF endImagePlayback(this); #endif } CRGBPalette16 &Segment::loadPalette(CRGBPalette16 &targetPalette, uint8_t pal) { // there is one randomy generated palette (1) followed by 4 palettes created from segment colors (2-5) // those are followed by 7 fastled palettes (6-12) and 59 gradient palettes (13-71) // then come the custom palettes (255,254,...) growing downwards from 255 (255 being 1st custom palette) // palette 0 is a varying palette depending on effect and may be replaced by segment's color if so // instructed in color_from_palette() if (pal >= FIXED_PALETTE_COUNT && pal <= 255-customPalettes.size()) pal = 0; // out of bounds palette //default palette. Differs depending on effect if (pal == 0) pal = _default_palette; // _default_palette is set in setMode() switch (pal) { case 0: //default palette. Exceptions for specific effects above targetPalette = PartyColors_p; break; case 1: //randomly generated palette targetPalette = _randomPalette; //random palette is generated at intervals in handleRandomPalette() break; case 2: {//primary color only CRGB prim = colors[0]; targetPalette = CRGBPalette16(prim); break;} case 3: {//primary + secondary CRGB prim = colors[0]; CRGB sec = colors[1]; targetPalette = CRGBPalette16(prim,prim,sec,sec); break;} case 4: {//primary + secondary + tertiary CRGB prim = colors[0]; CRGB sec = colors[1]; CRGB ter = colors[2]; targetPalette = CRGBPalette16(ter,sec,prim); break;} case 5: {//primary + secondary (+tertiary if not off), more distinct CRGB prim = colors[0]; CRGB sec = colors[1]; if (colors[2]) { CRGB ter = colors[2]; targetPalette = CRGBPalette16(prim,prim,prim,prim,prim,sec,sec,sec,sec,sec,ter,ter,ter,ter,ter,prim); } else { targetPalette = CRGBPalette16(prim,prim,prim,prim,prim,prim,prim,prim,sec,sec,sec,sec,sec,sec,sec,sec); } break;} default: //progmem palettes if (pal > 255 - customPalettes.size()) { targetPalette = customPalettes[255-pal]; // we checked bounds above } else if (pal < DYNAMIC_PALETTE_COUNT + FASTLED_PALETTE_COUNT) { // palette 6 - 12, fastled palettes targetPalette = *fastledPalettes[pal - DYNAMIC_PALETTE_COUNT]; } else { byte tcp[72]; memcpy_P(tcp, (byte*)pgm_read_dword(&(gGradientPalettes[pal - (DYNAMIC_PALETTE_COUNT + FASTLED_PALETTE_COUNT)])), sizeof(tcp)); targetPalette.loadDynamicGradientPalette(tcp); } break; } return targetPalette; } // starting a transition has to occur before change so we get current values 1st void Segment::startTransition(uint16_t dur, bool segmentCopy) { if (dur == 0 || !isActive()) { if (isInTransition()) _t->_dur = 0; return; } if (isInTransition()) { if (segmentCopy && !_t->_oldSegment) { // already in transition but segment copy requested and not yet created _t->_oldSegment = new(std::nothrow) Segment(*this); // store/copy current segment settings _t->_start = millis(); // restart countdown _t->_dur = dur; _t->_prevPaletteBlends = 0; if (_t->_oldSegment) { _t->_oldSegment->palette = _t->_palette; // restore original palette and colors (from start of transition) for (unsigned i = 0; i < NUM_COLORS; i++) _t->_oldSegment->colors[i] = _t->_colors[i]; DEBUGFX_PRINTF_P(PSTR("-- Updated transition with segment copy: S=%p T(%p) O[%p] OP[%p]\n"), this, _t, _t->_oldSegment, _t->_oldSegment->pixels); if (!_t->_oldSegment->isActive()) stopTransition(); } } return; } // no previous transition running, start by allocating memory for segment copy _t = new(std::nothrow) Transition(dur); if (_t) { _t->_bri = on ? opacity : 0; _t->_cct = cct; _t->_palette = palette; #ifndef WLED_SAVE_RAM loadPalette(_t->_palT, palette); #endif for (int i=0; i_colors[i] = colors[i]; if (segmentCopy) _t->_oldSegment = new(std::nothrow) Segment(*this); // store/copy current segment settings if (_t->_oldSegment) { DEBUGFX_PRINTF_P(PSTR("-- Started transition: S=%p T(%p) O[%p] OP[%p]\n"), this, _t, _t->_oldSegment, _t->_oldSegment->pixels); if (!_t->_oldSegment->isActive()) stopTransition(); } else { DEBUGFX_PRINTF_P(PSTR("-- Started transition without old segment: S=%p T(%p)\n"), this, _t); } }; } void Segment::stopTransition() { DEBUG_PRINTF_P(PSTR("-- Stopping transition: S=%p T(%p) O[%p]\n"), this, _t, _t->_oldSegment); delete _t; _t = nullptr; } // sets transition progress variable (0-65535) based on time passed since transition start void Segment::updateTransitionProgress() const { if (isInTransition()) { _t->_progress = 0xFFFF; unsigned diff = millis() - _t->_start; if (_t->_dur > 0 && diff < _t->_dur) _t->_progress = diff * 0xFFFFU / _t->_dur; } } // will return segment's CCT during a transition // isPreviousMode() is actually not implemented for CCT in strip.service() as WLED does not support per-pixel CCT uint8_t Segment::currentCCT() const { unsigned prog = progress(); if (prog < 0xFFFFU) { if (blendingStyle == BLEND_STYLE_FADE) return (cct * prog + (_t->_cct * (0xFFFFU - prog))) / 0xFFFFU; //else return Segment::isPreviousMode() ? _t->_cct : cct; } return cct; } // will return segment's opacity during a transition (blending it with old in case of FADE transition) uint8_t Segment::currentBri() const { unsigned prog = progress(); unsigned curBri = on ? opacity : 0; if (prog < 0xFFFFU) { // this will blend opacity in new mode if style is FADE (single effect call) if (blendingStyle == BLEND_STYLE_FADE) curBri = (prog * curBri + _t->_bri * (0xFFFFU - prog)) / 0xFFFFU; else curBri = Segment::isPreviousMode() ? _t->_bri : curBri; } return curBri; } // pre-calculate drawing parameters for faster access (based on the idea from @softhack007 from MM fork) // and blends colors and palettes if necessary // prog is the progress of the transition (0-65535) and is passed to the function as it may be called in the context of old segment // which does not have transition structure void Segment::beginDraw(uint16_t prog) { setDrawDimensions(); // load colors into _currentColors for (unsigned i = 0; i < NUM_COLORS; i++) _currentColors[i] = colors[i]; // load palette into _currentPalette loadPalette(Segment::_currentPalette, palette); if (isInTransition() && prog < 0xFFFFU && blendingStyle == BLEND_STYLE_FADE) { // blend colors for (unsigned i = 0; i < NUM_COLORS; i++) _currentColors[i] = color_blend16(_t->_colors[i], colors[i], prog); // blend palettes // there are about 255 blend passes of 48 "blends" to completely blend two palettes (in _dur time) // minimum blend time is 100ms maximum is 65535ms #ifndef WLED_SAVE_RAM unsigned noOfBlends = ((255U * prog) / 0xFFFFU) - _t->_prevPaletteBlends; if(noOfBlends > 255) noOfBlends = 255; // safety check for (unsigned i = 0; i < noOfBlends; i++, _t->_prevPaletteBlends++) nblendPaletteTowardPalette(_t->_palT, Segment::_currentPalette, 48); Segment::_currentPalette = _t->_palT; // copy transitioning/temporary palette #else unsigned noOfBlends = ((255U * prog) / 0xFFFFU); CRGBPalette16 tmpPalette; loadPalette(tmpPalette, _t->_palette); for (unsigned i = 0; i < noOfBlends; i++) nblendPaletteTowardPalette(tmpPalette, Segment::_currentPalette, 48); Segment::_currentPalette = tmpPalette; // copy transitioning/temporary palette #endif } } // relies on WS2812FX::service() to call it for each frame void Segment::handleRandomPalette() { unsigned long now = millis(); uint16_t now_s = now / 1000; // we only need seconds (and @dedehai hated shift >> 10) now = (now_s)*1000 + (now % 1000); // ignore days (now is limited to 18 hours as now_s can only store 65535s ~ 18h 12min) if (now_s < Segment::_lastPaletteChange) Segment::_lastPaletteChange = 0; // handle overflow (will cause 2*randomPaletteChangeTime glitch at most) // is it time to generate a new palette? if (now_s > Segment::_lastPaletteChange + randomPaletteChangeTime) { Segment::_newRandomPalette = useHarmonicRandomPalette ? generateHarmonicRandomPalette(Segment::_randomPalette) : generateRandomPalette(); Segment::_lastPaletteChange = now_s; Segment::_nextPaletteBlend = now; // starts blending immediately } // there are about 255 blend passes of 48 "blends" to completely blend two palettes (in strip.getTransition() time) // if randomPaletteChangeTime is shorter than strip.getTransition() palette will never fully blend unsigned frameTime = strip.getFrameTime(); // in ms [8-1000] unsigned transitionTime = strip.getTransition(); // in ms [100-65535] if ((uint16_t)now < Segment::_nextPaletteBlend || now > ((Segment::_lastPaletteChange*1000) + transitionTime + 2*frameTime)) return; // not yet time or past transition time, no need to blend unsigned transitionFrames = frameTime > transitionTime ? 1 : transitionTime / frameTime; // i.e. 700ms/23ms = 30 or 20000ms/8ms = 2500 or 100ms/1000ms = 0 -> 1 unsigned noOfBlends = transitionFrames > 255 ? 1 : (255 + (transitionFrames>>1)) / transitionFrames; // we do some rounding here for (unsigned i = 0; i < noOfBlends; i++) nblendPaletteTowardPalette(Segment::_randomPalette, Segment::_newRandomPalette, 48); Segment::_nextPaletteBlend = now + ((transitionFrames >> 8) * frameTime); // postpone next blend if necessary } // sets Segment geometry (length or width/height and grouping, spacing and offset as well as 2D mapping) // strip must be suspended (strip.suspend()) before calling this function // this function may call fill() to clear pixels if spacing or mapping changed (which requires setting _vWidth, _vHeight, _vLength or beginDraw()) void Segment::setGeometry(uint16_t i1, uint16_t i2, uint8_t grp, uint8_t spc, uint16_t ofs, uint16_t i1Y, uint16_t i2Y, uint8_t m12) { // return if neither bounds nor grouping have changed bool boundsUnchanged = (start == i1 && stop == i2); #ifndef WLED_DISABLE_2D boundsUnchanged &= (startY == i1Y && stopY == i2Y); // 2D #endif boundsUnchanged &= (grouping == grp && spacing == spc); // changing grouping and/or spacing changes virtual segment length (painting dimensions) if (stop && (spc > 0 || m12 != map1D2D)) clear(); if (grp) { // prevent assignment of 0 grouping = grp; spacing = spc; } else { grouping = 1; spacing = 0; } if (ofs < UINT16_MAX) offset = ofs; map1D2D = constrain(m12, 0, 7); if (boundsUnchanged) return; unsigned oldLength = length(); DEBUGFX_PRINTF_P(PSTR("Segment geometry: %d,%d -> %d,%d [%d,%d]\n"), (int)i1, (int)i2, (int)i1Y, (int)i2Y, (int)grp, (int)spc); markForReset(); if (_t) stopTransition(); // we can't use transition if segment dimensions changed stateChanged = true; // send UDP/WS broadcast // apply change immediately if (i2 <= i1) { //disable segment #ifdef WLED_ENABLE_GIF endImagePlayback(this); #endif deallocateData(); p_free(pixels); pixels = nullptr; stop = 0; return; } if (i1 < Segment::maxWidth || (i1 >= Segment::maxWidth*Segment::maxHeight && i1 < strip.getLengthTotal())) start = i1; // Segment::maxWidth equals strip.getLengthTotal() for 1D stop = i2 > Segment::maxWidth*Segment::maxHeight && i1 >= Segment::maxWidth*Segment::maxHeight ? MIN(i2,strip.getLengthTotal()) : constrain(i2, 1, Segment::maxWidth); // check for 2D trailing strip startY = 0; stopY = 1; #ifndef WLED_DISABLE_2D if (Segment::maxHeight>1) { // 2D if (i1Y < Segment::maxHeight) startY = i1Y; stopY = constrain(i2Y, 1, Segment::maxHeight); } #endif // safety check if (start >= stop || startY >= stopY) { #ifdef WLED_ENABLE_GIF endImagePlayback(this); #endif deallocateData(); p_free(pixels); pixels = nullptr; stop = 0; return; } // allocate FX render buffer if (length() != oldLength) { // allocate render buffer (always entire segment), prefer IRAM/PSRAM. Note: impact on FPS with PSRAM buffer is low (<2% with QSPI PSRAM) on S2/S3 p_free(pixels); pixels = static_cast(allocate_buffer(length() * sizeof(uint32_t), BFRALLOC_PREFER_PSRAM | BFRALLOC_NOBYTEACCESS)); if (!pixels) { DEBUGFX_PRINTLN(F("!!! Not enough RAM for pixel buffer !!!")); #ifdef WLED_ENABLE_GIF endImagePlayback(this); #endif deallocateData(); errorFlag = ERR_NORAM_PX; stop = 0; return; } } refreshLightCapabilities(); } Segment &Segment::setColor(uint8_t slot, uint32_t c) { if (slot >= NUM_COLORS || c == colors[slot]) return *this; if (!_isRGB && !_hasW) { if (slot == 0 && c == BLACK) return *this; // on/off segment cannot have primary color black if (slot == 1 && c != BLACK) return *this; // on/off segment cannot have secondary color non black } //DEBUG_PRINTF_P(PSTR("- Starting color transition: %d [0x%X]\n"), slot, c); startTransition(strip.getTransition(), blendingStyle != BLEND_STYLE_FADE); // start transition prior to change colors[slot] = c; stateChanged = true; // send UDP/WS broadcast return *this; } Segment &Segment::setCCT(uint16_t k) { if (k > 255) { //kelvin value, convert to 0-255 if (k < 1900) k = 1900; if (k > 10091) k = 10091; k = (k - 1900) >> 5; } if (cct != k) { //DEBUG_PRINTF_P(PSTR("- Starting CCT transition: %d\n"), k); startTransition(strip.getTransition(), false); // start transition prior to change (no need to copy segment) cct = k; stateChanged = true; // send UDP/WS broadcast } return *this; } Segment &Segment::setOpacity(uint8_t o) { if (opacity != o) { //DEBUG_PRINTF_P(PSTR("- Starting opacity transition: %d\n"), o); startTransition(strip.getTransition(), blendingStyle != BLEND_STYLE_FADE); // start transition prior to change opacity = o; stateChanged = true; // send UDP/WS broadcast } return *this; } Segment &Segment::setOption(uint8_t n, bool val) { bool prev = (options >> n) & 0x01; if (val == prev) return *this; //DEBUG_PRINTF_P(PSTR("- Starting option transition: %d\n"), n); if (n == SEG_OPTION_ON) startTransition(strip.getTransition(), blendingStyle != BLEND_STYLE_FADE); // start transition prior to change if (val) options |= 0x01 << n; else options &= ~(0x01 << n); stateChanged = true; // send UDP/WS broadcast return *this; } Segment &Segment::setMode(uint8_t fx, bool loadDefaults) { // skip reserved while (fx < strip.getModeCount() && strncmp_P("RSVD", strip.getModeData(fx), 4) == 0) fx++; if (fx >= strip.getModeCount()) fx = 0; // set solid mode // if we have a valid mode & is not reserved if (fx != mode) { startTransition(strip.getTransition(), true); // set effect transitions (must create segment copy) mode = fx; int sOpt; // load default values from effect string if (loadDefaults) { sOpt = extractModeDefaults(fx, "sx"); speed = (sOpt >= 0) ? sOpt : DEFAULT_SPEED; sOpt = extractModeDefaults(fx, "ix"); intensity = (sOpt >= 0) ? sOpt : DEFAULT_INTENSITY; sOpt = extractModeDefaults(fx, "c1"); custom1 = (sOpt >= 0) ? sOpt : DEFAULT_C1; sOpt = extractModeDefaults(fx, "c2"); custom2 = (sOpt >= 0) ? sOpt : DEFAULT_C2; sOpt = extractModeDefaults(fx, "c3"); custom3 = (sOpt >= 0) ? sOpt : DEFAULT_C3; sOpt = extractModeDefaults(fx, "o1"); check1 = (sOpt >= 0) ? (bool)sOpt : false; sOpt = extractModeDefaults(fx, "o2"); check2 = (sOpt >= 0) ? (bool)sOpt : false; sOpt = extractModeDefaults(fx, "o3"); check3 = (sOpt >= 0) ? (bool)sOpt : false; sOpt = extractModeDefaults(fx, "m12"); if (sOpt >= 0) map1D2D = constrain(sOpt, 0, 7); else map1D2D = M12_Pixels; // reset mapping if not defined (2D FX may not work) sOpt = extractModeDefaults(fx, "si"); if (sOpt >= 0) soundSim = constrain(sOpt, 0, 3); sOpt = extractModeDefaults(fx, "rev"); if (sOpt >= 0) reverse = (bool)sOpt; sOpt = extractModeDefaults(fx, "mi"); if (sOpt >= 0) mirror = (bool)sOpt; // NOTE: setting this option is a risky business sOpt = extractModeDefaults(fx, "rY"); if (sOpt >= 0) reverse_y = (bool)sOpt; sOpt = extractModeDefaults(fx, "mY"); if (sOpt >= 0) mirror_y = (bool)sOpt; // NOTE: setting this option is a risky business } sOpt = extractModeDefaults(fx, "pal"); // always extract 'pal' to set _default_palette if (sOpt >= 0 && loadDefaults) setPalette(sOpt); if (sOpt <= 0) sOpt = 6; // partycolors if zero or not set _default_palette = sOpt; // _deault_palette is loaded into pal0 in loadPalette() (if selected) markForReset(); stateChanged = true; // send UDP/WS broadcast } return *this; } Segment &Segment::setPalette(uint8_t pal) { if (pal <= 255-customPalettes.size() && pal > FIXED_PALETTE_COUNT) pal = 0; // not built in palette or custom palette if (pal != palette) { //DEBUG_PRINTF_P(PSTR("- Starting palette transition: %d\n"), pal); startTransition(strip.getTransition(), blendingStyle != BLEND_STYLE_FADE); // start transition prior to change (no need to copy segment) palette = pal; stateChanged = true; // send UDP/WS broadcast } return *this; } Segment &Segment::setName(const char *newName) { if (newName) { const int newLen = min(strlen(newName), (size_t)WLED_MAX_SEGNAME_LEN); if (newLen) { if (name) p_free(name); // free old name name = static_cast(allocate_buffer(newLen+1, BFRALLOC_PREFER_PSRAM)); if (mode == FX_MODE_2DSCROLLTEXT) startTransition(strip.getTransition(), true); // if the name changes in scrolling text mode, we need to copy the segment for blending if (name) strlcpy(name, newName, newLen+1); return *this; } } return clearName(); } // 2D matrix unsigned Segment::virtualWidth() const { unsigned groupLen = groupLength(); unsigned vWidth = ((transpose ? height() : width()) + groupLen - 1) / groupLen; if (mirror) vWidth = (vWidth + 1) /2; // divide by 2 if mirror, leave at least a single LED return vWidth; } unsigned Segment::virtualHeight() const { unsigned groupLen = groupLength(); unsigned vHeight = ((transpose ? width() : height()) + groupLen - 1) / groupLen; if (mirror_y) vHeight = (vHeight + 1) /2; // divide by 2 if mirror, leave at least a single LED return vHeight; } // Constants for mapping mode "Pinwheel" #ifndef WLED_DISABLE_2D constexpr int Fixed_Scale = 16384; // fixpoint scaling factor (14bit for fraction) // Pinwheel helper function: matrix dimensions to number of rays static int getPinwheelLength(int vW, int vH) { // Returns multiple of 8, prevents over drawing return (max(vW, vH) + 15) & ~7; } static void setPinwheelParameters(int i, int vW, int vH, int& startx, int& starty, int* cosVal, int* sinVal, bool getPixel = false) { int steps = getPinwheelLength(vW, vH); int baseAngle = ((0xFFFF + steps / 2) / steps); // 360° / steps, in 16 bit scale round to nearest integer int rotate = 0; if (getPixel) rotate = baseAngle / 2; // rotate by half a ray width when reading pixel color for (int k = 0; k < 2; k++) // angular steps for two consecutive rays { int angle = (i + k) * baseAngle + rotate; cosVal[k] = (cos16_t(angle) * Fixed_Scale) >> 15; // step per pixel in fixed point, cos16 output is -0x7FFF to +0x7FFF sinVal[k] = (sin16_t(angle) * Fixed_Scale) >> 15; // using explicit bit shifts as dividing negative numbers is not equivalent (rounding error is acceptable) } startx = (vW * Fixed_Scale) / 2; // + cosVal[0] / 4; // starting position = center + 1/4 pixel (in fixed point) starty = (vH * Fixed_Scale) / 2; // + sinVal[0] / 4; } #endif // 1D strip uint16_t Segment::virtualLength() const { #ifndef WLED_DISABLE_2D if (is2D()) { unsigned vW = virtualWidth(); unsigned vH = virtualHeight(); unsigned vLen; switch (map1D2D) { case M12_pBar: vLen = vH; break; case M12_pCorner: vLen = max(vW,vH); // get the longest dimension break; case M12_pArc: vLen = sqrt32_bw(vH*vH + vW*vW); // use diagonal break; case M12_sPinwheel: vLen = getPinwheelLength(vW, vH); break; default: vLen = vW * vH; // use all pixels from segment break; } return vLen; } #endif unsigned groupLen = groupLength(); // is always >= 1 unsigned vLength = (length() + groupLen - 1) / groupLen; if (mirror) vLength = (vLength + 1) /2; // divide by 2 if mirror, leave at least a single LED return vLength; } #ifndef WLED_DISABLE_2D // maximum length of a mapped 1D segment, used in PS for buffer allocation uint16_t Segment::maxMappingLength() const { uint32_t vW = virtualWidth(); uint32_t vH = virtualHeight(); return max(sqrt32_bw(vH*vH + vW*vW), (uint32_t)getPinwheelLength(vW, vH)); // use diagonal } #endif // pixel is clipped if it falls outside clipping range // if clipping start > stop the clipping range is inverted bool Segment::isPixelClipped(int i) const { if (blendingStyle != BLEND_STYLE_FADE && isInTransition() && _clipStart != _clipStop) { bool invert = _clipStart > _clipStop; // ineverted start & stop int start = invert ? _clipStop : _clipStart; int stop = invert ? _clipStart : _clipStop; if (blendingStyle == BLEND_STYLE_FAIRY_DUST) { unsigned len = stop - start; if (len < 2) return false; unsigned shuffled = hashInt(i) % len; unsigned pos = (shuffled * 0xFFFFU) / len; return progress() <= pos; } const bool iInside = (i >= start && i < stop); return !iInside ^ invert; // thanks @willmmiles (https://github.com/wled/WLED/pull/3877#discussion_r1554633876) } return false; } void WLED_O2_ATTR Segment::setPixelColor(int i, uint32_t col) const { if (!isActive() || i < 0) return; // not active or invalid index #ifndef WLED_DISABLE_2D int vStrip = 0; #endif const int vL = vLength(); // if the 1D effect is using virtual strips "i" will have virtual strip id stored in upper 16 bits // in such case "i" will be > virtualLength() if (i >= vL) { // check if this is a virtual strip #ifndef WLED_DISABLE_2D vStrip = i>>16; // hack to allow running on virtual strips (2D segment columns/rows) #endif i &= 0xFFFF; // truncate vstrip index. note: vStrip index is 1 even in 1D, still need to truncate if (i >= vL) return; // if pixel would still fall out of segment just exit } #ifndef WLED_DISABLE_2D if (is2D()) { const int vW = vWidth(); // segment width in logical pixels (can be 0 if segment is inactive) const int vH = vHeight(); // segment height in logical pixels (is always >= 1) const auto XY = [&](unsigned x, unsigned y){ return x + y*vW;}; switch (map1D2D) { case M12_Pixels: // use all available pixels as a long strip setPixelColorRaw(XY(i % vW, i / vW), col); break; case M12_pBar: // expand 1D effect vertically or have it play on virtual strips if (vStrip > 0) setPixelColorRaw(XY(vStrip - 1, vH - i - 1), col); else for (int x = 0; x < vW; x++) setPixelColorRaw(XY(x, vH - i - 1), col); break; case M12_pArc: // expand in circular fashion from center if (i == 0) setPixelColorRaw(XY(0, 0), col); else { float r = i; float step = HALF_PI / (2.8284f * r + 4); // we only need (PI/4)/(r/sqrt(2)+1) steps for (float rad = 0.0f; rad <= (HALF_PI/2)+step/2; rad += step) { int x = roundf(sin_t(rad) * r); int y = roundf(cos_t(rad) * r); // exploit symmetry setPixelColorXY(x, y, col); setPixelColorXY(y, x, col); } // Bresenham’s Algorithm (may not fill every pixel) //int d = 3 - (2*i); //int y = i, x = 0; //while (y >= x) { // setPixelColorXY(x, y, col); // setPixelColorXY(y, x, col); // x++; // if (d > 0) { // y--; // d += 4 * (x - y) + 10; // } else { // d += 4 * x + 6; // } //} } break; case M12_pCorner: for (int x = 0; x <= i; x++) setPixelColorXY(x, i, col); // note: <= to include i=0. Relies on overflow check in sPC() for (int y = 0; y < i; y++) setPixelColorXY(i, y, col); break; case M12_sPinwheel: { // Uses Bresenham's algorithm to place coordinates of two lines in arrays then draws between them int startX, startY, cosVal[2], sinVal[2]; // in fixed point scale setPinwheelParameters(i, vW, vH, startX, startY, cosVal, sinVal); unsigned maxLineLength = max(vW, vH) + 2; // pixels drawn is always smaller than dx or dy, +1 pair for rounding errors uint16_t lineCoords[2][maxLineLength]; // uint16_t to save ram int lineLength[2] = {0}; static int prevRays[2] = {INT_MAX, INT_MAX}; // previous two ray numbers int closestEdgeIdx = INT_MAX; // index of the closest edge pixel for (int lineNr = 0; lineNr < 2; lineNr++) { int x0 = startX; // x, y coordinates in fixed scale int y0 = startY; int x1 = (startX + (cosVal[lineNr] << 9)); // outside of grid int y1 = (startY + (sinVal[lineNr] << 9)); // outside of grid const int dx = abs(x1-x0), sx = x0= (unsigned)vW || (unsigned)y0 >= (unsigned)vH) { closestEdgeIdx = min(closestEdgeIdx, idx-2); break; // stop if outside of grid (exploit unsigned int overflow) } coordinates[idx++] = x0; coordinates[idx++] = y0; (*length)++; // note: since endpoint is out of grid, no need to check if endpoint is reached int e2 = 2 * err; if (e2 >= dy) { err += dy; x0 += sx; } if (e2 <= dx) { err += dx; y0 += sy; } } } // fill up the shorter line with missing coordinates, so block filling works correctly and efficiently int diff = lineLength[0] - lineLength[1]; int longLineIdx = (diff > 0) ? 0 : 1; int shortLineIdx = longLineIdx ? 0 : 1; if (diff != 0) { int idx = (lineLength[shortLineIdx] - 1) * 2; // last valid coordinate index int lastX = lineCoords[shortLineIdx][idx++]; int lastY = lineCoords[shortLineIdx][idx++]; bool keepX = lastX == 0 || lastX == vW - 1; for (int d = 0; d < abs(diff); d++) { lineCoords[shortLineIdx][idx] = keepX ? lastX :lineCoords[longLineIdx][idx]; idx++; lineCoords[shortLineIdx][idx] = keepX ? lineCoords[longLineIdx][idx] : lastY; idx++; } } // draw and block-fill the line coordinates. Note: block filling only efficient if angle between lines is small closestEdgeIdx += 2; int max_i = getPinwheelLength(vW, vH) - 1; bool drawFirst = !(prevRays[0] == i - 1 || (i == 0 && prevRays[0] == max_i)); // draw first line if previous ray was not adjacent including wrap bool drawLast = !(prevRays[0] == i + 1 || (i == max_i && prevRays[0] == 0)); // same as above for last line for (int idx = 0; idx < lineLength[longLineIdx] * 2;) { //!! should be long line idx! int x1 = lineCoords[0][idx]; int x2 = lineCoords[1][idx++]; int y1 = lineCoords[0][idx]; int y2 = lineCoords[1][idx++]; int minX, maxX, minY, maxY; (x1 < x2) ? (minX = x1, maxX = x2) : (minX = x2, maxX = x1); (y1 < y2) ? (minY = y1, maxY = y2) : (minY = y2, maxY = y1); // fill the block between the two x,y points bool alwaysDraw = (drawFirst && drawLast) || // No adjacent rays, draw all pixels (idx > closestEdgeIdx) || // Edge pixels on uneven lines are always drawn (i == 0 && idx == 2) || // Center pixel special case (i == prevRays[1]); // Effect drawing twice in 1 frame for (int x = minX; x <= maxX; x++) { for (int y = minY; y <= maxY; y++) { bool onLine1 = x == x1 && y == y1; bool onLine2 = x == x2 && y == y2; if ((alwaysDraw) || (!onLine1 && (!onLine2 || drawLast)) || // Middle pixels and line2 if drawLast (!onLine2 && (!onLine1 || drawFirst)) // Middle pixels and line1 if drawFirst ) { setPixelColorXY(x, y, col); } } } } prevRays[1] = prevRays[0]; prevRays[0] = i; break; } } return; } else if (Segment::maxHeight != 1 && (width() == 1 || height() == 1)) { if (start < Segment::maxWidth*Segment::maxHeight) { // we have a vertical or horizontal 1D segment (WARNING: virtual...() may be transposed) int x = 0, y = 0; if (vHeight() > 1) y = i; if (vWidth() > 1) x = i; setPixelColorXY(x, y, col); return; } } #endif setPixelColorRaw(i, col); } #ifdef WLED_USE_AA_PIXELS // anti-aliased normalized version of setPixelColor() void Segment::setPixelColor(float i, uint32_t col, bool aa) const { if (!isActive()) return; // not active int vStrip = int(i/10.0f); // hack to allow running on virtual strips (2D segment columns/rows) i -= int(i); if (i<0.0f || i>1.0f) return; // not normalized float fC = i * (virtualLength()-1); if (aa) { unsigned iL = roundf(fC-0.49f); unsigned iR = roundf(fC+0.49f); float dL = (fC - iL)*(fC - iL); float dR = (iR - fC)*(iR - fC); uint32_t cIL = getPixelColor(iL | (vStrip<<16)); uint32_t cIR = getPixelColor(iR | (vStrip<<16)); if (iR!=iL) { // blend L pixel cIL = color_blend(col, cIL, uint8_t(dL*255.0f)); setPixelColor(iL | (vStrip<<16), cIL); // blend R pixel cIR = color_blend(col, cIR, uint8_t(dR*255.0f)); setPixelColor(iR | (vStrip<<16), cIR); } else { // exact match (x & y land on a pixel) setPixelColor(iL | (vStrip<<16), col); } } else { setPixelColor(int(roundf(fC)) | (vStrip<<16), col); } } #endif uint32_t WLED_O2_ATTR Segment::getPixelColor(int i) const { if (!isActive() || i < 0) return 0; // not active or invalid index #ifndef WLED_DISABLE_2D int vStrip = i>>16; // virtual strips are only relevant in Bar expansion mode i &= 0xFFFF; #endif if (i >= (int)vLength()) return 0; #ifndef WLED_DISABLE_2D if (is2D()) { const int vW = vWidth(); // segment width in logical pixels (can be 0 if segment is inactive) const int vH = vHeight(); // segment height in logical pixels (is always >= 1) int x = 0, y = 0; switch (map1D2D) { case M12_Pixels: x = i % vW; y = i / vW; break; case M12_pBar: if (vStrip > 0) { x = vStrip - 1; y = vH - i - 1; } else { y = vH - i - 1; }; break; case M12_pArc: if (i > vW && i > vH) { x = y = sqrt32_bw(i*i/2); break; // use diagonal } // otherwise fallthrough case M12_pCorner: // use longest dimension if (vW > vH) x = i; else y = i; break; case M12_sPinwheel: { // not 100% accurate, returns pixel at outer edge int cosVal[2], sinVal[2]; setPinwheelParameters(i, vW, vH, x, y, cosVal, sinVal, true); int maxX = (vW-1) * Fixed_Scale; int maxY = (vH-1) * Fixed_Scale; // trace ray from center until we hit any edge - to avoid rounding problems, we use fixed point coordinates while ((x < maxX) && (y < maxY) && (x > Fixed_Scale) && (y > Fixed_Scale)) { x += cosVal[0]; // advance to next position y += sinVal[0]; } x /= Fixed_Scale; y /= Fixed_Scale; break; } } return getPixelColorXY(x, y); } #endif return getPixelColorRaw(i); } void Segment::refreshLightCapabilities() const { unsigned capabilities = 0; if (!isActive()) { _capabilities = 0; return; } // we must traverse each pixel in segment to determine its capabilities (as pixel may be mapped) for (unsigned y = startY; y < stopY; y++) for (unsigned x = start; x < stop; x++) { unsigned index = x + Segment::maxWidth * y; index = strip.getMappedPixelIndex(index); // convert logical address to physical if (index == 0xFFFF) continue; // invalid/missing pixel for (unsigned b = 0; b < BusManager::getNumBusses(); b++) { const Bus *bus = BusManager::getBus(b); if (!bus || !bus->isOk()) break; if (bus->containsPixel(index)) { if (bus->hasRGB() || (strip.cctFromRgb && bus->hasCCT())) capabilities |= SEG_CAPABILITY_RGB; if (!strip.cctFromRgb && bus->hasCCT()) capabilities |= SEG_CAPABILITY_CCT; if (strip.correctWB && (bus->hasRGB() || bus->hasCCT())) capabilities |= SEG_CAPABILITY_CCT; //white balance correction (CCT slider) if (bus->hasWhite()) { unsigned aWM = Bus::getGlobalAWMode() == AW_GLOBAL_DISABLED ? bus->getAutoWhiteMode() : Bus::getGlobalAWMode(); bool whiteSlider = (aWM == RGBW_MODE_DUAL || aWM == RGBW_MODE_MANUAL_ONLY); // white slider allowed // if auto white calculation from RGB is active (Accurate/Brighter), force RGB controls even if there are no RGB busses if (!whiteSlider) capabilities |= SEG_CAPABILITY_RGB; // if auto white calculation from RGB is disabled/optional (None/Dual), allow white channel adjustments if ( whiteSlider) capabilities |= SEG_CAPABILITY_W; } break; } } } _capabilities = capabilities; } /* * Fills segment with color */ void Segment::fill(uint32_t c) const { if (!isActive()) return; // not active for (unsigned i = 0; i < length(); i++) setPixelColorRaw(i,c); // always fill all pixels (blending will take care of grouping, spacing and clipping) } /* * fade out function, higher rate = quicker fade * fading is highly dependant on frame rate (higher frame rates, faster fading) * each frame will fade at max 9% or as little as 0.8% */ void Segment::fade_out(uint8_t rate) const { if (!isActive()) return; // not active rate = (256-rate) >> 1; const int mappedRate = 256 / (rate + 1); const size_t rlength = rawLength(); // calculate only once for (unsigned j = 0; j < rlength; j++) { uint32_t color = getPixelColorRaw(j); if (color == colors[1]) continue; // already at target color for (int i = 0; i < 32; i += 8) { uint8_t c2 = (colors[1]>>i); // get background channel uint8_t c1 = (color>>i); // get foreground channel // we can't use bitshift since we are using int int delta = (c2 - c1) * mappedRate / 256; // if fade isn't complete, make sure delta is at least 1 (fixes rounding issues) if (delta == 0) delta += (c2 == c1) ? 0 : (c2 > c1) ? 1 : -1; // stuff new value back into color color &= ~(0xFF< 215 this function does not work properly (creates alternating pattern) */ void Segment::blur(uint8_t blur_amount, bool smear) const { if (!isActive() || blur_amount == 0) return; // optimization: 0 means "don't blur" #ifndef WLED_DISABLE_2D if (is2D()) { // compatibility with 2D blur2D(blur_amount, blur_amount, smear); // symmetrical 2D blur //box_blur(map(blur_amount,1,255,1,3), smear); return; } #endif uint8_t keep = smear ? 255 : 255 - blur_amount; uint8_t seep = blur_amount >> 1; unsigned vlength = vLength(); // handle first pixel to avoid conditional in loop (faster) uint32_t cur = getPixelColorRaw(0); uint32_t carryover = fast_color_scale(cur, seep); setPixelColorRaw(0, fast_color_scale(cur, keep)); for (unsigned i = 1; i < vlength; i++) { cur = getPixelColorRaw(i); uint32_t part = fast_color_scale(cur, seep); cur = fast_color_scale(cur, keep); cur = color_add(cur, carryover); setPixelColorRaw(i - 1, color_add(getPixelColorRaw(i - 1), part)); // previous pixel setPixelColorRaw(i, cur); // current pixel carryover = part; } } /* * Put a value 0 to 255 in to get a color value. * The colours are a transition r -> g -> b -> back to r * Rotates the color in HSV space, where pos is H. (0=0deg, 256=360deg) */ uint32_t Segment::color_wheel(uint8_t pos) const { if (palette) return color_from_palette(pos, false, false, 0); // only wrap if "always wrap" is set uint8_t w = W(getCurrentColor(0)); uint32_t rgb; hsv2rgb(CHSV32(static_cast(pos << 8), 255, 255), rgb); return rgb | (w << 24); // add white channel } /* * Gets a single color from the currently selected palette. * @param i Palette Index (if mapping is true, the full palette will be _virtualSegmentLength long, if false, 255). Will wrap around automatically. * @param mapping if true, LED position in segment is considered for color * @param moving FastLED palettes will usually wrap back to the start smoothly. Set to true if effect has moving palette and you want wrap. * @param mcol If the default palette 0 is selected, return the standard color 0, 1 or 2 instead. If >2, Party palette is used instead * @param pbri Value to scale the brightness of the returned color by. Default is 255. (no scaling) * @returns Single color from palette */ uint32_t Segment::color_from_palette(uint16_t i, bool mapping, bool moving, uint8_t mcol, uint8_t pbri) const { uint32_t color = getCurrentColor(mcol); // default palette or no RGB support on segment if ((palette == 0 && mcol < NUM_COLORS) || !_isRGB) { return color_fade(color, pbri, true); } unsigned paletteIndex = i; if (mapping) paletteIndex = min((i*255)/vLength(), 255U); // paletteBlend: 0 - wrap when moving, 1 - always wrap, 2 - never wrap, 3 - none (undefined/no interpolation of palette entries) // ColorFromPalette interpolations are: NOBLEND, LINEARBLEND, LINEARBLEND_NOWRAP TBlendType blend = NOBLEND; switch (paletteBlend) { case 0: blend = moving ? LINEARBLEND : LINEARBLEND_NOWRAP; break; case 1: blend = LINEARBLEND; break; case 2: blend = LINEARBLEND_NOWRAP; break; } CRGBW palcol = ColorFromPalette(_currentPalette, paletteIndex, pbri, blend); palcol.w = W(color); return palcol.color32; } /////////////////////////////////////////////////////////////////////////////// // WS2812FX class implementation /////////////////////////////////////////////////////////////////////////////// //do not call this method from system context (network callback) void WS2812FX::finalizeInit() { //reset segment runtimes restartRuntime(); // for the lack of better place enumerate ledmaps here // if we do it in json.cpp (serializeInfo()) we are getting flashes on LEDs // unfortunately this means we do not get updates after uploads // the other option is saving UI settings which will cause enumeration enumerateLedmaps(); _hasWhiteChannel = _isOffRefreshRequired = false; BusManager::removeAll(); // TODO: ideally we would free everything segment related here to reduce fragmentation (pixel buffers, ledamp, segments, etc) but that somehow leads to heap corruption if touchig any of the buffers. unsigned digitalCount = 0; #if defined(ARDUINO_ARCH_ESP32) && !defined(CONFIG_IDF_TARGET_ESP32C3) // validate the bus config: count I2S buses and check if they meet requirements unsigned i2sBusCount = 0; for (const auto &bus : busConfigs) { if (Bus::isDigital(bus.type) && !Bus::is2Pin(bus.type)) { digitalCount++; if (bus.driverType == 1) i2sBusCount++; } } DEBUG_PRINTF_P(PSTR("Digital buses: %u, I2S buses: %u\n"), digitalCount, i2sBusCount); // Determine parallel vs single I2S usage (used for memory calculation only) bool useParallelI2S = false; #if defined(CONFIG_IDF_TARGET_ESP32S3) // ESP32-S3 always uses parallel LCD driver for I2S if (i2sBusCount > 0) { useParallelI2S = true; } #else if (i2sBusCount > 1) { useParallelI2S = true; } #endif #endif DEBUG_PRINTF_P(PSTR("Heap before buses: %d\n"), getFreeHeapSize()); // create buses/outputs unsigned mem = 0; // memory estimation including DMA buffer for I2S and pixel buffers unsigned I2SdmaMem = 0; for (auto &bus : busConfigs) { // assign bus types: call to getI() determines bus types/drivers, allocates and tracks polybus channels // store the result in iType for later use during bus creation (getI() must only be called once per BusConfig) // note: this needs to be determined for all buses prior to creating them as it also determines parallel I2S usage bus.iType = BusManager::getI(bus.type, bus.pins, bus.driverType); } for (auto &bus : busConfigs) { bool use_placeholder = false; unsigned busMemUsage = bus.memUsage(); // does not include DMA/RMT buffer but includes pixel buffers (segment buffer + global buffer) mem += busMemUsage; // estimate maximum I2S memory usage (only relevant for digital non-2pin busses when I2S is enabled) #if !defined(CONFIG_IDF_TARGET_ESP32C3) && !defined(ESP8266) bool usesI2S = (bus.iType & 0x01) == 0; // I2S bus types are even numbered, can't use bus.driverType == 1 as getI() may have defaulted to RMT if (Bus::isDigital(bus.type) && !Bus::is2Pin(bus.type) && usesI2S) { #ifdef NPB_CONF_4STEP_CADENCE constexpr unsigned stepFactor = 4; // 4 step cadence (4 bits per pixel bit) #else constexpr unsigned stepFactor = 3; // 3 step cadence (3 bits per pixel bit) #endif unsigned i2sCommonMem = (stepFactor * bus.count * (3*Bus::hasRGB(bus.type)+Bus::hasWhite(bus.type)+Bus::hasCCT(bus.type)) * (Bus::is16bit(bus.type)+1)); if (useParallelI2S) i2sCommonMem *= 8; // parallel I2S uses 8 channels, requiring 8x the DMA buffer size (common buffer shared between all parallel busses) if (i2sCommonMem > I2SdmaMem) I2SdmaMem = i2sCommonMem; } #endif if (mem + I2SdmaMem > MAX_LED_MEMORY + 1024) { // +1k to allow some margin to not drop buses that are allowed in UI (calculation here includes bus overhead) DEBUG_PRINTF_P(PSTR("Bus %d with %d LEDS memory usage exceeds limit\n"), (int)bus.type, bus.count); errorFlag = ERR_NORAM; // alert UI TODO: make this a distinct error: not enough memory for bus use_placeholder = true; } if (BusManager::add(bus, use_placeholder) != -1) { mem += BusManager::busses.back()->getBusSize(); if (Bus::isDigital(bus.type) && !Bus::is2Pin(bus.type) && BusManager::busses.back()->isPlaceholder()) digitalCount--; // remove placeholder from digital count } } DEBUG_PRINTF_P(PSTR("Estimated buses + pixel-buffers size: %uB\n"), mem + I2SdmaMem); busConfigs.clear(); busConfigs.shrink_to_fit(); _length = 0; for (size_t i=0; iisOk() || bus->getStart() + bus->getLength() > MAX_LEDS) break; //RGBW mode is enabled if at least one of the strips is RGBW _hasWhiteChannel |= bus->hasWhite(); //refresh is required to remain off if at least one of the strips requires the refresh. _isOffRefreshRequired |= bus->isOffRefreshRequired() && !bus->isPWM(); // use refresh bit for phase shift with analog unsigned busEnd = bus->getStart() + bus->getLength(); if (busEnd > _length) _length = busEnd; // This must be done after all buses have been created, as some kinds (parallel I2S) interact bus->begin(); bus->setBrightness(scaledBri(bri)); } BusManager::initializeABL(); // init brightness limiter DEBUG_PRINTF_P(PSTR("Heap after buses: %d\n"), ESP.getFreeHeap()); Segment::maxWidth = _length; Segment::maxHeight = 1; //segments are created in makeAutoSegments(); DEBUG_PRINTLN(F("Loading custom palettes")); loadCustomPalettes(); // (re)load all custom palettes DEBUG_PRINTLN(F("Loading custom ledmaps")); deserializeMap(); // (re)load default ledmap (will also setUpMatrix() if ledmap does not exist) // allocate frame buffer after matrix has been set up (gaps!) p_free(_pixels); // using realloc on large buffers can cause additional fragmentation instead of reducing it // use PSRAM if available: there is no measurable perfomance impact between PSRAM and DRAM on S2/S3 with QSPI PSRAM for this buffer _pixels = static_cast(allocate_buffer(getLengthTotal() * sizeof(uint32_t), BFRALLOC_ENFORCE_PSRAM | BFRALLOC_NOBYTEACCESS | BFRALLOC_CLEAR)); DEBUG_PRINTF_P(PSTR("strip buffer size: %uB\n"), getLengthTotal() * sizeof(uint32_t)); DEBUG_PRINTF_P(PSTR("Heap after strip init: %uB\n"), getFreeHeapSize()); } void WS2812FX::service() { unsigned long nowUp = millis(); // Be aware, millis() rolls over every 49 days now = nowUp + timebase; unsigned long elapsed = nowUp - _lastServiceShow; if (_suspend || elapsed <= MIN_FRAME_DELAY) return; // keep wifi alive - no matter if triggered or unlimited if (!_triggered && (_targetFps != FPS_UNLIMITED)) { // unlimited mode = no frametime if (elapsed < _frametime) return; // too early for service } bool doShow = false; _isServicing = true; _segment_index = 0; for (Segment &seg : _segments) { if (_suspend) break; // immediately stop processing segments if suspend requested during service() // process transition (also pre-calculates progress value) seg.handleTransition(); // reset the segment runtime data if needed seg.resetIfRequired(); if (!seg.isActive()) continue; // last condition ensures all solid segments are updated at the same time if (nowUp > _lastServiceShow + _frametime || _triggered || (doShow && seg.mode == FX_MODE_STATIC)) { doShow = true; if (!seg.freeze) { //only run effect function if not frozen // Effect blending uint16_t prog = seg.progress(); seg.beginDraw(prog); // set up parameters for get/setPixelColor() (will also blend colors and palette if blend style is FADE) _currentSegment = &seg; // set current segment for effect functions (SEGMENT & SEGENV) // workaround for on/off transition to respect blending style _mode[seg.mode](); // run new/current mode (needed for bri workaround) seg.call++; // if segment is in transition and no old segment exists we don't need to run the old mode // (blendSegments() takes care of On/Off transitions and clipping) Segment *segO = seg.getOldSegment(); if (segO && segO->isActive() && (seg.mode != segO->mode || blendingStyle != BLEND_STYLE_FADE || (segO->name != seg.name && segO->name && seg.name && strncmp(segO->name, seg.name, WLED_MAX_SEGNAME_LEN) != 0))) { Segment::modeBlend(true); // set semaphore for beginDraw() to blend colors and palette segO->beginDraw(prog); // set up palette & colors (also sets draw dimensions), parent segment has transition progress _currentSegment = segO; // set current segment // workaround for on/off transition to respect blending style _mode[segO->mode](); // run old mode (needed for bri workaround; semaphore!!) segO->call++; // increment old mode run counter Segment::modeBlend(false); // unset semaphore } } } _segment_index++; } #ifdef WLED_DEBUG if ((_targetFps != FPS_UNLIMITED) && (millis() - nowUp > _frametime)) DEBUG_PRINTF_P(PSTR("Slow effects %u/%d.\n"), (unsigned)(millis()-nowUp), (int)_frametime); #endif if (doShow && !_suspend) { yield(); Segment::handleRandomPalette(); // slowly transition random palette; move it into for loop when each segment has individual random palette _lastServiceShow = nowUp; // update timestamp, for precise FPS control show(); } #ifdef WLED_DEBUG if ((_targetFps != FPS_UNLIMITED) && (millis() - nowUp > _frametime)) DEBUG_PRINTF_P(PSTR("Slow strip %u/%d.\n"), (unsigned)(millis()-nowUp), (int)_frametime); #endif _triggered = false; _isServicing = false; } // https://en.wikipedia.org/wiki/Blend_modes but using a for top layer & b for bottom layer static uint8_t _top (uint8_t a, uint8_t b) { return a; } static uint8_t _bottom (uint8_t a, uint8_t b) { return b; } static uint8_t _add (uint8_t a, uint8_t b) { unsigned t = a + b; return t > 255 ? 255 : t; } static uint8_t _subtract (uint8_t a, uint8_t b) { return b > a ? (b - a) : 0; } static uint8_t _difference(uint8_t a, uint8_t b) { return b > a ? (b - a) : (a - b); } static uint8_t _average (uint8_t a, uint8_t b) { return (a + b) >> 1; } #if defined(ESP8266) || defined(CONFIG_IDF_TARGET_ESP32C3) static uint8_t _multiply (uint8_t a, uint8_t b) { return ((a * b) + 255) >> 8; } // faster than division on C3 but slightly less accurate #else static uint8_t _multiply (uint8_t a, uint8_t b) { return (a * b) / 255; } // origianl uses a & b in range [0,1] #endif static uint8_t _divide (uint8_t a, uint8_t b) { return a > b ? (b * 255) / a : 255; } static uint8_t _lighten (uint8_t a, uint8_t b) { return a > b ? a : b; } static uint8_t _darken (uint8_t a, uint8_t b) { return a < b ? a : b; } static uint8_t _screen (uint8_t a, uint8_t b) { return 255 - _multiply(~a,~b); } // 255 - (255-a)*(255-b)/255 static uint8_t _overlay (uint8_t a, uint8_t b) { return b < 128 ? 2 * _multiply(a,b) : (255 - 2 * _multiply(~a,~b)); } static uint8_t _hardlight (uint8_t a, uint8_t b) { return a < 128 ? 2 * _multiply(a,b) : (255 - 2 * _multiply(~a,~b)); } #if defined(ESP8266) || defined(CONFIG_IDF_TARGET_ESP32C3) static uint8_t _softlight (uint8_t a, uint8_t b) { return (((b * b * (255 - 2 * a))) + ((2 * a * b + 256) << 8)) >> 16; } // Pegtop's formula (1 - 2a)b^2 #else static uint8_t _softlight (uint8_t a, uint8_t b) { return (b * b * (255 - 2 * a) + 255 * 2 * a * b) / (255 * 255); } // Pegtop's formula (1 - 2a)b^2 + 2ab #endif static uint8_t _dodge (uint8_t a, uint8_t b) { return _divide(~a,b); } static uint8_t _burn (uint8_t a, uint8_t b) { return ~_divide(a,~b); } void WS2812FX::blendSegment(const Segment &topSegment) const { typedef uint8_t(*FuncType)(uint8_t, uint8_t); FuncType funcs[] = { _top, _bottom, _add, _subtract, _difference, _average, _multiply, _divide, _lighten, _darken, _screen, _overlay, _hardlight, _softlight, _dodge, _burn }; const size_t blendMode = topSegment.blendMode < (sizeof(funcs) / sizeof(FuncType)) ? topSegment.blendMode : 0; const auto func = funcs[blendMode]; // blendMode % (sizeof(funcs) / sizeof(FuncType)) const auto blend = [&](uint32_t top, uint32_t bottom){ return RGBW32(func(R(top),R(bottom)), func(G(top),G(bottom)), func(B(top),B(bottom)), func(W(top),W(bottom))); }; const int length = topSegment.length(); // physical segment length (counts all pixels in 2D segment) const int width = topSegment.width(); const int height = topSegment.height(); const auto XY = [](int x, int y){ return x + y*Segment::maxWidth; }; const size_t matrixSize = Segment::maxWidth * Segment::maxHeight; const size_t startIndx = XY(topSegment.start, topSegment.startY); const size_t stopIndx = startIndx + length; const unsigned progress = topSegment.progress(); const unsigned progInv = 0xFFFFU - progress; uint8_t opacity = topSegment.currentBri(); // returns transitioned opacity for style FADE uint8_t cct = topSegment.currentCCT(); if (gammaCorrectCol) opacity = gamma8inv(opacity); // use inverse gamma on brightness for correct color scaling after gamma correction (see #5343 for details) Segment::setClippingRect(0, 0); // disable clipping by default const unsigned dw = (blendingStyle==BLEND_STYLE_OUTSIDE_IN ? progInv : progress) * width / 0xFFFFU + 1; const unsigned dh = (blendingStyle==BLEND_STYLE_OUTSIDE_IN ? progInv : progress) * height / 0xFFFFU + 1; const unsigned orgBS = blendingStyle; if (width*height == 1) blendingStyle = BLEND_STYLE_FADE; // disable style for single pixel segments (use fade instead) switch (blendingStyle) { case BLEND_STYLE_CIRCULAR_IN: // (must set entire segment, see isPixelXYClipped()) case BLEND_STYLE_CIRCULAR_OUT:// (must set entire segment, see isPixelXYClipped()) case BLEND_STYLE_FAIRY_DUST: // fairy dust (must set entire segment, see isPixelXYClipped()) Segment::setClippingRect(0, width, 0, height); break; case BLEND_STYLE_SWIPE_RIGHT: // left-to-right case BLEND_STYLE_PUSH_RIGHT: // left-to-right Segment::setClippingRect(0, dw, 0, height); break; case BLEND_STYLE_SWIPE_LEFT: // right-to-left case BLEND_STYLE_PUSH_LEFT: // right-to-left Segment::setClippingRect(width - dw, width, 0, height); break; case BLEND_STYLE_OUTSIDE_IN: // corners Segment::setClippingRect((width + dw)/2, (width - dw)/2, (height + dh)/2, (height - dh)/2); // inverted!! break; case BLEND_STYLE_INSIDE_OUT: // outward Segment::setClippingRect((width - dw)/2, (width + dw)/2, (height - dh)/2, (height + dh)/2); break; case BLEND_STYLE_SWIPE_DOWN: // top-to-bottom (2D) case BLEND_STYLE_PUSH_DOWN: // top-to-bottom (2D) Segment::setClippingRect(0, width, 0, dh); break; case BLEND_STYLE_SWIPE_UP: // bottom-to-top (2D) case BLEND_STYLE_PUSH_UP: // bottom-to-top (2D) Segment::setClippingRect(0, width, height - dh, height); break; case BLEND_STYLE_OPEN_H: // horizontal-outward (2D) same look as INSIDE_OUT on 1D Segment::setClippingRect((width - dw)/2, (width + dw)/2, 0, height); break; case BLEND_STYLE_OPEN_V: // vertical-outward (2D) Segment::setClippingRect(0, width, (height - dh)/2, (height + dh)/2); break; case BLEND_STYLE_SWIPE_TL: // TL-to-BR (2D) case BLEND_STYLE_PUSH_TL: // TL-to-BR (2D) Segment::setClippingRect(0, dw, 0, dh); break; case BLEND_STYLE_SWIPE_TR: // TR-to-BL (2D) case BLEND_STYLE_PUSH_TR: // TR-to-BL (2D) Segment::setClippingRect(width - dw, width, 0, dh); break; case BLEND_STYLE_SWIPE_BR: // BR-to-TL (2D) case BLEND_STYLE_PUSH_BR: // BR-to-TL (2D) Segment::setClippingRect(width - dw, width, height - dh, height); break; case BLEND_STYLE_SWIPE_BL: // BL-to-TR (2D) case BLEND_STYLE_PUSH_BL: // BL-to-TR (2D) Segment::setClippingRect(0, dw, height - dh, height); break; } if (isMatrix && stopIndx <= matrixSize) { #ifndef WLED_DISABLE_2D const int nCols = topSegment.virtualWidth(); const int nRows = topSegment.virtualHeight(); const Segment *segO = topSegment.getOldSegment(); const int oCols = segO ? segO->virtualWidth() : nCols; const int oRows = segO ? segO->virtualHeight() : nRows; const auto setMirroredPixel = [&](int x, int y, uint32_t c, uint8_t o) { const int baseX = topSegment.start + x; const int baseY = topSegment.startY + y; size_t indx = XY(baseX, baseY); // absolute address on strip _pixels[indx] = color_blend(_pixels[indx], blend(c, _pixels[indx]), o); if (_pixelCCT) _pixelCCT[indx] = cct; // Apply mirroring if (topSegment.mirror || topSegment.mirror_y) { const int mirrorX = topSegment.start + width - x - 1; const int mirrorY = topSegment.startY + height - y - 1; const size_t idxMX = XY(topSegment.transpose ? baseX : mirrorX, topSegment.transpose ? mirrorY : baseY); const size_t idxMY = XY(topSegment.transpose ? mirrorX : baseX, topSegment.transpose ? baseY : mirrorY); const size_t idxMM = XY(mirrorX, mirrorY); if (topSegment.mirror) _pixels[idxMX] = color_blend(_pixels[idxMX], blend(c, _pixels[idxMX]), o); if (topSegment.mirror_y) _pixels[idxMY] = color_blend(_pixels[idxMY], blend(c, _pixels[idxMY]), o); if (topSegment.mirror && topSegment.mirror_y) _pixels[idxMM] = color_blend(_pixels[idxMM], blend(c, _pixels[idxMM]), o); if (_pixelCCT) { if (topSegment.mirror) _pixelCCT[idxMX] = cct; if (topSegment.mirror_y) _pixelCCT[idxMY] = cct; if (topSegment.mirror && topSegment.mirror_y) _pixelCCT[idxMM] = cct; } } }; // if we blend using "push" style we need to "shift" canvas to left/right/up/down unsigned offsetX = (blendingStyle == BLEND_STYLE_PUSH_UP || blendingStyle == BLEND_STYLE_PUSH_DOWN) ? 0 : progInv * nCols / 0xFFFFU; unsigned offsetY = (blendingStyle == BLEND_STYLE_PUSH_LEFT || blendingStyle == BLEND_STYLE_PUSH_RIGHT) ? 0 : progInv * nRows / 0xFFFFU; // we only traverse new segment, not old one for (int r = 0; r < nRows; r++) for (int c = 0; c < nCols; c++) { const bool clipped = topSegment.isPixelXYClipped(c, r); // if segment is in transition and pixel is clipped take old segment's pixel and opacity const Segment *seg = clipped && segO ? segO : &topSegment; // pixel is never clipped for FADE int vCols = seg == segO ? oCols : nCols; // old segment may have different dimensions int vRows = seg == segO ? oRows : nRows; // old segment may have different dimensions int x = c; int y = r; // if we blend using "push" style we need to "shift" canvas to left/right/up/down switch (blendingStyle) { case BLEND_STYLE_PUSH_RIGHT: x = (x + offsetX) % nCols; break; case BLEND_STYLE_PUSH_LEFT: x = (x - offsetX + nCols) % nCols; break; case BLEND_STYLE_PUSH_DOWN: y = (y + offsetY) % nRows; break; case BLEND_STYLE_PUSH_UP: y = (y - offsetY + nRows) % nRows; break; } uint32_t c_a = BLACK; if (x < vCols && y < vRows) c_a = seg->getPixelColorRaw(x + y*vCols); // will get clipped pixel from old segment or unclipped pixel from new segment if (segO && blendingStyle == BLEND_STYLE_FADE && (topSegment.mode != segO->mode || (segO->name != topSegment.name && segO->name && topSegment.name && strncmp(segO->name, topSegment.name, WLED_MAX_SEGNAME_LEN) != 0)) && x < oCols && y < oRows) { // we need to blend old segment using fade as pixels are not clipped c_a = color_blend16(c_a, segO->getPixelColorRaw(x + y*oCols), progInv); } else if (blendingStyle != BLEND_STYLE_FADE) { // if we have global brightness change (not On/Off change) we will ignore transition style and just fade brightness (see led.cpp) // workaround for On/Off transition // (bri != briT) && !bri => from On to Off // (bri != briT) && bri => from Off to On if ((briOld == 0 || bri == 0) && ((!clipped && (bri != briT) && !bri) || (clipped && (bri != briT) && bri))) c_a = BLACK; } // map it into frame buffer x = c; // restore coordiates if we were PUSHing y = r; if (topSegment.reverse ) x = nCols - x - 1; if (topSegment.reverse_y) y = nRows - y - 1; if (topSegment.transpose) std::swap(x,y); // swap X & Y if segment transposed // expand pixel const unsigned groupLen = topSegment.groupLength(); if (groupLen == 1) { setMirroredPixel(x, y, c_a, opacity); } else { // handle grouping and spacing x *= groupLen; // expand to physical pixels y *= groupLen; // expand to physical pixels const int maxX = std::min(x + topSegment.grouping, width); const int maxY = std::min(y + topSegment.grouping, height); while (y < maxY) { int _x = x; while (_x < maxX) setMirroredPixel(_x++, y, c_a, opacity); y++; } } } #endif } else { const int nLen = topSegment.virtualLength(); const Segment *segO = topSegment.getOldSegment(); const int oLen = segO ? segO->virtualLength() : nLen; const auto setMirroredPixel = [&](int i, uint32_t c, uint8_t o) { int indx = topSegment.start + i; // Apply mirroring if (topSegment.mirror) { unsigned indxM = topSegment.stop - i - 1; indxM += topSegment.offset; // offset/phase if (indxM >= topSegment.stop) indxM -= length; // wrap _pixels[indxM] = color_blend(_pixels[indxM], blend(c, _pixels[indxM]), o); if (_pixelCCT) _pixelCCT[indxM] = cct; } indx += topSegment.offset; // offset/phase if (indx >= topSegment.stop) indx -= length; // wrap _pixels[indx] = color_blend(_pixels[indx], blend(c, _pixels[indx]), o); if (_pixelCCT) _pixelCCT[indx] = cct; }; // if we blend using "push" style we need to "shift" canvas to left/right/ unsigned offsetI = progInv * nLen / 0xFFFFU; for (int k = 0; k < nLen; k++) { const bool clipped = topSegment.isPixelClipped(k); // if segment is in transition and pixel is clipped take old segment's pixel and opacity const Segment *seg = clipped && segO ? segO : &topSegment; // pixel is never clipped for FADE const int vLen = seg == segO ? oLen : nLen; int i = k; // if we blend using "push" style we need to "shift" canvas to left or right switch (blendingStyle) { case BLEND_STYLE_PUSH_RIGHT: i = (i + offsetI) % nLen; break; case BLEND_STYLE_PUSH_LEFT: i = (i - offsetI + nLen) % nLen; break; } uint32_t c_a = BLACK; if (i < vLen) c_a = seg->getPixelColorRaw(i); // will get clipped pixel from old segment or unclipped pixel from new segment if (segO && blendingStyle == BLEND_STYLE_FADE && topSegment.mode != segO->mode && i < oLen) { // we need to blend old segment using fade as pixels are not clipped c_a = color_blend16(c_a, segO->getPixelColorRaw(i), progInv); } else if (blendingStyle != BLEND_STYLE_FADE) { // if we have global brightness change (not On/Off change) we will ignore transition style and just fade brightness (see led.cpp) // workaround for On/Off transition // (bri != briT) && !bri => from On to Off // (bri != briT) && bri => from Off to On if ((briOld == 0 || bri == 0) && ((!clipped && (bri != briT) && !bri) || (clipped && (bri != briT) && bri))) c_a = BLACK; } // map into frame buffer i = k; // restore index if we were PUSHing if (topSegment.reverse) i = nLen - i - 1; // is segment reversed? // expand pixel i *= topSegment.groupLength(); // set all the pixels in the group const int maxI = std::min(i + topSegment.grouping, length); // make sure to not go beyond physical length while (i < maxI) setMirroredPixel(i++, c_a, opacity); } } blendingStyle = orgBS; Segment::setClippingRect(0, 0); // disable clipping for overlays } void WS2812FX::show() { if (!_pixels) { DEBUGFX_PRINTLN(F("Error: no _pixels!")); errorFlag = ERR_NORAM; return; // no pixels allocated, nothing to show } unsigned long showNow = millis(); size_t diff = showNow - _lastShow; size_t totalLen = getLengthTotal(); // WARNING: as WLED doesn't handle CCT on pixel level but on Segment level instead // we need to keep track of each pixel's CCT when blending segments (if CCT is present) // and then set appropriate CCT from that pixel during paint (see below). if ((hasCCTBus() || correctWB) && !cctFromRgb) _pixelCCT = static_cast(allocate_buffer(totalLen * sizeof(uint8_t), BFRALLOC_PREFER_PSRAM)); // allocate CCT buffer if necessary, prefer PSRAM if (_pixelCCT) memset(_pixelCCT, 127, totalLen); // set neutral (50:50) CCT if (realtimeMode == REALTIME_MODE_INACTIVE || useMainSegmentOnly || realtimeOverride > REALTIME_OVERRIDE_NONE) { // clear frame buffer for (size_t i = 0; i < totalLen; i++) _pixels[i] = BLACK; // memset(_pixels, 0, sizeof(uint32_t) * getLengthTotal()); // blend all segments into (cleared) buffer for (Segment &seg : _segments) if (seg.isActive() && (seg.on || seg.isInTransition())) { blendSegment(seg); // blend segment's buffer into frame buffer } } // avoid race condition, capture _callback value show_callback callback = _callback; if (callback) callback(); // will call setPixelColor or setRealtimePixelColor // paint actual pixels int oldCCT = Bus::getCCT(); // store original CCT value (since it is global) // when cctFromRgb is true we implicitly calculate WW and CW from RGB values (cct==-1) if (cctFromRgb) BusManager::setSegmentCCT(-1); for (size_t i = 0; i < totalLen; i++) { // when correctWB is true setSegmentCCT() will convert CCT into K with which we can then // correct/adjust RGB value according to desired CCT value, it will still affect actual WW/CW ratio if (_pixelCCT) { // cctFromRgb already exluded at allocation if (i == 0 || _pixelCCT[i-1] != _pixelCCT[i]) BusManager::setSegmentCCT(_pixelCCT[i], correctWB); } uint32_t c = _pixels[i]; // need a copy, do not modify _pixels directly (no byte access allowed on ESP32) if(c > 0 && !(realtimeMode && arlsDisableGammaCorrection)) c = gamma32(c); // apply gamma correction if enabled note: applying gamma after brightness has too much color loss BusManager::setPixelColor(getMappedPixelIndex(i), c); } Bus::setCCT(oldCCT); // restore old CCT for ABL adjustments p_free(_pixelCCT); _pixelCCT = nullptr; // some buses send asynchronously and this method will return before // all of the data has been sent. // See https://github.com/Makuna/NeoPixelBus/wiki/ESP32-NeoMethods#neoesp32rmt-methods BusManager::show(); if (diff > 0) { // skip calculation if no time has passed size_t fpsCurr = (1000 << FPS_CALC_SHIFT) / diff; // fixed point math _cumulativeFps = (FPS_CALC_AVG * _cumulativeFps + fpsCurr + FPS_CALC_AVG / 2) / (FPS_CALC_AVG + 1); // "+FPS_CALC_AVG/2" for proper rounding _lastShow = showNow; } } void WS2812FX::setRealtimePixelColor(unsigned i, uint32_t c) { if (useMainSegmentOnly) { const Segment &seg = getMainSegment(); if (seg.isActive() && i < seg.length()) seg.setPixelColorRaw(i, c); } else { setPixelColor(i, c); } } // reset all segments void WS2812FX::restartRuntime() { suspend(); waitForIt(); for (Segment &seg : _segments) seg.markForReset().resetIfRequired(); resume(); } // start or stop transition for all segments void WS2812FX::setTransitionMode(bool t) { suspend(); waitForIt(); for (Segment &seg : _segments) seg.startTransition(t ? _transitionDur : 0); resume(); } // wait until frame is over (service() has finished or time for 2 frames have passed; yield() crashes on 8266) // the latter may, in rare circumstances, lead to incorrectly assuming strip is done servicing but will not block // other processing "indefinitely" // rare circumstances are: setting FPS to high number (i.e. 120) and have very slow effect that will need more // time than 2 * _frametime (1000/FPS) to draw content void WS2812FX::waitForIt() { unsigned long waitStart = millis(); unsigned long maxWait = 2*getFrameTime() + 100; // TODO: this needs a proper fix for timeout! see #4779 while (isServicing() && (millis() - waitStart < maxWait)) delay(1); // safe even when millis() rolls over #ifdef WLED_DEBUG if (millis()-waitStart >= maxWait) DEBUG_PRINTLN(F("Waited for strip to finish servicing.")); #endif }; void WS2812FX::setTargetFps(unsigned fps) { if (fps <= 250) _targetFps = fps; if (_targetFps > 0) _frametime = 1000 / _targetFps; else _frametime = MIN_FRAME_DELAY; // unlimited mode } void WS2812FX::setCCT(uint16_t k) { for (Segment &seg : _segments) { if (seg.isActive() && seg.isSelected()) { seg.setCCT(k); } } } // direct=true either expects the caller to call show() themselves (realtime modes) or be ok waiting for the next frame for the change to apply // direct=false immediately triggers an effect redraw void WS2812FX::setBrightness(uint8_t b, bool direct) { if (gammaCorrectBri) b = gamma8(b); if (_brightness == b) return; _brightness = b; if (_brightness == 0) { //unfreeze all segments on power off for (const Segment &seg : _segments) seg.freeze = false; // freeze is mutable } BusManager::setBrightness(scaledBri(b)); if (!direct) { unsigned long t = millis(); if (t - _lastShow > MIN_SHOW_DELAY) trigger(); //apply brightness change immediately if no refresh soon } } uint8_t WS2812FX::getActiveSegsLightCapabilities(bool selectedOnly) const { uint8_t totalLC = 0; for (const Segment &seg : _segments) { if (seg.isActive() && (!selectedOnly || seg.isSelected())) totalLC |= seg.getLightCapabilities(); } return totalLC; } uint8_t WS2812FX::getFirstSelectedSegId() const { size_t i = 0; for (const Segment &seg : _segments) { if (seg.isActive() && seg.isSelected()) return i; i++; } // if none selected, use the main segment return getMainSegmentId(); } void WS2812FX::setMainSegmentId(unsigned n) { _mainSegment = getLastActiveSegmentId(); if (n < _segments.size() && _segments[n].isActive()) { // only set if segment is active _mainSegment = n; } return; } uint8_t WS2812FX::getLastActiveSegmentId() const { for (size_t i = _segments.size() -1; i > 0; i--) { if (_segments[i].isActive()) return i; } return 0; } uint8_t WS2812FX::getActiveSegmentsNum() const { unsigned c = 0; for (const Segment &seg : _segments) if (seg.isActive()) c++; return c; } uint16_t WS2812FX::getLengthTotal() const { unsigned len = Segment::maxWidth * Segment::maxHeight; // will be _length for 1D (see finalizeInit()) but should cover whole matrix for 2D if (isMatrix && _length > len) len = _length; // for 2D with trailing strip return len; } uint16_t WS2812FX::getLengthPhysical() const { return BusManager::getTotalLength(true); } //used for JSON API info.leds.rgbw. Little practical use, deprecate with info.leds.rgbw. //returns if there is an RGBW bus (supports RGB and White, not only white) //not influenced by auto-white mode, also true if white slider does not affect output white channel bool WS2812FX::hasRGBWBus() const { for (size_t b = 0; b < BusManager::getNumBusses(); b++) { const Bus *bus = BusManager::getBus(b); if (!bus || !bus->isOk()) break; if (bus->hasRGB() && bus->hasWhite()) return true; } return false; } bool WS2812FX::hasCCTBus() const { if (cctFromRgb && !correctWB) return false; for (size_t b = 0; b < BusManager::getNumBusses(); b++) { const Bus *bus = BusManager::getBus(b); if (!bus || !bus->isOk()) break; if (bus->hasCCT()) return true; } return false; } void WS2812FX::purgeSegments() { // remove all inactive segments (from the back) int deleted = 0; if (_segments.size() <= 1) return; for (size_t i = _segments.size()-1; i > 0; i--) if (_segments[i].stop == 0) { deleted++; _segments.erase(_segments.begin() + i); } if (deleted) { _segments.shrink_to_fit(); setMainSegmentId(0); } } Segment& WS2812FX::getSegment(unsigned id) { return _segments[id >= _segments.size() ? getMainSegmentId() : id]; // vectors } // WARNING: resetSegments(), makeAutoSegments() and fixInvalidSegments() must not be called while // strip is being serviced (strip.service()), you must call suspend prior if changing segments outside // loop() context void WS2812FX::resetSegments() { if (isServicing()) return; _segments.clear(); // destructs all Segment as part of clearing _segments.emplace_back(0, isMatrix ? Segment::maxWidth : _length, 0, isMatrix ? Segment::maxHeight : 1); if(_segments.size() == 0) { _segments.emplace_back(); // if out of heap, create a default segment errorFlag = ERR_NORAM_PX; } _segments.shrink_to_fit(); // just in case ... _mainSegment = 0; } void WS2812FX::makeAutoSegments(bool forceReset) { if (isServicing()) return; if (autoSegments) { //make one segment per bus unsigned segStarts[MAX_NUM_SEGMENTS] = {0}; unsigned segStops [MAX_NUM_SEGMENTS] = {0}; size_t s = 0; #ifndef WLED_DISABLE_2D // 2D segment is the 1st one using entire matrix if (isMatrix) { segStarts[0] = 0; segStops[0] = Segment::maxWidth*Segment::maxHeight; s++; } #endif for (size_t i = s; i < BusManager::getNumBusses(); i++) { const Bus *bus = BusManager::getBus(i); if (!bus) break; segStarts[s] = bus->getStart(); segStops[s] = segStarts[s] + bus->getLength(); #ifndef WLED_DISABLE_2D if (isMatrix && segStops[s] <= Segment::maxWidth*Segment::maxHeight) continue; // ignore buses comprising matrix if (isMatrix && segStarts[s] < Segment::maxWidth*Segment::maxHeight) segStarts[s] = Segment::maxWidth*Segment::maxHeight; #endif //check for overlap with previous segments for (size_t j = 0; j < s; j++) { if (segStops[j] > segStarts[s] && segStarts[j] < segStops[s]) { //segments overlap, merge segStarts[j] = min(segStarts[s],segStarts[j]); segStops [j] = max(segStops [s],segStops [j]); segStops[s] = 0; s--; } } s++; } _segments.clear(); _segments.reserve(s); // prevent reallocations // there is always at least one segment (but we need to differentiate between 1D and 2D) #ifndef WLED_DISABLE_2D if (isMatrix) _segments.emplace_back(0, Segment::maxWidth, 0, Segment::maxHeight); else #endif _segments.emplace_back(segStarts[0], segStops[0]); for (size_t i = 1; i < s; i++) { _segments.emplace_back(segStarts[i], segStops[i]); } DEBUGFX_PRINTF_P(PSTR("%d auto segments created.\n"), _segments.size()); } else { if (forceReset || getSegmentsNum() == 0) resetSegments(); //expand the main seg to the entire length, but only if there are no other segments, or reset is forced else if (getActiveSegmentsNum() == 1) { size_t i = getLastActiveSegmentId(); #ifndef WLED_DISABLE_2D _segments[i].setGeometry(0, Segment::maxWidth, 1, 0, 0xFFFF, 0, Segment::maxHeight); #else _segments[i].setGeometry(0, _length); #endif } } _mainSegment = 0; fixInvalidSegments(); } void WS2812FX::fixInvalidSegments() { if (isServicing()) return; //make sure no segment is longer than total (sanity check) for (size_t i = getSegmentsNum()-1; i > 0; i--) { if (isMatrix) { #ifndef WLED_DISABLE_2D if (_segments[i].start >= Segment::maxWidth * Segment::maxHeight) { // 1D segment at the end of matrix if (_segments[i].start >= _length || _segments[i].startY > 0 || _segments[i].stopY > 1) { _segments.erase(_segments.begin()+i); continue; } if (_segments[i].stop > _length) _segments[i].stop = _length; continue; } if (_segments[i].start >= Segment::maxWidth || _segments[i].startY >= Segment::maxHeight) { _segments.erase(_segments.begin()+i); continue; } if (_segments[i].stop > Segment::maxWidth) _segments[i].stop = Segment::maxWidth; if (_segments[i].stopY > Segment::maxHeight) _segments[i].stopY = Segment::maxHeight; #endif } else { if (_segments[i].start >= _length) { _segments.erase(_segments.begin()+i); continue; } if (_segments[i].stop > _length) _segments[i].stop = _length; } } // if any segments were deleted free memory purgeSegments(); // this is always called as the last step after finalizeInit(), update covered bus types for (const Segment &seg : _segments) seg.refreshLightCapabilities(); } //true if all segments align with a bus, or if a segment covers the total length //irrelevant in 2D set-up bool WS2812FX::checkSegmentAlignment() const { bool aligned = false; for (const Segment &seg : _segments) { for (unsigned b = 0; bisOk()) break; if (seg.start == bus->getStart() && seg.stop == bus->getStart() + bus->getLength()) aligned = true; } if (seg.start == 0 && seg.stop == _length) aligned = true; if (!aligned) return false; } return true; } // used by analog clock overlay void WS2812FX::setRange(uint16_t i, uint16_t i2, uint32_t col) { if (i2 < i) std::swap(i,i2); for (unsigned x = i; x <= i2; x++) setPixelColor(x, col); } #ifdef WLED_DEBUG void WS2812FX::printSize() { size_t size = 0; for (const Segment &seg : _segments) size += seg.getSize(); DEBUG_PRINTF_P(PSTR("Segments: %d -> %u/%dB\n"), _segments.size(), size, Segment::getUsedSegmentData()); for (const Segment &seg : _segments) DEBUG_PRINTF_P(PSTR(" Seg: %d,%d [A=%d, 2D=%d, RGB=%d, W=%d, CCT=%d]\n"), seg.width(), seg.height(), seg.isActive(), seg.is2D(), seg.hasRGB(), seg.hasWhite(), seg.isCCT()); DEBUG_PRINTF_P(PSTR("Modes: %d*%d=%uB\n"), sizeof(mode_ptr), _mode.size(), (_mode.capacity()*sizeof(mode_ptr))); DEBUG_PRINTF_P(PSTR("Data: %d*%d=%uB\n"), sizeof(const char *), _modeData.size(), (_modeData.capacity()*sizeof(const char *))); DEBUG_PRINTF_P(PSTR("Map: %d*%d=%uB\n"), sizeof(uint16_t), (int)customMappingSize, customMappingSize*sizeof(uint16_t)); } #endif // load custom mapping table from JSON file (called from finalizeInit() or deserializeState()) // if this is a matrix set-up and default ledmap.json file does not exist, create mapping table using setUpMatrix() from panel information // WARNING: effect drawing has to be suspended (strip.suspend()) or must be called from loop() context bool WS2812FX::deserializeMap(unsigned n) { char fileName[32]; strcpy_P(fileName, PSTR("/ledmap")); if (n) sprintf(fileName +7, "%d", n); strcat_P(fileName, PSTR(".json")); bool isFile = WLED_FS.exists(fileName); customMappingSize = 0; // prevent use of mapping if anything goes wrong currentLedmap = 0; if (n == 0 || isFile) interfaceUpdateCallMode = CALL_MODE_WS_SEND; // schedule WS update (to inform UI) if (!isFile && n==0 && isMatrix) { // 2D panel support creates its own ledmap (on the fly) if a ledmap.json does not exist setUpMatrix(); return false; } if (!isFile || !requestJSONBufferLock(JSON_LOCK_LEDMAP)) return false; StaticJsonDocument<64> filter; filter[F("width")] = true; filter[F("height")] = true; if (!readObjectFromFile(fileName, nullptr, pDoc, &filter)) { DEBUG_PRINTF_P(PSTR("ERROR Invalid ledmap in %s\n"), fileName); releaseJSONBufferLock(); return false; // if file does not load properly then exit } else DEBUG_PRINTF_P(PSTR("Reading LED map from %s\n"), fileName); JsonObject root = pDoc->as(); // if we are loading default ledmap (at boot) set matrix width and height from the ledmap (compatible with WLED MM ledmaps) if (n == 0 && (!root[F("width")].isNull() || !root[F("height")].isNull())) { Segment::maxWidth = min(max(root[F("width")].as(), 1), 255); Segment::maxHeight = min(max(root[F("height")].as(), 1), 255); isMatrix = true; DEBUG_PRINTF_P(PSTR("LED map width=%d, height=%d\n"), Segment::maxWidth, Segment::maxHeight); } d_free(customMappingTable); customMappingTable = static_cast(d_malloc(sizeof(uint16_t)*getLengthTotal())); // prefer DRAM for speed if (customMappingTable) { DEBUG_PRINTF_P(PSTR("ledmap allocated: %uB\n"), sizeof(uint16_t)*getLengthTotal()); File f = WLED_FS.open(fileName, "r"); f.find("\"map\":["); while (f.available()) { // f.position() < f.size() - 1 char number[32]; size_t numRead = f.readBytesUntil(',', number, sizeof(number)-1); // read a single number (may include array terminating "]" but not number separator ',') number[numRead] = 0; if (numRead > 0) { char *end = strchr(number,']'); // we encountered end of array so stop processing if no digit found bool foundDigit = (end == nullptr); int i = 0; if (end != nullptr) do { if (number[i] >= '0' && number[i] <= '9') foundDigit = true; if (foundDigit || &number[i++] == end) break; } while (i < 32); if (!foundDigit) break; int index = atoi(number); if (index < 0 || index > 65535) index = 0xFFFF; // prevent integer wrap around customMappingTable[customMappingSize++] = index; if (customMappingSize >= getLengthTotal()) break; } else break; // there was nothing to read, stop } currentLedmap = n; f.close(); #ifdef WLED_DEBUG DEBUG_PRINT(F("Loaded ledmap:")); for (unsigned i=0; i 0); } const char JSON_mode_names[] PROGMEM = R"=====(["FX names moved"])====="; const char JSON_palette_names[] PROGMEM = R"=====([ "Default","* Random Cycle","* Color 1","* Colors 1&2","* Color Gradient","* Colors Only","Party","Cloud","Lava","Ocean", "Forest","Rainbow","Rainbow Bands","Sunset","Rivendell","Breeze","Red & Blue","Yellowout","Analogous","Splash", "Pastel","Sunset 2","Beach","Vintage","Departure","Landscape","Beech","Sherbet","Hult","Hult 64", "Drywet","Jul","Grintage","Rewhi","Tertiary","Fire","Icefire","Cyane","Light Pink","Autumn", "Magenta","Magred","Yelmag","Yelblu","Orange & Teal","Tiamat","April Night","Orangery","C9","Sakura", "Aurora","Atlantica","C9 2","C9 New","Temperature","Aurora 2","Retro Clown","Candy","Toxy Reaf","Fairy Reaf", "Semi Blue","Pink Candy","Red Reaf","Aqua Flash","Yelblu Hot","Lite Light","Red Flash","Blink Red","Red Shift","Red Tide", "Candy2","Traffic Light" ])====="; ================================================ FILE: wled00/FXparticleSystem.cpp ================================================ /* FXparticleSystem.cpp Particle system with functions for particle generation, particle movement and particle rendering to RGB matrix. by DedeHai (Damian Schneider) 2013-2024 Copyright (c) 2024 Damian Schneider Licensed under the EUPL v. 1.2 or later */ #ifdef WLED_DISABLE_2D #define WLED_DISABLE_PARTICLESYSTEM2D #endif #if !(defined(WLED_DISABLE_PARTICLESYSTEM2D) && defined(WLED_DISABLE_PARTICLESYSTEM1D)) // not both disabled #include "FXparticleSystem.h" // local shared functions (used both in 1D and 2D system) static int32_t calcForce_dv(const int8_t force, uint8_t &counter); static bool checkBoundsAndWrap(int32_t &position, const int32_t max, const int32_t particleradius, const bool wrap); // returns false if out of bounds by more than particleradius static uint32_t fast_color_scaleAdd(const uint32_t c1, const uint32_t c2, uint8_t scale = 255); // fast and accurate color adding with scaling (scales c2 before adding) #endif #ifndef WLED_DISABLE_PARTICLESYSTEM2D ParticleSystem2D::ParticleSystem2D(uint32_t width, uint32_t height, uint32_t numberofparticles, uint32_t numberofsources, bool isadvanced, bool sizecontrol) { PSPRINTLN("\n ParticleSystem2D constructor"); numSources = numberofsources; // number of sources allocated in init numParticles = numberofparticles; // number of particles allocated in init usedParticles = numParticles; // use all particles by default advPartProps = nullptr; //make sure we start out with null pointers (just in case memory was not cleared) advPartSize = nullptr; setMatrixSize(width, height); updatePSpointers(isadvanced, sizecontrol); // set the particle and sources pointer (call this before accessing sprays or particles) setWallHardness(255); // set default wall hardness to max setWallRoughness(0); // smooth walls by default setGravity(0); //gravity disabled by default setParticleSize(1); // 2x2 rendering size by default (disables per particle size control by default) motionBlur = 0; //no fading by default smearBlur = 0; //no smearing by default emitIndex = 0; collisionStartIdx = 0; //initialize some default non-zero values most FX use for (uint32_t i = 0; i < numParticles; i++) { particles[i].sat = 255; // full saturation } for (uint32_t i = 0; i < numSources; i++) { sources[i].source.sat = 255; //set saturation to max by default sources[i].source.ttl = 1; //set source alive sources[i].sourceFlags.asByte = 0; // all flags disabled } perParticleSize = isadvanced; // enable per particle size by default if using advanced properties (FX can disable if needed) } // update function applies gravity, moves the particles, handles collisions and renders the particles void ParticleSystem2D::update(void) { //apply gravity globally if enabled if (particlesettings.useGravity) applyGravity(); //update size settings before handling collisions if (advPartSize != nullptr) { for (uint32_t i = 0; i < usedParticles; i++) { if (updateSize(&advPartProps[i], &advPartSize[i]) == false) { // if particle shrinks to 0 size particles[i].ttl = 0; // kill particle } } } // handle collisions (can push particles, must be done before updating particles or they can render out of bounds, causing a crash if using local buffer for speed) if (particlesettings.useCollisions) handleCollisions(); //move all particles for (uint32_t i = 0; i < usedParticles; i++) { particleMoveUpdate(particles[i], particleFlags[i], nullptr, advPartProps ? &advPartProps[i] : nullptr); // note: splitting this into two loops is slower and uses more flash } render(); } // update function for fire animation void ParticleSystem2D::updateFire(const uint8_t intensity) { fireParticleupdate(); fireIntesity = intensity > 0 ? intensity : 1; // minimum of 1, zero checking is used in render function render(); } // set percentage of used particles as uint8_t i.e 127 means 50% for example void ParticleSystem2D::setUsedParticles(uint8_t percentage) { usedParticles = max((uint32_t)1, (numParticles * ((int)percentage+1)) >> 8); // number of particles to use (percentage is 0-255, 255 = 100%) PSPRINT(" SetUsedpaticles: allocated particles: "); PSPRINT(numParticles); PSPRINT(" ,used particles: "); PSPRINTLN(usedParticles); } void ParticleSystem2D::setWallHardness(uint8_t hardness) { wallHardness = hardness; } void ParticleSystem2D::setWallRoughness(uint8_t roughness) { wallRoughness = roughness; } void ParticleSystem2D::setCollisionHardness(uint8_t hardness) { collisionHardness = (int)hardness + 1; } void ParticleSystem2D::setMatrixSize(uint32_t x, uint32_t y) { maxXpixel = x - 1; // last physical pixel that can be drawn to maxYpixel = y - 1; maxX = x * PS_P_RADIUS - 1; // particle system boundary for movements maxY = y * PS_P_RADIUS - 1; // this value is often needed (also by FX) to calculate positions } void ParticleSystem2D::setWrapX(bool enable) { particlesettings.wrapX = enable; } void ParticleSystem2D::setWrapY(bool enable) { particlesettings.wrapY = enable; } void ParticleSystem2D::setBounceX(bool enable) { particlesettings.bounceX = enable; } void ParticleSystem2D::setBounceY(bool enable) { particlesettings.bounceY = enable; } void ParticleSystem2D::setKillOutOfBounds(bool enable) { particlesettings.killoutofbounds = enable; } void ParticleSystem2D::setColorByAge(bool enable) { particlesettings.colorByAge = enable; } void ParticleSystem2D::setMotionBlur(uint8_t bluramount) { motionBlur = bluramount; } void ParticleSystem2D::setSmearBlur(uint8_t bluramount) { smearBlur = bluramount; } // set global particle size void ParticleSystem2D::setParticleSize(uint8_t size) { particlesize = size; particleHardRadius = PS_P_MINHARDRADIUS; // ~1 pixel perParticleSize = false; // disable per particle size control if global size is set if (particlesize > 1) { particleHardRadius = PS_P_MINHARDRADIUS + ((particlesize * 52) >> 6); // use 1 pixel + 80% of size for hard radius (slight overlap with boarders so they do not "float" and nicer stacking) } else if (particlesize == 0) particleHardRadius = PS_P_MINHARDRADIUS >> 1; // single pixel particles have half the radius (i.e. 1/2 pixel) } // enable/disable gravity, optionally, set the force (force=8 is default) can be -127 to +127, 0 is disable // if enabled, gravity is applied to all particles in ParticleSystemUpdate() // force is in 3.4 fixed point notation so force=16 means apply v+1 each frame default of 8 is every other frame (gives good results) void ParticleSystem2D::setGravity(int8_t force) { if (force) { gforce = force; particlesettings.useGravity = true; } else { particlesettings.useGravity = false; } } void ParticleSystem2D::enableParticleCollisions(bool enable, uint8_t hardness) { // enable/disable gravity, optionally, set the force (force=8 is default) can be 1-255, 0 is also disable particlesettings.useCollisions = enable; collisionHardness = (int)hardness + 1; } // emit one particle with variation, returns index of emitted particle (or -1 if no particle emitted) int32_t ParticleSystem2D::sprayEmit(const PSsource &emitter) { bool success = false; for (uint32_t i = 0; i < usedParticles; i++) { emitIndex++; if (emitIndex >= usedParticles) emitIndex = 0; if (particles[emitIndex].ttl == 0) { // find a dead particle success = true; int32_t dx = hw_random16(emitter.var << 1) - emitter.var; int32_t dy = hw_random16(emitter.var << 1) - emitter.var; if (emitter.var > 5) { // use circular random distribution for large variance to generate nicer "explosions" while (dx*dx + dy*dy > emitter.var*emitter.var) { // reject points outside circle dx = hw_random16(emitter.var << 1) - emitter.var; dy = hw_random16(emitter.var << 1) - emitter.var; } } particles[emitIndex].vx = emitter.vx + dx; particles[emitIndex].vy = emitter.vy + dy; particles[emitIndex].x = emitter.source.x; particles[emitIndex].y = emitter.source.y; particles[emitIndex].hue = emitter.source.hue; particles[emitIndex].sat = emitter.source.sat; particleFlags[emitIndex].collide = emitter.sourceFlags.collide; particles[emitIndex].ttl = hw_random16(emitter.minLife, emitter.maxLife); if (advPartProps != nullptr) advPartProps[emitIndex].size = emitter.size; break; } } if (success) return emitIndex; else return -1; } // Spray emitter for particles used for flames (particle TTL depends on source TTL) void ParticleSystem2D::flameEmit(const PSsource &emitter) { int emitIndex = sprayEmit(emitter); if (emitIndex > 0) particles[emitIndex].ttl += emitter.source.ttl; } // Emits a particle at given angle and speed, angle is from 0-65535 (=0-360deg), speed is also affected by emitter->var // angle = 0 means in positive x-direction (i.e. to the right) int32_t ParticleSystem2D::angleEmit(PSsource &emitter, const uint16_t angle, const int32_t speed) { emitter.vx = ((int32_t)cos16_t(angle) * speed) / (int32_t)32600; // cos16_t() and sin16_t() return signed 16bit, division should be 32767 but 32600 gives slightly better rounding emitter.vy = ((int32_t)sin16_t(angle) * speed) / (int32_t)32600; // note: cannot use bit shifts as bit shifting is asymmetrical (1>>1=0 / -1>>1=-1) and this needs to be accurate! return sprayEmit(emitter); } // particle moves, decays and dies, if killoutofbounds is set, out of bounds particles are set to ttl=0 // uses passed settings to set bounce or wrap, if useGravity is enabled, it will never bounce at the top and killoutofbounds is not applied over the top void ParticleSystem2D::particleMoveUpdate(PSparticle &part, PSparticleFlags &partFlags, PSsettings2D *options, PSadvancedParticle *advancedproperties) { if (options == nullptr) options = &particlesettings; //use PS system settings by default if (part.ttl > 0) { if (!partFlags.perpetual) part.ttl--; // age if (options->colorByAge) part.hue = min(part.ttl, (uint16_t)255); //set color to ttl int32_t renderradius = PS_P_HALFRADIUS - 1 + particlesize; // used to check out of bounds, if its more than half a radius out of bounds, it will render to x = -2/-1 or x=max/max+1 in standard 2x2 rendering int32_t newX = part.x + (int32_t)part.vx; int32_t newY = part.y + (int32_t)part.vy; partFlags.outofbounds = false; // reset out of bounds (in case particle was created outside the matrix and is now moving into view) note: moving this to checks below adds code and is not faster if (perParticleSize && advancedproperties != nullptr) { // using individual particle size renderradius = PS_P_HALFRADIUS - 1 + advancedproperties->size; // note: single pixel particles should be zero but OOB checks in rendering function handle this if (advancedproperties->size > 0) particleHardRadius = PS_P_MINHARDRADIUS + ((advancedproperties->size * 52) >> 6); // use 1 pixel + 80% of size for hard radius (slight overlap with boarders so they do not "float") else // single pixel particles use half the collision distance for walls particleHardRadius = PS_P_MINHARDRADIUS >> 1; } // note: if wall collisions are enabled, bounce them before they reach the edge, it looks much nicer if the particle does not go half out of view if (options->bounceY) { if ((newY < (int32_t)particleHardRadius) || ((newY > (int32_t)(maxY - particleHardRadius)) && !options->useGravity)) { // reached floor / ceiling bounce(part.vy, part.vx, newY, maxY); } } if (!checkBoundsAndWrap(newY, maxY, renderradius, options->wrapY)) { // check out of bounds note: this must not be skipped. if gravity is enabled, particles will never bounce at the top partFlags.outofbounds = true; if (options->killoutofbounds) { if (newY < 0) // if gravity is enabled, only kill particles below ground part.ttl = 0; else if (!options->useGravity) part.ttl = 0; } } if (part.ttl) { //check x direction only if still alive if (options->bounceX) { if ((newX < (int32_t)particleHardRadius) || (newX > (int32_t)(maxX - particleHardRadius))) // reached a wall bounce(part.vx, part.vy, newX, maxX); } else if (!checkBoundsAndWrap(newX, maxX, renderradius, options->wrapX)) { // check out of bounds partFlags.outofbounds = true; if (options->killoutofbounds) part.ttl = 0; } } part.x = (int16_t)newX; // set new position part.y = (int16_t)newY; // set new position } } // move function for fire particles void ParticleSystem2D::fireParticleupdate() { for (uint32_t i = 0; i < usedParticles; i++) { if (particles[i].ttl > 0) { particles[i].ttl--; // age int32_t newY = particles[i].y + (int32_t)particles[i].vy + (particles[i].ttl >> 2); // younger particles move faster upward as they are hotter int32_t newX = particles[i].x + (int32_t)particles[i].vx; particleFlags[i].outofbounds = 0; // reset out of bounds flag note: moving this to checks below is not faster but adds code // check if particle is out of bounds, wrap x around to other side if wrapping is enabled // as fire particles start below the frame, lots of particles are out of bounds in y direction. to improve speed, only check x direction if y is not out of bounds if (newY < -PS_P_HALFRADIUS) particleFlags[i].outofbounds = 1; else if (newY > int32_t(maxY + PS_P_HALFRADIUS)) // particle moved out at the top particles[i].ttl = 0; else // particle is in frame in y direction, also check x direction now Note: using checkBoundsAndWrap() is slower, only saves a few bytes { if ((newX < 0) || (newX > (int32_t)maxX)) { // handle out of bounds & wrap if (particlesettings.wrapX) { newX = newX % (maxX + 1); if (newX < 0) // handle negative modulo newX += maxX + 1; } else if ((newX < -PS_P_HALFRADIUS) || (newX > int32_t(maxX + PS_P_HALFRADIUS))) { //if fully out of view particles[i].ttl = 0; } } particles[i].x = newX; } particles[i].y = newY; } } } // update advanced particle size control, returns false if particle shrinks to 0 size bool ParticleSystem2D::updateSize(PSadvancedParticle *advprops, PSsizeControl *advsize) { if (advsize == nullptr) // safety check return false; // grow/shrink particle int32_t newsize = advprops->size; uint32_t counter = advsize->sizecounter; uint32_t increment = 0; // calculate grow speed using 0-8 for low speeds and 9-15 for higher speeds if (advsize->grow) increment = advsize->growspeed; else if (advsize->shrink) increment = advsize->shrinkspeed; if (increment < 9) { // 8 means +1 every frame counter += increment; if (counter > 7) { counter -= 8; increment = 1; } else increment = 0; advsize->sizecounter = counter; } else { increment = (increment - 8) << 1; // 9 means +2, 10 means +4 etc. 15 means +14 } if (advsize->grow) { if (newsize < advsize->maxsize) { newsize += increment; if (newsize >= advsize->maxsize) { advsize->grow = false; // stop growing, shrink from now on if enabled newsize = advsize->maxsize; // limit if (advsize->pulsate) advsize->shrink = true; } } } else if (advsize->shrink) { if (newsize > advsize->minsize) { newsize -= increment; if (newsize <= advsize->minsize) { if (advsize->minsize == 0) return false; // particle shrunk to zero advsize->shrink = false; // disable shrinking newsize = advsize->minsize; // limit if (advsize->pulsate) advsize->grow = true; } } } advprops->size = newsize; // handle wobbling if (advsize->wobble) { advsize->asymdir += advsize->wobblespeed; // note: if need better wobblespeed control a counter is already in the struct } return true; } // calculate x and y size for asymmetrical particles (advanced size control) void ParticleSystem2D::getParticleXYsize(PSadvancedParticle *advprops, PSsizeControl *advsize, uint32_t &xsize, uint32_t &ysize) { if (advsize == nullptr) // if advsize is valid, also advanced properties pointer is valid (handled by updatePSpointers()) return; int32_t size = advprops->size; int32_t asymdir = advsize->asymdir; int32_t deviation = ((uint32_t)size * (uint32_t)advsize->asymmetry + 255) >> 8; // deviation from symmetrical size // Calculate x and y size based on deviation and direction (0 is symmetrical, 64 is x, 128 is symmetrical, 192 is y) if (asymdir < 64) { deviation = (asymdir * deviation) >> 6; } else if (asymdir < 192) { deviation = ((128 - asymdir) * deviation) >> 6; } else { deviation = ((asymdir - 255) * deviation) >> 6; } // Calculate x and y size based on deviation, limit to 255 (rendering function cannot handle larger sizes) xsize = min((size - deviation), (int32_t)255); ysize = min((size + deviation), (int32_t)255);; } // function to bounce a particle from a wall using set parameters (wallHardness and wallRoughness) void ParticleSystem2D::bounce(int8_t &incomingspeed, int8_t ¶llelspeed, int32_t &position, const uint32_t maxposition) { incomingspeed = -incomingspeed; incomingspeed = (incomingspeed * wallHardness + 128) >> 8; // reduce speed as energy is lost on non-hard surface if (position < (int32_t)particleHardRadius) position = particleHardRadius; // fast particles will never reach the edge if position is inverted, this looks better else position = maxposition - particleHardRadius; if (wallRoughness) { int32_t incomingspeed_abs = abs((int32_t)incomingspeed); int32_t totalspeed = incomingspeed_abs + abs((int32_t)parallelspeed); // transfer an amount of incomingspeed speed to parallel speed int32_t donatespeed = ((hw_random16(incomingspeed_abs << 1) - incomingspeed_abs) * (int32_t)wallRoughness) / (int32_t)255; // take random portion of + or - perpendicular speed, scaled by roughness parallelspeed = limitSpeed((int32_t)parallelspeed + donatespeed); // give the remainder of the speed to perpendicular speed donatespeed = int8_t(totalspeed - abs(parallelspeed)); // keep total speed the same incomingspeed = incomingspeed > 0 ? donatespeed : -donatespeed; } } // apply a force in x,y direction to individual particle // caller needs to provide a 8bit counter (for each particle) that holds its value between calls // force is in 3.4 fixed point notation so force=16 means apply v+1 each frame default of 8 is every other frame (gives good results) void ParticleSystem2D::applyForce(PSparticle &part, const int8_t xforce, const int8_t yforce, uint8_t &counter) { // for small forces, need to use a delay counter uint8_t xcounter = counter & 0x0F; // lower four bits uint8_t ycounter = counter >> 4; // upper four bits // velocity increase int32_t dvx = calcForce_dv(xforce, xcounter); int32_t dvy = calcForce_dv(yforce, ycounter); // save counter values back counter = xcounter & 0x0F; // write lower four bits, make sure not to write more than 4 bits counter |= (ycounter << 4) & 0xF0; // write upper four bits // apply the force to particle part.vx = limitSpeed((int32_t)part.vx + dvx); part.vy = limitSpeed((int32_t)part.vy + dvy); } // apply a force in x,y direction to individual particle using advanced particle properties void ParticleSystem2D::applyForce(const uint32_t particleindex, const int8_t xforce, const int8_t yforce) { if (advPartProps == nullptr) return; // no advanced properties available applyForce(particles[particleindex], xforce, yforce, advPartProps[particleindex].forcecounter); } // apply a force in x,y direction to all particles // force is in 3.4 fixed point notation (see above) void ParticleSystem2D::applyForce(const int8_t xforce, const int8_t yforce) { // for small forces, need to use a delay counter uint8_t tempcounter; // note: this is not the most computationally efficient way to do this, but it saves on duplicate code and is fast enough for (uint32_t i = 0; i < usedParticles; i++) { tempcounter = forcecounter; applyForce(particles[i], xforce, yforce, tempcounter); } forcecounter = tempcounter; // save value back } // apply a force in angular direction to single particle // caller needs to provide a 8bit counter that holds its value between calls (if using single particles, a counter for each particle is needed) // angle is from 0-65535 (=0-360deg) angle = 0 means in positive x-direction (i.e. to the right) // force is in 3.4 fixed point notation so force=16 means apply v+1 each frame (useful force range is +/- 127) void ParticleSystem2D::applyAngleForce(PSparticle &part, const int8_t force, const uint16_t angle, uint8_t &counter) { int8_t xforce = ((int32_t)force * cos16_t(angle)) / 32767; // force is +/- 127 int8_t yforce = ((int32_t)force * sin16_t(angle)) / 32767; // note: cannot use bit shifts as bit shifting is asymmetrical (1>>1=0 / -1>>1=-1) and this needs to be accurate! applyForce(part, xforce, yforce, counter); } void ParticleSystem2D::applyAngleForce(const uint32_t particleindex, const int8_t force, const uint16_t angle) { if (advPartProps == nullptr) return; // no advanced properties available applyAngleForce(particles[particleindex], force, angle, advPartProps[particleindex].forcecounter); } // apply a force in angular direction to all particles // angle is from 0-65535 (=0-360deg) angle = 0 means in positive x-direction (i.e. to the right) void ParticleSystem2D::applyAngleForce(const int8_t force, const uint16_t angle) { int8_t xforce = ((int32_t)force * cos16_t(angle)) / 32767; // force is +/- 127 int8_t yforce = ((int32_t)force * sin16_t(angle)) / 32767; // note: cannot use bit shifts as bit shifting is asymmetrical (1>>1=0 / -1>>1=-1) and this needs to be accurate! applyForce(xforce, yforce); } // apply gravity to all particles using PS global gforce setting // force is in 3.4 fixed point notation, see note above // note: faster than apply force since direction is always down and counter is fixed for all particles void ParticleSystem2D::applyGravity() { int32_t dv = calcForce_dv(gforce, gforcecounter); if (dv == 0) return; for (uint32_t i = 0; i < usedParticles; i++) { // Note: not checking if particle is dead is faster as most are usually alive and if few are alive, rendering is fast anyways particles[i].vy = limitSpeed((int32_t)particles[i].vy - dv); } } // apply gravity to single particle using system settings (use this for sources) // function does not increment gravity counter, if gravity setting is disabled, this cannot be used void ParticleSystem2D::applyGravity(PSparticle &part) { uint32_t counterbkp = gforcecounter; // backup PS gravity counter int32_t dv = calcForce_dv(gforce, gforcecounter); gforcecounter = counterbkp; //save it back part.vy = limitSpeed((int32_t)part.vy - dv); } // slow down particle by friction, the higher the speed, the higher the friction. a high friction coefficient slows them more (255 means instant stop) // note: a coefficient smaller than 0 will speed them up (this is a feature, not a bug), coefficient larger than 255 inverts the speed, so don't do that void ParticleSystem2D::applyFriction(PSparticle &part, const int32_t coefficient) { // note: not checking if particle is dead can be done by caller (or can be omitted) #if defined(CONFIG_IDF_TARGET_ESP32C3) || defined(ESP8266) // use bitshifts with rounding instead of division (2x faster) int32_t friction = 256 - coefficient; part.vx = ((int32_t)part.vx * friction + (((int32_t)part.vx >> 31) & 0xFF)) >> 8; // note: (v>>31) & 0xFF)) extracts the sign and adds 255 if negative for correct rounding using shifts part.vy = ((int32_t)part.vy * friction + (((int32_t)part.vy >> 31) & 0xFF)) >> 8; #else // division is faster on ESP32, S2 and S3 int32_t friction = 255 - coefficient; part.vx = ((int32_t)part.vx * friction) / 255; part.vy = ((int32_t)part.vy * friction) / 255; #endif } // apply friction to all particles // note: not checking if particle is dead is faster as most are usually alive and if few are alive, rendering is fast anyways void ParticleSystem2D::applyFriction(const int32_t coefficient) { #if defined(CONFIG_IDF_TARGET_ESP32C3) || defined(ESP8266) // use bitshifts with rounding instead of division (2x faster) int32_t friction = 256 - coefficient; for (uint32_t i = 0; i < usedParticles; i++) { particles[i].vx = ((int32_t)particles[i].vx * friction + (((int32_t)particles[i].vx >> 31) & 0xFF)) >> 8; // note: (v>>31) & 0xFF)) extracts the sign and adds 255 if negative for correct rounding using shifts particles[i].vy = ((int32_t)particles[i].vy * friction + (((int32_t)particles[i].vy >> 31) & 0xFF)) >> 8; } #else // division is faster on ESP32, S2 and S3 int32_t friction = 255 - coefficient; for (uint32_t i = 0; i < usedParticles; i++) { particles[i].vx = ((int32_t)particles[i].vx * friction) / 255; particles[i].vy = ((int32_t)particles[i].vy * friction) / 255; } #endif } // attracts a particle to an attractor particle using the inverse square-law void ParticleSystem2D::pointAttractor(const uint32_t particleindex, PSparticle &attractor, const uint8_t strength, const bool swallow) { if (advPartProps == nullptr) return; // no advanced properties available // Calculate the distance between the particle and the attractor int32_t dx = attractor.x - particles[particleindex].x; int32_t dy = attractor.y - particles[particleindex].y; // Calculate the force based on inverse square law int32_t distanceSquared = dx * dx + dy * dy; if (distanceSquared < 8192) { if (swallow) { // particle is close, age it fast so it fades out, do not attract further if (particles[particleindex].ttl > 7) particles[particleindex].ttl -= 8; else { particles[particleindex].ttl = 0; return; } } distanceSquared = 2 * PS_P_RADIUS * PS_P_RADIUS; // limit the distance to avoid very high forces } int32_t force = ((int32_t)strength << 16) / distanceSquared; int8_t xforce = (force * dx) / 1024; // scale to a lower value, found by experimenting int8_t yforce = (force * dy) / 1024; // note: cannot use bit shifts as bit shifting is asymmetrical (1>>1=0 / -1>>1=-1) and this needs to be accurate! applyForce(particleindex, xforce, yforce); } // render particles to the LED buffer (uses palette to render the 8bit particle color value) // if wrap is set, particles half out of bounds are rendered to the other side of the matrix // warning: do not render out of bounds particles or system will crash! rendering does not check if particle is out of bounds // firemode is only used for PS Fire FX void ParticleSystem2D::render() { if (framebuffer == nullptr) { PSPRINTLN(F("PS render: no framebuffer!")); return; } CRGBW baseRGB; uint32_t brightness; // particle brightness, fades if dying TBlendType blend = LINEARBLEND; // default color rendering: wrap palette if (particlesettings.colorByAge) { blend = LINEARBLEND_NOWRAP; } if (motionBlur) { // motion-blurring active for (int32_t y = 0; y <= maxYpixel; y++) { int index = y * (maxXpixel + 1); for (int32_t x = 0; x <= maxXpixel; x++) { framebuffer[index] = fast_color_scale(framebuffer[index], motionBlur); // note: could skip if only globalsmear is active but usually they are both active and scaling is fast enough index++; } } } else { // no blurring: clear buffer memset(framebuffer, 0, (maxXpixel+1) * (maxYpixel+1) * sizeof(CRGBW)); } // go over particles and render them to the buffer for (uint32_t i = 0; i < usedParticles; i++) { if (particles[i].ttl == 0 || particleFlags[i].outofbounds) continue; // generate RGB values for particle if (fireIntesity) { // fire mode brightness = (uint32_t)particles[i].ttl * (3 + (fireIntesity >> 5)) + 5; brightness = min(brightness, (uint32_t)255); baseRGB = ColorFromPaletteWLED(SEGPALETTE, brightness, 255, LINEARBLEND_NOWRAP); // map hue to brightness for fire effect } else { brightness = min((particles[i].ttl << 1), (int)255); baseRGB = ColorFromPaletteWLED(SEGPALETTE, particles[i].hue, 255, blend); if (particles[i].sat < 255) { CHSV32 baseHSV; rgb2hsv(baseRGB.color32, baseHSV); // convert to HSV baseHSV.s = min(baseHSV.s, particles[i].sat); // set the saturation but don't increase it hsv2rgb(baseHSV, baseRGB.color32); // convert back to RGB } } if (gammaCorrectCol) brightness = gamma8(brightness); // apply gamma correction, used for gamma-inverted brightness distribution renderParticle(i, brightness, baseRGB, particlesettings.wrapX, particlesettings.wrapY); } // apply 2D blur to rendered frame if (smearBlur) { SEGMENT.blur2D(smearBlur, smearBlur, true); } } // calculate pixel positions and brightness distribution and render the particle to local buffer or global buffer void WLED_O2_ATTR ParticleSystem2D::renderParticle(const uint32_t particleindex, const uint8_t brightness, const CRGBW& color, const bool wrapX, const bool wrapY) { uint32_t size = particlesize; if (perParticleSize && advPartProps != nullptr) // use advanced size properties size = 1 + advPartProps[particleindex].size; // add 1 to avoid single pixel size particles (collisions do not support it) if (size == 0) { // single pixel rendering uint32_t x = particles[particleindex].x >> PS_P_RADIUS_SHIFT; uint32_t y = particles[particleindex].y >> PS_P_RADIUS_SHIFT; if (x <= (uint32_t)maxXpixel && y <= (uint32_t)maxYpixel) { uint32_t index = x + (maxYpixel - y) * (maxXpixel + 1); // flip y coordinate (0,0 is bottom left in PS but top left in framebuffer) framebuffer[index] = fast_color_scaleAdd(framebuffer[index], color, brightness); } return; } if (size > 1) { // size > 1: render as ellipse renderLargeParticle(size, particleindex, brightness, color, wrapX, wrapY); // larger size rendering return; } // size = 1: standard 2x2 pixel rendering using bilinear interpolation (20% faster than ellipse rendering) uint8_t pxlbrightness[4]; // brightness values for the four pixels representing a particle struct { int32_t x,y; } pixco[4]; // particle pixel coordinates, the order is bottom left [0], bottom right[1], top right [2], top left [3] (thx @blazoncek for improved readability struct) bool pixelvalid[4] = {true, true, true, true}; // is set to false if pixel is out of bounds // add half a radius as the rendering algorithm always starts at the bottom left, this leaves things positive, so shifts can be used, then shift coordinate by a full pixel (x--/y-- below) // if sub-pixel position is 0-PS_P_HALFRADIUS it will render to x>>PS_P_RADIUS_SHIFT as the right pixel int32_t xoffset = particles[particleindex].x + PS_P_HALFRADIUS; int32_t yoffset = particles[particleindex].y + PS_P_HALFRADIUS; int32_t dx = xoffset & (PS_P_RADIUS - 1); // relativ particle position in subpixel space int32_t dy = yoffset & (PS_P_RADIUS - 1); // modulo replaced with bitwise AND, as radius is always a power of 2 int32_t x = (xoffset >> PS_P_RADIUS_SHIFT); // divide by PS_P_RADIUS which is 64, so can bitshift (compiler can not optimize integer) int32_t y = (yoffset >> PS_P_RADIUS_SHIFT); // set the four raw pixel coordinates pixco[1].x = pixco[2].x = x; // bottom right & top right pixco[2].y = pixco[3].y = y; // top right & top left x--; // shift by a full pixel here, this is skipped above to not do -1 and then +1 y--; pixco[0].x = pixco[3].x = x; // bottom left & top left pixco[0].y = pixco[1].y = y; // bottom left & bottom right // calculate brightness values for all four pixels representing a particle using linear interpolation // could check for out of frame pixels here but calculating them is faster (very few are out) // precalculate values for speed optimization. Note: rounding is not perfect but close enough, some inaccuracy is traded for speed int32_t precal1 = (int32_t)PS_P_RADIUS - dx; int32_t precal2 = ((int32_t)PS_P_RADIUS - dy) * brightness; int32_t precal3 = dy * brightness; pxlbrightness[0] = (precal1 * precal2) >> PS_P_SURFACE; // bottom left value equal to ((PS_P_RADIUS - dx) * (PS_P_RADIUS-dy) * brightness) >> PS_P_SURFACE pxlbrightness[1] = (dx * precal2) >> PS_P_SURFACE; // bottom right value equal to (dx * (PS_P_RADIUS-dy) * brightness) >> PS_P_SURFACE pxlbrightness[2] = (dx * precal3) >> PS_P_SURFACE; // top right value equal to (dx * dy * brightness) >> PS_P_SURFACE pxlbrightness[3] = (precal1 * precal3) >> PS_P_SURFACE; // top left value equal to ((PS_P_RADIUS-dx) * dy * brightness) >> PS_P_SURFACE // adjust brightness such that distribution is linear after gamma correction: // - scale brigthness with gamma correction (done in render()) // - apply inverse gamma correction to brightness values // - gamma is applied again in show() -> the resulting brightness distribution is linear but gamma corrected in total if (gammaCorrectCol) { for (uint32_t i = 0; i < 4; i++) { pxlbrightness[i] = gamma8inv(pxlbrightness[i]); // use look-up-table for invers gamma } } // standard rendering (2x2 pixels) // check for out of frame pixels and wrap them if required: x,y is bottom left pixel coordinate of the particle if (pixco[0].x < 0) { // left pixels out of frame if (wrapX) { // wrap x to the other side if required pixco[0].x = pixco[3].x = maxXpixel; } else { pixelvalid[0] = pixelvalid[3] = false; // out of bounds if (pixco[0].x < -1) return; // both left pixels out of bounds, no need to continue (safety check) } } else if (pixco[1].x > (int32_t)maxXpixel) { // right pixels, only has to be checked if left pixel is in frame if (wrapX) { // wrap y to the other side if required pixco[1].x = pixco[2].x = 0; } else { pixelvalid[1] = pixelvalid[2] = false; // out of bounds if (pixco[0].x > (int32_t)maxXpixel) return; // both pixels out of bounds, no need to continue (safety check) } } if (pixco[0].y < 0) { // bottom pixels out of frame if (wrapY) { // wrap y to the other side if required pixco[0].y = pixco[1].y = maxYpixel; } else { pixelvalid[0] = pixelvalid[1] = false; // out of bounds if (pixco[0].y < -1) return; // both bottom pixels out of bounds, no need to continue (safety check) } } else if (pixco[2].y > maxYpixel) { // top pixels if (wrapY) { // wrap y to the other side if required pixco[2].y = pixco[3].y = 0; } else { pixelvalid[2] = pixelvalid[3] = false; // out of bounds if (pixco[2].y > (int32_t)maxYpixel + 1) return; // both top pixels out of bounds, no need to continue (safety check) } } for (uint32_t i = 0; i < 4; i++) { if (pixelvalid[i]) { uint32_t idx = pixco[i].x + (maxYpixel - pixco[i].y) * (maxXpixel + 1); // flip y coordinate (0,0 is bottom left in PS but top left in framebuffer) framebuffer[idx] = fast_color_scaleAdd(framebuffer[idx], color, pxlbrightness[i]); // order is: bottom left, bottom right, top right, top left } } } // render particle as ellipse/circle with linear brightness falloff and sub-pixel precision void WLED_O2_ATTR ParticleSystem2D::renderLargeParticle(const uint32_t size, const uint32_t particleindex, const uint8_t brightness, const CRGBW& color, const bool wrapX, const bool wrapY) { // particle position with sub-pixel precision int32_t x_subcenter = particles[particleindex].x; int32_t y_subcenter = particles[particleindex].y; // example: for x = 128, a paticle is exacly between pixel 1 and 2, with a radius of 2 pixels, we draw pixels 0-3 // integer center jumps when x = 127 -> pixel 1 goes to x = 128 -> pixel 2 // when calculating the dx, we need to take this into account: at x = 128 the x offset is 1, the pixel center is at pixel 2: // for pixel 1, dx = 1 * PS_P_RADIUS - 128 = -64 but the center of the pixel is actually only -32 from the particle center so need to add half a radius: // dx = pixel_x * PS_P_RADIUS - x_subcenter + PS_P_HALFRADIUS // sub-pixel offset (0-63) int32_t x_offset = x_subcenter & (PS_P_RADIUS - 1); // same as modulo PS_P_RADIUS but faster int32_t y_offset = y_subcenter & (PS_P_RADIUS - 1); // integer pixel position, this is rounded down int32_t x_center = (x_subcenter) >> PS_P_RADIUS_SHIFT; int32_t y_center = (y_subcenter) >> PS_P_RADIUS_SHIFT; // ellipse radii in pixels uint32_t xsize = size; uint32_t ysize = size; if (advPartSize != nullptr && advPartSize[particleindex].asymmetry > 0) { getParticleXYsize(&advPartProps[particleindex], &advPartSize[particleindex], xsize, ysize); } int32_t rx_subpixel = xsize + PS_P_RADIUS + 1; // size = 1 means radius of just over 1 pixel, + PS_P_RADIUS (+1 to accoutn for bit-shift loss) int32_t ry_subpixel = ysize + PS_P_RADIUS + 1; // size = 255 is radius of 5, so add 65 -> 65+255=320, 320>>6=5 pixels // rendering bounding box in pixels int32_t rx_pixels = (rx_subpixel >> PS_P_RADIUS_SHIFT); int32_t ry_pixels = (ry_subpixel >> PS_P_RADIUS_SHIFT); int32_t x_min = x_center - rx_pixels; // note: the "+1" extension needed for 1D is not required for 2D, it is smooth as-is int32_t x_max = x_center + rx_pixels; int32_t y_min = y_center - ry_pixels; int32_t y_max = y_center + ry_pixels; // cache for speed uint32_t matrixX = maxXpixel + 1; uint32_t matrixY = maxYpixel + 1; uint32_t rx_sq = rx_subpixel * rx_subpixel; uint32_t ry_sq = ry_subpixel * ry_subpixel; // iterate over bounding box and render each pixel for (int32_t py = y_min; py <= y_max; py++) { for (int32_t px = x_min; px <= x_max; px++) { // Check bounds and apply wrapping int32_t render_x = px; int32_t render_y = py; if (render_x < 0) { if (!wrapX) continue; render_x += matrixX; } else if (render_x > maxXpixel) { if (!wrapX) continue; render_x -= matrixX; } if (render_y < 0) { if (!wrapY) continue; render_y += matrixY; } else if (render_y > maxYpixel) { if (!wrapY) continue; render_y -= matrixY; } // distance from particle center, explanation see above int32_t dx_subpixel = (px << PS_P_RADIUS_SHIFT) - x_subcenter + PS_P_HALFRADIUS; int32_t dy_subpixel = (py << PS_P_RADIUS_SHIFT) - y_subcenter + PS_P_HALFRADIUS; // calculate brightness based on squared distance to ellipse center uint8_t pixel_brightness = calculateEllipseBrightness(dx_subpixel, dy_subpixel, rx_sq, ry_sq, brightness); if (pixel_brightness == 0) continue; // skip black pixels // apply inverse gamma correction if needed, if this is skipped, particles flicker due to changing total brightness if (gammaCorrectCol) { pixel_brightness = gamma8inv(pixel_brightness); // invert brigthess so brightness distribution is linear after gamma correction } // Render pixel uint32_t idx = render_x + (maxYpixel - render_y) * matrixX; // flip y coordinate (0,0 is bottom left in PS but top left in framebuffer) framebuffer[idx] = fast_color_scaleAdd(framebuffer[idx], color, pixel_brightness); } } } // detect collisions in an array of particles and handle them // uses binning by dividing the frame into slices in x direction which is efficient if using gravity in y direction (but less efficient for FX that use forces in x direction) // for code simplicity, no y slicing is done, making very tall matrix configurations less efficient // note: also tested adding y slicing, it gives diminishing returns, some FX even get slower. FX not using gravity would benefit with a 10% FPS improvement void ParticleSystem2D::handleCollisions() { uint32_t collDistSq = particleHardRadius << 1; // distance is double the radius note: particleHardRadius is updated when setting global particle size collDistSq = collDistSq * collDistSq; // square it for faster comparison (square is one operation) // note: partices are binned in x-axis, assumption is that no more than half of the particles are in the same bin // if they are, collisionStartIdx is increased so each particle collides at least every second frame (which still gives decent collisions) int binWidth = 6 * PS_P_RADIUS; // width of a bin in sub-pixels int32_t overlap = particleHardRadius << 1; // overlap bins to include edge particles to neighbouring bins if (perParticleSize && advPartProps != nullptr) overlap = 512; // max overlap for collision detection if using per-particle size, enough to catch all particles even at max speed uint32_t maxBinParticles = max((uint32_t)50, (usedParticles + 1) / 2); // assume no more than half of the particles are in the same bin, do not bin small amounts of particles uint32_t numBins = (maxX + (binWidth - 1)) / binWidth; // number of bins in x direction if (usedParticles < maxBinParticles) { numBins = 1; // use single bin for small number of particles binWidth = maxX + 1; } uint16_t binIndices[maxBinParticles]; // creat array on stack for indices, 2kB max for 1024 particles (ESP32_MAXPARTICLES/2) uint32_t binParticleCount; // number of particles in the current bin uint32_t nextFrameStartIdx = hw_random16(usedParticles); // index of the first particle in the next frame (set to fixed value if bin overflow) uint32_t pidx = collisionStartIdx; //start index in case a bin is full, process remaining particles next frame // fill the binIndices array for this bin for (uint32_t bin = 0; bin < numBins; bin++) { binParticleCount = 0; // reset for this bin int32_t binStart = bin * binWidth - overlap; // note: first bin will extend to negative, but that is ok as out of bounds particles are ignored int32_t binEnd = binStart + binWidth + (overlap << 1); // add twice the overlap as start is start-overlap, note: last bin can be out of bounds, see above; // fill the binIndices array for this bin for (uint32_t i = 0; i < usedParticles; i++) { if (particles[pidx].ttl > 0) { // is alive if (particles[pidx].x >= binStart && particles[pidx].x <= binEnd) { // >= and <= to include particles on the edge of the bin (overlap to ensure boarder particles collide with adjacent bins) if (particleFlags[pidx].outofbounds == 0 && particleFlags[pidx].collide) { // particle is in frame and does collide note: checking flags is quite slow and usually these are set, so faster to check here if (binParticleCount >= maxBinParticles) { // bin is full, more particles in this bin so do the rest next frame nextFrameStartIdx = pidx; // bin overflow can only happen once as bin size is at least half of the particles (or half +1) break; } binIndices[binParticleCount++] = pidx; } } } pidx++; if (pidx >= usedParticles) pidx = 0; // wrap around } int32_t massratio1 = 0; // 0 means dont use mass ratio (equal mass) int32_t massratio2 = 0; // TODO: if implementing "fixed" particles, set to 1 (fixed) and 255 (movable) for (uint32_t i = 0; i < binParticleCount; i++) { // go though all 'higher number' particles in this bin and see if any of those are in close proximity and if they are, make them collide uint32_t idx_i = binIndices[i]; for (uint32_t j = i + 1; j < binParticleCount; j++) { // check against higher number particles uint32_t idx_j = binIndices[j]; if (perParticleSize && advPartProps != nullptr) { // using individual particle size collDistSq = (PS_P_MINHARDRADIUS << 1) + ((((uint32_t)advPartProps[idx_i].size + (uint32_t)advPartProps[idx_j].size) * 52) >> 6); // collision distance, use 80% of size for tighter stacking (slight overlap) collDistSq = collDistSq * collDistSq; // square it for faster comparison // calculate mass ratio for collision response uint32_t mass1 = PS_P_RADIUS + advPartProps[idx_i].size; uint32_t mass2 = PS_P_RADIUS + advPartProps[idx_j].size; mass1 = mass1 * mass1; // mass proportional to area mass2 = mass2 * mass2; uint32_t totalmass = mass1 + mass2; massratio1 = (mass2 << 8) / totalmass; // massratio 1 depends on mass of particle 2, i.e. if 2 is heavier -> higher velocity impact on 1 massratio2 = (mass1 << 8) / totalmass; } // note: using the same logic as in 1D is much slower though it would be more accurate but it is not really needed in 2D: particles slipping through each other is much less visible int32_t dx = (particles[idx_j].x + particles[idx_j].vx) - (particles[idx_i].x + particles[idx_i].vx); // distance with lookahead if (dx * dx < collDistSq) { // check x direction, if close, check y direction (squaring is faster than abs() or dual compare) int32_t dy = (particles[idx_j].y + particles[idx_j].vy) - (particles[idx_i].y + particles[idx_i].vy); // distance with lookahead if (dy * dy < collDistSq) // particles are close collideParticles(particles[idx_i], particles[idx_j], dx, dy, collDistSq, massratio1, massratio2); } } } } collisionStartIdx = nextFrameStartIdx; // set the start index for the next frame } // handle a collision if close proximity is detected, i.e. dx and/or dy smaller than 2*PS_P_RADIUS // takes two pointers to the particles to collide and the particle hardness (softer means more energy lost in collision, 255 means full hard) void WLED_O2_ATTR ParticleSystem2D::collideParticles(PSparticle &particle1, PSparticle &particle2, int32_t dx, int32_t dy, const uint32_t collDistSq, int32_t massratio1, int32_t massratio2) { int32_t distanceSquared = dx * dx + dy * dy; // Calculate relative velocity note: could zero check but that does not improve overall speed but deminish it as that is rarely the case and pushing is still required int32_t relativeVx = (int32_t)particle2.vx - (int32_t)particle1.vx; int32_t relativeVy = (int32_t)particle2.vy - (int32_t)particle1.vy; // if dx and dy are zero (i.e. same position) give them an offset, if speeds are also zero, also offset them (pushes particles apart if they are clumped before enabling collisions) if (distanceSquared == 0) { // Adjust positions based on relative velocity direction dx = -1; if (relativeVx < 0) // if true, particle2 is on the right side dx = 1; else if (relativeVx == 0) relativeVx = 1; dy = -1; if (relativeVy < 0) dy = 1; else if (relativeVy == 0) relativeVy = 1; distanceSquared = 2; // 1 + 1 } // Calculate dot product of relative velocity and relative distance int32_t dotProduct = (dx * relativeVx + dy * relativeVy); // is always negative if moving towards each other if (dotProduct < 0) {// particles are moving towards each other // integer math is much faster than using floats (float divisions are slow on all ESPs) // overflow check: dx/dy are 7bit, relativV are 8bit -> dotproduct is 15bit, dotproduct/distsquared ist 8b, multiplied by collisionhardness of 8bit. so a 16bit shift is ok, make it 15 to be sure no overflows happen // note: cannot use right shifts as bit shifting in right direction is asymmetrical (1>>1=0 / -1>>1=-1) and this needs to be accurate! the trick is: only shift positive numers // Calculate new velocities after collision int32_t surfacehardness = max(collisionHardness, (int32_t)PS_P_MINSURFACEHARDNESS >> 1); // if particles are soft, the impulse must stay above a limit or collisions slip through at higher speeds, 170 seems to be a good value int32_t impulse = (((((-dotProduct) << 15) / distanceSquared) * surfacehardness) >> 8); // note: inverting before bitshift corrects for asymmetry in right-shifts (is slightly faster) #if defined(CONFIG_IDF_TARGET_ESP32C3) || defined(ESP8266) // use bitshifts with rounding instead of division (2x faster) int32_t ximpulse = (impulse * dx + ((dx >> 31) & 0x7FFF)) >> 15; // note: extracting sign bit and adding rounding value to correct for asymmetry in right shifts int32_t yimpulse = (impulse * dy + ((dy >> 31) & 0x7FFF)) >> 15; #else int32_t ximpulse = (impulse * dx) / 32767; int32_t yimpulse = (impulse * dy) / 32767; #endif // if particles are not the same size, use a mass ratio. mass ratio is set to 0 if particles are the same size if (massratio1) { int32_t vx1 = (int32_t)particle1.vx - ((ximpulse * massratio1) >> 7); // mass ratio is in fixed point 8bit, multiply by two to account for the fact that we distribute the impulse to both particles int32_t vy1 = (int32_t)particle1.vy - ((yimpulse * massratio1) >> 7); int32_t vx2 = (int32_t)particle2.vx + ((ximpulse * massratio2) >> 7); int32_t vy2 = (int32_t)particle2.vy + ((yimpulse * massratio2) >> 7); // limit speeds to max speed (required if a lot of impulse is transferred from a large to a small particle) particle1.vx = limitSpeed(vx1); particle1.vy = limitSpeed(vy1); particle2.vx = limitSpeed(vx2); particle2.vy = limitSpeed(vy2); } else { particle1.vx -= ximpulse; // note: impulse is inverted, so subtracting it particle1.vy -= yimpulse; particle2.vx += ximpulse; particle2.vy += yimpulse; } if (collisionHardness < PS_P_MINSURFACEHARDNESS && (SEGMENT.call & 0x07) == 0) { // if particles are soft, they become 'sticky' i.e. apply some friction (they do pile more nicely and stop sloshing around) const uint32_t coeff = collisionHardness + (255 - PS_P_MINSURFACEHARDNESS); // Note: could call applyFriction, but this is faster and speed is key here #if defined(CONFIG_IDF_TARGET_ESP32C3) || defined(ESP8266) // use bitshifts with rounding instead of division (2x faster) particle1.vx = ((int32_t)particle1.vx * coeff + (((int32_t)particle1.vx >> 31) & 0xFF)) >> 8; // note: (v>>31) & 0xFF)) extracts the sign and adds 255 if negative for correct rounding using shifts particle1.vy = ((int32_t)particle1.vy * coeff + (((int32_t)particle1.vy >> 31) & 0xFF)) >> 8; particle2.vx = ((int32_t)particle2.vx * coeff + (((int32_t)particle2.vx >> 31) & 0xFF)) >> 8; particle2.vy = ((int32_t)particle2.vy * coeff + (((int32_t)particle2.vy >> 31) & 0xFF)) >> 8; #else // division is faster on ESP32, S2 and S3 particle1.vx = ((int32_t)particle1.vx * coeff) / 255; particle1.vy = ((int32_t)particle1.vy * coeff) / 255; particle2.vx = ((int32_t)particle2.vx * coeff) / 255; particle2.vy = ((int32_t)particle2.vy * coeff) / 255; #endif } } // particles have volume, push particles apart if they are too close // tried lots of configurations, what works best is to give one particle a little velocity. When adding hard pushing things tend to oscillate. // when hard pushing by offsetting position without velocity, they tend to sink into each other under gravity. // when using hard-pushing and velocity, there are some oscillations and softer particles do not pile nicely. // oscillation get worse if pushing both particles so one is chosen somewhat randomly. // softer collisions are not perfect on purpose: soft particles should pile up and overlap slightly, if separation is made perfect, it does not have the intended look if (distanceSquared < collDistSq && (relativeVx*relativeVx + relativeVy*relativeVy < 50)) { // too close and also slow, push them apart bool fairlyrandom = dotProduct & 0x01; //dotprouct LSB should be somewhat random, so no need to calculate a random number int32_t pushamount = 1 + ((collDistSq - distanceSquared) >> 13); // found this by experimentation: it means push by 1, push more if overlapping more than 1.4 physical pixels (i.e. larger particles only) int8_t pushx = dx > 0 ? -pushamount : pushamount; // particle 1 is on the left int8_t pushy = dy > 0 ? -pushamount : pushamount; // particle 1 is below particle 2 // if they are very soft, stop slow particles completely to make them stick to each other if (collisionHardness < 5) { if (fairlyrandom) { // do not stop them every frame to avoid groups of particles hanging mid-air particle1.vx = 0; particle1.vy = 0; particle2.vx = 0; particle2.vy = 0; // hard-push particle 1 only: if both are pushed, this oscillates ever so slightly particle1.x += pushx; particle1.y += pushy; } } else { if (fairlyrandom) { particle1.vx += pushx; //particle1.x += pushx; particle1.vy += pushy; //particle1.y += pushy; } else { particle2.vx -= pushx; //particle2.x -= pushx; particle2.vy -= pushy; //particle2.y -= pushy; } } } } // update size and pointers (memory location and size can change dynamically) // note: do not access the PS class in FX befor running this function (or it messes up SEGENV.data) void ParticleSystem2D::updateSystem(void) { //PSPRINTLN("updateSystem2D"); setMatrixSize(SEGMENT.vWidth(), SEGMENT.vHeight()); updatePSpointers(advPartProps != nullptr, advPartSize != nullptr); // update pointers to PS data, also updates availableParticles //PSPRINTLN("\n END update System2D, running FX..."); } // set the pointers for the class (this only has to be done once and not on every FX call, only the class pointer needs to be reassigned to SEGENV.data every time) // function returns the pointer to the next byte available for the FX (if it assigned more memory for other stuff using the above allocate function) // FX handles the PSsources, need to tell this function how many there are void ParticleSystem2D::updatePSpointers(bool isadvanced, bool sizecontrol) { //PSPRINTLN("updatePSpointers"); // Note on memory alignment: // a pointer MUST be 4 byte aligned. sizeof() in a struct/class is always aligned to the largest element. if it contains a 32bit, it will be padded to 4 bytes, 16bit is padded to 2byte alignment. // The PS is aligned to 4 bytes, a PSparticle is aligned to 2 and a struct containing only byte sized variables is not aligned at all and may need to be padded when dividing the memoryblock. // by making sure that the number of sources and particles is a multiple of 4, padding can be skipped here as alignent is ensured, independent of struct sizes. particles = reinterpret_cast(this + 1); // pointer to particles particleFlags = reinterpret_cast(particles + numParticles); // pointer to particle flags sources = reinterpret_cast(particleFlags + numParticles); // pointer to source(s) at data+sizeof(ParticleSystem2D) framebuffer = SEGMENT.getPixels(); // pointer to framebuffer PSdataEnd = reinterpret_cast(sources + numSources); // pointer to first available byte after the PS for FX additional data (already aligned to 4 byte boundary) if (isadvanced) { advPartProps = reinterpret_cast(PSdataEnd); PSdataEnd = reinterpret_cast(advPartProps + numParticles); if (sizecontrol) { advPartSize = reinterpret_cast(PSdataEnd); PSdataEnd = reinterpret_cast(advPartSize + numParticles); } } #ifdef DEBUG_PS Serial.printf_P(PSTR(" particles %p "), particles); Serial.printf_P(PSTR(" sources %p "), sources); Serial.printf_P(PSTR(" adv. props %p "), advPartProps); Serial.printf_P(PSTR(" adv. ctrl %p "), advPartSize); Serial.printf_P(PSTR("end %p\n"), PSdataEnd); #endif } //non class functions to use for initialization uint32_t calculateNumberOfParticles2D(uint32_t const pixels, const bool isadvanced, const bool sizecontrol) { uint32_t numberofParticles = pixels; // 1 particle per pixel (for example 512 particles on 32x16) uint32_t particlelimit = MAXPARTICLES_2D; // maximum number of paticles allowed numberofParticles = max((uint32_t)4, min(numberofParticles, particlelimit)); // limit to 4 - particlelimit if (isadvanced) // advanced property array needs ram, reduce number of particles to use the same amount numberofParticles = (numberofParticles * sizeof(PSparticle)) / (sizeof(PSparticle) + sizeof(PSadvancedParticle)); if (sizecontrol) // advanced property array needs ram, reduce number of particles numberofParticles /= 8; // if advanced size control is used, much fewer particles are needed note: if changing this number, adjust FX using this accordingly //make sure it is a multiple of 4 for proper memory alignment (easier than using padding bytes) numberofParticles = (numberofParticles+3) & ~0x03; return numberofParticles; } uint32_t calculateNumberOfSources2D(uint32_t pixels, uint32_t requestedsources) { int numberofSources = min((pixels) / SOURCEREDUCTIONFACTOR, (uint32_t)requestedsources); numberofSources = max(1, min(numberofSources, MAXSOURCES_2D)); // limit // make sure it is a multiple of 4 for proper memory alignment numberofSources = (numberofSources+3) & ~0x03; return numberofSources; } //allocate memory for particle system class, particles, sprays plus additional memory requested by FX //TODO: add percentofparticles like in 1D to reduce memory footprint of some FX? bool allocateParticleSystemMemory2D(uint32_t numparticles, uint32_t numsources, bool isadvanced, bool sizecontrol, uint32_t additionalbytes) { PSPRINTLN("PS 2D alloc"); PSPRINTLN("numparticles:" + String(numparticles) + " numsources:" + String(numsources) + " additionalbytes:" + String(additionalbytes)); uint32_t requiredmemory = sizeof(ParticleSystem2D); // functions above make sure numparticles is a multiple of 4 bytes (to avoid alignment issues) requiredmemory += sizeof(PSparticleFlags) * numparticles; requiredmemory += sizeof(PSparticle) * numparticles; if (isadvanced) requiredmemory += sizeof(PSadvancedParticle) * numparticles; if (sizecontrol) requiredmemory += sizeof(PSsizeControl) * numparticles; requiredmemory += sizeof(PSsource) * numsources; requiredmemory += additionalbytes; return(SEGMENT.allocateData(requiredmemory)); } // initialize Particle System, allocate additional bytes if needed (pointer to those bytes can be read from particle system class: PSdataEnd) bool initParticleSystem2D(ParticleSystem2D *&PartSys, uint32_t requestedsources, uint32_t additionalbytes, bool advanced, bool sizecontrol) { PSPRINT("PS 2D init "); if (!strip.isMatrix) { errorFlag = ERR_NOT_IMPL; // TODO: need a better error code if more codes are added SEGMENT.deallocateData(); // deallocate any data to make sure data is null (there is no valid PS in data and data can only be checked for null) return false; // only for 2D } uint32_t cols = SEGMENT.virtualWidth(); uint32_t rows = SEGMENT.virtualHeight(); uint32_t pixels = cols * rows; if (sizecontrol) advanced = true; // size control needs advanced properties, prevent wrong usage uint32_t numparticles = calculateNumberOfParticles2D(pixels, advanced, sizecontrol); PSPRINT(" segmentsize:" + String(cols) + " x " + String(rows)); PSPRINTLN(" request numparticles:" + String(numparticles)); uint32_t numsources = calculateNumberOfSources2D(pixels, requestedsources); bool allocsuccess = false; while(numparticles >= 5) { // make sure we have at least 5 particles or quit if (allocateParticleSystemMemory2D(numparticles, numsources, advanced, sizecontrol, additionalbytes)) { PSPRINTLN(F("PS 2D alloc succeeded")); allocsuccess = true; break; // allocation succeeded } numparticles = ((numparticles / 2) + 3) & ~0x03; // cut number of particles in half and try again, must be 4 byte aligned PSPRINTLN(F("PS 2D alloc failed, trying with less particles...")); } if (!allocsuccess) { PSPRINTLN(F("PS 2D alloc failed, not enough memory!")); return false; // allocation failed } PartSys = new (SEGENV.data) ParticleSystem2D(cols, rows, numparticles, numsources, advanced, sizecontrol); // particle system constructor PSPRINTLN(F("2D PS init done")); return true; } #endif // WLED_DISABLE_PARTICLESYSTEM2D //////////////////////// // 1D Particle System // //////////////////////// #ifndef WLED_DISABLE_PARTICLESYSTEM1D ParticleSystem1D::ParticleSystem1D(uint32_t length, uint32_t numberofparticles, uint32_t numberofsources, bool isadvanced) { numSources = numberofsources; numParticles = numberofparticles; // number of particles allocated in init usedParticles = numParticles; // use all particles by default advPartProps = nullptr; //make sure we start out with null pointers (just in case memory was not cleared) //advPartSize = nullptr; setSize(length); updatePSpointers(isadvanced); // set the particle and sources pointer (call this before accessing sprays or particles) setWallHardness(255); // set default wall hardness to max setGravity(0); //gravity disabled by default setParticleSize(0); // 1 pixel size by default motionBlur = 0; //no fading by default smearBlur = 0; //no smearing by default emitIndex = 0; collisionStartIdx = 0; // initialize some default non-zero values most FX use for (uint32_t i = 0; i < numSources; i++) { sources[i].source.ttl = 1; //set source alive sources[i].sourceFlags.asByte = 0; // all flags disabled } perParticleSize = isadvanced; // enable per particle size by default so FX do not need to set this explicitly. FX can disable by setting global size. if (isadvanced) { for (uint32_t i = 0; i < numParticles; i++) { advPartProps[i].sat = 255; // set full saturation } } } // update function applies gravity, moves the particles, handles collisions and renders the particles void ParticleSystem1D::update(void) { //apply gravity globally if enabled if (particlesettings.useGravity) //note: in 1D system, applying gravity after collisions also works but may be worse applyGravity(); // handle collisions (can push particles, must be done before updating particles or they can render out of bounds, causing a crash if using local buffer for speed) if (particlesettings.useCollisions) { handleCollisions(); if (perParticleSize) handleCollisions(); // second pass for per particle size (as impulse transfer can recoil at high speed, this improves "slip through" issues for small particles but is expensive) } //move all particles for (uint32_t i = 0; i < usedParticles; i++) { particleMoveUpdate(particles[i], particleFlags[i], nullptr, advPartProps ? &advPartProps[i] : nullptr); } if (particlesettings.colorByPosition) { uint32_t scale = (255 << 16) / maxX; for (uint32_t i = 0; i < usedParticles; i++) { particles[i].hue = (scale * particles[i].x) >> 16; // note: x is > 0 if not out of bounds } } render(); } // set percentage of used particles as uint8_t i.e 127 means 50% for example void ParticleSystem1D::setUsedParticles(const uint8_t percentage) { usedParticles = max((uint32_t)1, (numParticles * ((int)percentage+1)) >> 8); // number of particles to use (percentage is 0-255, 255 = 100%) PSPRINT(" SetUsedpaticles: allocated particles: "); PSPRINT(numParticles); PSPRINT(" ,used particles: "); PSPRINTLN(usedParticles); } void ParticleSystem1D::setWallHardness(const uint8_t hardness) { wallHardness = hardness; } void ParticleSystem1D::setSize(const uint32_t x) { maxXpixel = x - 1; // last physical pixel that can be drawn to maxX = x * PS_P_RADIUS_1D - 1; // particle system boundary for movements } void ParticleSystem1D::setWrap(const bool enable) { particlesettings.wrap = enable; } void ParticleSystem1D::setBounce(const bool enable) { particlesettings.bounce = enable; } void ParticleSystem1D::setKillOutOfBounds(const bool enable) { particlesettings.killoutofbounds = enable; } void ParticleSystem1D::setColorByAge(const bool enable) { particlesettings.colorByAge = enable; } void ParticleSystem1D::setColorByPosition(const bool enable) { particlesettings.colorByPosition = enable; } void ParticleSystem1D::setMotionBlur(const uint8_t bluramount) { motionBlur = bluramount; } void ParticleSystem1D::setSmearBlur(const uint8_t bluramount) { smearBlur = bluramount; } // render size, 0 = 1 pixel, 1 = 2 pixel (interpolated), 255 = 18 pixel diameter void ParticleSystem1D::setParticleSize(const uint8_t size) { particlesize = size; particleHardRadius = PS_P_MINHARDRADIUS_1D; // ~1 pixel perParticleSize = false; // disable per particle size control if global size is set if (particlesize > 1) { particleHardRadius = PS_P_MINHARDRADIUS_1D + ((particlesize * 52) >> 6); // use 1 pixel + 80% of size for hard radius (slight overlap with boarders so they do not "float" and nicer stacking) } else if (particlesize == 0) particleHardRadius = PS_P_MINHARDRADIUS_1D >> 1; // single pixel particles have half the radius (i.e. 1/2 pixel) } // enable/disable gravity, optionally, set the force (force=8 is default) can be -127 to +127, 0 is disable // if enabled, gravity is applied to all particles in ParticleSystemUpdate() // force is in 3.4 fixed point notation so force=16 means apply v+1 each frame default of 8 is every other frame (gives good results) void ParticleSystem1D::setGravity(const int8_t force) { if (force) { gforce = force; particlesettings.useGravity = true; } else particlesettings.useGravity = false; } void ParticleSystem1D::enableParticleCollisions(const bool enable, const uint8_t hardness) { particlesettings.useCollisions = enable; collisionHardness = hardness; } // emit one particle with variation, returns index of last emitted particle (or -1 if no particle emitted) int32_t ParticleSystem1D::sprayEmit(const PSsource1D &emitter) { for (uint32_t i = 0; i < usedParticles; i++) { emitIndex++; if (emitIndex >= usedParticles) emitIndex = 0; if (particles[emitIndex].ttl == 0) { // find a dead particle particles[emitIndex].vx = emitter.v + hw_random16(emitter.var << 1) - emitter.var; // random(-var,var) particles[emitIndex].x = emitter.source.x; particles[emitIndex].hue = emitter.source.hue; particles[emitIndex].ttl = hw_random16(emitter.minLife, emitter.maxLife); particleFlags[emitIndex].collide = emitter.sourceFlags.collide; // TODO: could just set all flags (asByte) but need to check if that breaks any of the FX particleFlags[emitIndex].reversegrav = emitter.sourceFlags.reversegrav; particleFlags[emitIndex].perpetual = emitter.sourceFlags.perpetual; if (advPartProps) { advPartProps[emitIndex].sat = emitter.sat; advPartProps[emitIndex].size = emitter.size; } return emitIndex; } } return -1; } // particle moves, decays and dies, if killoutofbounds is set, out of bounds particles are set to ttl=0 // uses passed settings to set bounce or wrap, if useGravity is set, it will never bounce at the top and killoutofbounds is not applied over the top void ParticleSystem1D::particleMoveUpdate(PSparticle1D &part, PSparticleFlags1D &partFlags, PSsettings1D *options, PSadvancedParticle1D *advancedproperties) { if (options == nullptr) options = &particlesettings; // use PS system settings by default if (part.ttl > 0) { if (!partFlags.perpetual) part.ttl--; // age if (options->colorByAge) part.hue = min(part.ttl, (uint16_t)255); // set color to ttl int32_t renderradius = PS_P_HALFRADIUS_1D - 1 + particlesize; // used to check out of bounds, default for 2 pixel rendering int32_t newX = part.x + (int32_t)part.vx; partFlags.outofbounds = false; // reset out of bounds (in case particle was created outside the matrix and is now moving into view) if (perParticleSize && advancedproperties != nullptr) { // using individual particle size? renderradius = PS_P_HALFRADIUS_1D - 1 + advancedproperties->size; // note: for single pixel particles, it should be zero, but it does not matter as out of bounds checking is done in rendering function if (advancedproperties->size > 1) particleHardRadius = PS_P_MINHARDRADIUS_1D + ((advancedproperties->size * 52) >> 6); // use 1 pixel + 80% of size for hard radius (slight overlap with boarders so they do not "float" and nicer stacking) else // single pixel particles use half the collision distance for walls particleHardRadius = PS_P_MINHARDRADIUS_1D >> 1; } // if wall collisions are enabled, bounce them before they reach the edge, it looks much nicer if the particle is not half out of view if (options->bounce) { if ((newX < (int32_t)particleHardRadius) || ((newX > (int32_t)(maxX - particleHardRadius)))) { // reached a wall bool bouncethis = true; if (options->useGravity) { if (partFlags.reversegrav) { // skip bouncing at x = 0 if (newX < (int32_t)particleHardRadius) bouncethis = false; } else if (newX > (int32_t)particleHardRadius) { // skip bouncing at x = max bouncethis = false; } } if (bouncethis) { part.vx = -part.vx; // invert speed part.vx = ((int32_t)part.vx * (int32_t)wallHardness) / 255; // reduce speed as energy is lost on non-hard surface if (newX < (int32_t)particleHardRadius) newX = particleHardRadius; // fast particles will never reach the edge if position is inverted, this looks better else newX = maxX - particleHardRadius; } } } if (!checkBoundsAndWrap(newX, maxX, renderradius, options->wrap)) { // check out of bounds note: this must not be skipped or it can lead to crashes partFlags.outofbounds = true; if (options->killoutofbounds) { bool killthis = true; if (options->useGravity) { // if gravity is used, only kill below 'floor level' if (partFlags.reversegrav) { // skip at x = 0, do not skip far out of bounds if (newX < 0 || newX > maxX << 2) killthis = false; } else { // skip at x = max, do not skip far out of bounds if (newX > 0 && newX < maxX << 2) killthis = false; } } if (killthis) part.ttl = 0; } } if (!partFlags.fixed) part.x = newX; // set new position else part.vx = 0; // set speed to zero. note: particle can get speed in collisions, if unfixed, it should not speed away } } // apply a force in x direction to individual particle (or source) // caller needs to provide a 8bit counter (for each paticle) that holds its value between calls // force is in 3.4 fixed point notation so force=16 means apply v+1 each frame default of 8 is every other frame void ParticleSystem1D::applyForce(PSparticle1D &part, const int8_t xforce, uint8_t &counter) { int32_t dv = calcForce_dv(xforce, counter); // velocity increase part.vx = limitSpeed((int32_t)part.vx + dv); // apply the force to particle } // apply a force to all particles // force is in 3.4 fixed point notation (see above) void ParticleSystem1D::applyForce(const int8_t xforce) { int32_t dv = calcForce_dv(xforce, forcecounter); // velocity increase for (uint32_t i = 0; i < usedParticles; i++) { particles[i].vx = limitSpeed((int32_t)particles[i].vx + dv); } } // apply gravity to all particles using PS global gforce setting // gforce is in 3.4 fixed point notation, see note above void ParticleSystem1D::applyGravity() { int32_t dv_raw = calcForce_dv(gforce, gforcecounter); for (uint32_t i = 0; i < usedParticles; i++) { int32_t dv = dv_raw; if (particleFlags[i].reversegrav) dv = -dv_raw; // note: not checking if particle is dead is omitted as most are usually alive and if few are alive, rendering is fast anyways particles[i].vx = limitSpeed((int32_t)particles[i].vx - dv); } } // apply gravity to single particle using system settings (use this for sources) // function does not increment gravity counter, if gravity setting is disabled, this cannot be used void ParticleSystem1D::applyGravity(PSparticle1D &part, PSparticleFlags1D &partFlags) { uint32_t counterbkp = gforcecounter; int32_t dv = calcForce_dv(gforce, gforcecounter); if (partFlags.reversegrav) dv = -dv; gforcecounter = counterbkp; //save it back part.vx = limitSpeed((int32_t)part.vx - dv); } // slow down particle by friction, the higher the speed, the higher the friction. a high friction coefficient slows them more (255 means instant stop) // note: a coefficient smaller than 0 will speed them up (this is a feature, not a bug), coefficient larger than 255 inverts the speed, so don't do that void ParticleSystem1D::applyFriction(int32_t coefficient) { #if defined(CONFIG_IDF_TARGET_ESP32C3) || defined(ESP8266) // use bitshifts with rounding instead of division (2x faster) int32_t friction = 256 - coefficient; for (uint32_t i = 0; i < usedParticles; i++) { if (particles[i].ttl) particles[i].vx = ((int32_t)particles[i].vx * friction + (((int32_t)particles[i].vx >> 31) & 0xFF)) >> 8; // note: (v>>31) & 0xFF)) extracts the sign and adds 255 if negative for correct rounding using shifts } #else // division is faster on ESP32, S2 and S3 int32_t friction = 255 - coefficient; for (uint32_t i = 0; i < usedParticles; i++) { if (particles[i].ttl) particles[i].vx = ((int32_t)particles[i].vx * friction) / 255; } #endif } // render particles to the LED buffer (uses palette to render the 8bit particle color value) // if wrap is set, particles half out of bounds are rendered to the other side of the matrix // warning: do not render out of bounds particles or system will crash! rendering does not check if particle is out of bounds void ParticleSystem1D::render() { if (framebuffer == nullptr) { PSPRINTLN(F("PS render: no framebuffer!")); return; } CRGBW baseRGB; uint32_t brightness; // particle brightness, fades if dying TBlendType blend = LINEARBLEND; // default color rendering: wrap palette if (particlesettings.colorByAge || particlesettings.colorByPosition) { blend = LINEARBLEND_NOWRAP; } if (motionBlur) { // blurring active for (int32_t x = 0; x <= maxXpixel; x++) { framebuffer[x] = fast_color_scale(framebuffer[x], motionBlur); } } else { // no blurring: clear buffer memset(framebuffer, 0, (maxXpixel+1) * sizeof(CRGBW)); } // go over particles and render them to the buffer for (uint32_t i = 0; i < usedParticles; i++) { if ( particles[i].ttl == 0 || particleFlags[i].outofbounds) continue; // generate RGB values for particle brightness = min(particles[i].ttl << 1, (int)255); baseRGB = ColorFromPaletteWLED(SEGPALETTE, particles[i].hue, 255, blend); if (advPartProps != nullptr) { //saturation is advanced property in 1D system if (advPartProps[i].sat < 255) { CHSV32 baseHSV; rgb2hsv(baseRGB.color32, baseHSV); // convert to HSV baseHSV.s = min(baseHSV.s, advPartProps[i].sat); // set the saturation but don't increase it hsv2rgb(baseHSV, baseRGB.color32); // convert back to RGB } } if (gammaCorrectCol) brightness = gamma8(brightness); // apply gamma correction, used for gamma-inverted brightness distribution renderParticle(i, brightness, baseRGB, particlesettings.wrap); } // apply smear-blur to rendered frame if (smearBlur) { SEGMENT.blur(smearBlur, true); } // add background color CRGBW bg_color = SEGCOLOR(1); if (bg_color > 0) { //if not black for (int32_t i = 0; i <= maxXpixel; i++) { framebuffer[i] = fast_color_scaleAdd(framebuffer[i], bg_color); } } #ifndef WLED_DISABLE_2D // transfer local buffer to segment if using 1D->2D mapping if (SEGMENT.is2D() && SEGMENT.map1D2D) { for (int x = 0; x <= maxXpixel; x++) { //for (int x = 0; x < SEGMENT.vLength(); x++) { SEGMENT.setPixelColor(x, framebuffer[x]); // this applies the mapping } } #endif } // calculate pixel positions and brightness distribution and render the particle to local buffer or global buffer void WLED_O2_ATTR ParticleSystem1D::renderParticle(const uint32_t particleindex, const uint8_t brightness, const CRGBW &color, const bool wrap) { uint32_t size = particlesize; if (perParticleSize && advPartProps != nullptr) // use advanced size properties size = 1 + advPartProps[particleindex].size; // add 1 to avoid single pixel size particles (collisions do not support it) if (size == 0) { //single pixel particle, can be out of bounds as oob checking is made for 2-pixel particles (and updating it uses more code) uint32_t x = particles[particleindex].x >> PS_P_RADIUS_SHIFT_1D; if (x <= (uint32_t)maxXpixel) { //by making x unsigned there is no need to check < 0 as it will overflow framebuffer[x] = fast_color_scaleAdd(framebuffer[x], color, brightness); } return; } //render larger particles if (size > 1) { // size > 1: render as gradient line renderLargeParticle(size, particleindex, brightness, color, wrap); // larger size rendering return; } // standard rendering (2 pixels per particle) bool pxlisinframe[2] = {true, true}; int32_t pxlbrightness[2]; int32_t pixco[2]; // physical pixel coordinates of the two pixels representing a particle // add half a radius as the rendering algorithm always starts at the bottom left, this leaves things positive, so shifts can be used, then shift coordinate by a full pixel (x-- below) int32_t xoffset = particles[particleindex].x + PS_P_HALFRADIUS_1D; int32_t dx = xoffset & (PS_P_RADIUS_1D - 1); //relativ particle position in subpixel space, modulo replaced with bitwise AND int32_t x = xoffset >> PS_P_RADIUS_SHIFT_1D; // divide by PS_P_RADIUS, bitshift of negative number stays negative -> checking below for x < 0 works (but does not when using division) // set the raw pixel coordinates pixco[1] = x; // right pixel x--; // shift by a full pixel here, this is skipped above to not do -1 and then +1 pixco[0] = x; // left pixel //calculate the brightness values for both pixels using linear interpolation (note: in standard rendering out of frame pixels could be skipped but if checks add more clock cycles over all) pxlbrightness[0] = (((int32_t)PS_P_RADIUS_1D - dx) * brightness) >> PS_P_SURFACE_1D; pxlbrightness[1] = (dx * brightness) >> PS_P_SURFACE_1D; // adjust brightness such that distribution is linear after gamma correction: // - scale brigthness with gamma correction (done in render()) // - apply inverse gamma correction to brightness values // - gamma is applied again in show() -> the resulting brightness distribution is linear but gamma corrected in total -> fixes brightness fluctuations if (gammaCorrectCol) { pxlbrightness[0] = gamma8inv(pxlbrightness[0]); // use look-up-table for invers gamma pxlbrightness[1] = gamma8inv(pxlbrightness[1]); } // check if any pixels are out of frame if (pixco[0] < 0) { // left pixels out of frame if (wrap) // wrap x to the other side if required pixco[0] = maxXpixel; else { pxlisinframe[0] = false; // pixel is out of matrix boundaries, do not render if (pixco[0] < -1) return; // both pixels out of frame (safety check) } } else if (pixco[1] > (int32_t)maxXpixel) { // right pixel, only has to be checkt if left pixel did not overflow if (wrap) // wrap y to the other side if required pixco[1] = 0; else { pxlisinframe[1] = false; if (pixco[0] > (int32_t)maxXpixel) return; // both pixels out of frame (safety check) } } for (uint32_t i = 0; i < 2; i++) { if (pxlisinframe[i]) { framebuffer[pixco[i]] = fast_color_scaleAdd(framebuffer[pixco[i]], color, pxlbrightness[i]); } } } // render particle as a line with linear brightness falloff and sub-pixel precision, size is in 0-255 (1-9 pixel radius) void WLED_O2_ATTR ParticleSystem1D::renderLargeParticle(const uint32_t size, const uint32_t particleindex, const uint8_t brightness, const CRGBW& color, const bool wrap) { int32_t x_subcenter = particles[particleindex].x; // particle position in sub-pixel space // sub-pixel offset (0-31) int32_t x_offset = x_subcenter & (PS_P_RADIUS_1D - 1); // same as modulo PS_P_RADIUS but faster int32_t x_center = x_subcenter >> PS_P_RADIUS_SHIFT_1D; // integer pixel position, this is rounded down // particle radius in pixels, size = 1 means radius of just over 1 pixel int32_t r_subpixel = size + PS_P_RADIUS_1D + 1; // size = 255 is radius of 9, so add 33 -> 33+255=288, 288>>5=9 pixels (i.e. the +1 is needed to correct for bitshift losses) // rendering bounding box in pixels int32_t r_pixels = r_subpixel >> PS_P_RADIUS_SHIFT_1D; int32_t x_min = x_center - r_pixels - 1; // extend by one for much smoother movement int32_t x_max = x_center + r_pixels + 1; // cache for speed uint32_t matrixX = maxXpixel + 1; // iterate over bounding box and render each pixel for (int32_t px = x_min; px <= x_max; px++) { // Check bounds and apply wrapping int32_t render_x = px; if (render_x < 0) { if (!wrap) continue; // skip out of frame pixels render_x += matrixX; } else if (render_x > maxXpixel) { if (!wrap) continue; render_x -= matrixX; } // squared distance from particle center int32_t dx_sq = ((px << PS_P_RADIUS_SHIFT_1D) - x_subcenter + PS_P_HALFRADIUS_1D); // explanation see 2D version dx_sq = dx_sq * dx_sq; int32_t rx_sq = r_subpixel * r_subpixel; uint32_t dist_sq = (dx_sq << 8) / rx_sq; // normalized squared distance in fixed point (0-256) // calculate brightness based on distance from particle center with linear falloff uint8_t pixel_brightness = dist_sq >= 256 ? 0 : ((256 - dist_sq) * brightness) >> 8; //if (pixel_brightness == 0) continue; // skip black pixels note: very few pixels will be black, skipping this is usually faster // Render pixel framebuffer[render_x] = fast_color_scaleAdd(framebuffer[render_x], color, pixel_brightness); } } // detect collisions in an array of particles and handle them void ParticleSystem1D::handleCollisions() { uint32_t collisiondistance = particleHardRadius << 1; // twice the radius is min distance between colliding particles uint32_t checkDistSq = max(2 * PS_P_MAXSPEED, (int)collisiondistance); if (perParticleSize && advPartProps != nullptr) // using individual particle size checkDistSq = max(2 * PS_P_MAXSPEED, (512 * 52) >> 6); // max possible collision distance that catches all collisons checkDistSq = checkDistSq * checkDistSq; // square it for distance comparison (faster than abs() ) // note: partices are binned by position, assumption is that no more than half of the particles are in the same bin // if they are, collisionStartIdx is increased so each particle collides at least every second frame (which still gives decent collisions) int binWidth = 64 * PS_P_RADIUS_1D; // width of each bin, a compromise between speed and accuracy int32_t overlap = collisiondistance + (2 * PS_P_MAXSPEED); // overlap bins to include edge particles to neighbouring bins (+ look-ahead of speed) if (perParticleSize && advPartProps != nullptr) //may be using individual particle size overlap = 512; // 2 * max radius, enough to catch all collisions even at full speed uint32_t maxBinParticles = max((uint32_t)50, (usedParticles + 1) / 4); // do not bin small amounts, limit max to 1/4 of particles uint32_t numBins = (maxX + (binWidth - 1)) / binWidth; // calculate number of bins if (usedParticles < maxBinParticles) { numBins = 1; // use single bin for small number of particles binWidth = maxX + 1; } uint16_t binIndices[maxBinParticles]; // array to store indices of particles in a bin uint32_t binParticleCount; // number of particles in the current bin uint32_t nextFrameStartIdx = hw_random16(usedParticles); // index of the first particle in the next frame (set to fixed value if bin overflow) uint32_t pidx = collisionStartIdx; //start index in case a bin is full, process remaining particles next frame for (uint32_t bin = 0; bin < numBins; bin++) { binParticleCount = 0; // reset for this bin int32_t binStart = bin * binWidth - overlap; // note: first bin will extend to negative, but that is ok as out of bounds particles are ignored int32_t binEnd = binStart + binWidth + (overlap << 1); // add twice the overlap as start is start-overlap, note: last bin can be out of bounds, see above // fill the binIndices array for this bin for (uint32_t i = 0; i < usedParticles; i++) { if (particles[pidx].ttl > 0) { // alivee if (particles[pidx].x >= binStart && particles[pidx].x <= binEnd) { // >= and <= to include particles on the edge of the bin (overlap to ensure boarder particles collide with adjacent bins) if (particleFlags[pidx].outofbounds == 0 && particleFlags[pidx].collide) { // particle is in frame and does collide note: checking flags is quite slow and usually these are set, so faster to check here if (binParticleCount >= maxBinParticles) { // bin is full, more particles in this bin so do the rest next frame nextFrameStartIdx = pidx; // bin overflow can only happen once as bin size is at least half of the particles (or half +1) break; } binIndices[binParticleCount++] = pidx; } } } pidx++; if (pidx >= usedParticles) pidx = 0; // wrap around } for (uint32_t i = 0; i < binParticleCount; i++) { // go though all 'higher number' particles and see if any of those are in close proximity and if they are, make them collide uint32_t idx_i = binIndices[i]; for (uint32_t j = i + 1; j < binParticleCount; j++) { // check against higher number particles uint32_t idx_j = binIndices[j]; int32_t dx = particles[idx_j].x - particles[idx_i].x; // distance between particles uint32_t dx_sq = dx * dx; // square distance (faster than abs() and works the same) if (dx_sq <= checkDistSq) { // possible collision imminent, check properly note: this is slower than using direct speed look-ahead (like in 2D) but more accurate and fast enough for 1D collideParticles(idx_i, idx_j, dx, collisiondistance); // handle the collision } } } } collisionStartIdx = nextFrameStartIdx; // set the start index for the next frame } // handle a collision if close proximity is detected, i.e. dx smaller than 2*radius + speed look-ahead void WLED_O2_ATTR ParticleSystem1D::collideParticles(uint32_t partIdx1, uint32_t partIdx2, int32_t dx, uint32_t collisiondistance) { int32_t massratio1 = 0; // 0 means dont use mass ratio (equal mass) int32_t massratio2 = 0; if (perParticleSize && advPartProps != nullptr) { // use advanced size properties, calculate collision distance and mass ratio collisiondistance = (PS_P_MINHARDRADIUS_1D * 2) + ((((uint32_t)advPartProps[partIdx1].size + (uint32_t)advPartProps[partIdx2].size) * 52) >> 6); // collision distance, use 80% of size for tighter stacking (slight overlap) // calculate mass ratio for collision response uint32_t mass1 = PS_P_RADIUS_1D + advPartProps[partIdx1].size; uint32_t mass2 = PS_P_RADIUS_1D + advPartProps[partIdx2].size; uint32_t totalmass = mass1 + mass2 - 2; // -2 to account for rounding massratio1 = (mass2 << 8) / totalmass; // massratio 1 depends on mass of particle 2, i.e. if 2 is heavier -> higher velocity impact on 1 massratio2 = (mass1 << 8) / totalmass; } int32_t dv = (int)particles[partIdx2].vx - (int)particles[partIdx1].vx; int32_t absdv = abs(dv); int32_t dotProduct = (dx * dv); // is always negative if moving towards each other uint32_t dx_abs = abs(dx); if (dotProduct < 0) { // particles are moving towards each other uint32_t lookaheadDistance = collisiondistance + absdv; // add look-ahead: if reaching collisiondistance in this frame, collide if (dx_abs <= lookaheadDistance) { // if one of the particles is fixed, invert the other particle's velocity and multiply by hardness, also set its position to the edge of the fixed particle if (particleFlags[partIdx1].fixed) { particles[partIdx2].vx = -(particles[partIdx2].vx * collisionHardness) / 255; particles[partIdx2].x = particles[partIdx1].x + (dx < 0 ? -collisiondistance : collisiondistance); // dv < 0 means particle2.x < particle1.x return; } else if (particleFlags[partIdx2].fixed) { particles[partIdx1].vx = -(particles[partIdx1].vx * collisionHardness) / 255; particles[partIdx1].x = particles[partIdx2].x + (dx < 0 ? collisiondistance : -collisiondistance); return; } int32_t surfacehardness = max(collisionHardness, (int32_t)PS_P_MINSURFACEHARDNESS_1D); // if particles are soft, the impulse must stay above a limit or collisions slip through // Calculate new velocities after collision note: not using dot product like in 2D as impulse is purely speed depnedent #if defined(CONFIG_IDF_TARGET_ESP32C3) || defined(ESP8266) // use bitshifts with rounding instead of division (2x faster) int32_t impulse = (dv * surfacehardness + ((dv >> 31) & 0xFF)) >> 8; // note: (v>>31) & 0xFF)) extracts the sign and adds 255 if negative for correct rounding using shifts #else // division is faster on ESP32, S2 and S3 int32_t impulse = (dv * surfacehardness) / 255; #endif // if particles are not the same size, use a mass ratio. mass ratio is set to 0 if particles are the same size if (massratio1) { int vx1 = (int)particles[partIdx1].vx + ((impulse * massratio1) >> 7); // mass ratio is in fixed point 8bit int vx2 = (int)particles[partIdx2].vx - ((impulse * massratio2) >> 7); // limit speeds to max speed (required as a lot of impulse can be transferred from a large to a small particle) particles[partIdx1].vx = limitSpeed(vx1); particles[partIdx2].vx = limitSpeed(vx2); } else { particles[partIdx1].vx += impulse; particles[partIdx2].vx -= impulse; } if (collisionHardness < PS_P_MINSURFACEHARDNESS_1D && (SEGMENT.call & 0x07) == 0) { // if particles are soft, they become 'sticky' i.e. apply some friction const uint32_t coeff = collisionHardness + (250 - PS_P_MINSURFACEHARDNESS_1D); #if defined(CONFIG_IDF_TARGET_ESP32C3) || defined(ESP8266) // use bitshifts with rounding instead of division (2x faster) particles[partIdx1].vx = ((int32_t)particles[partIdx1].vx * coeff + (((int32_t)particles[partIdx1].vx >> 31) & 0xFF)) >> 8; // note: (v>>31) & 0xFF)) extracts the sign and adds 255 if negative for correct rounding using shifts particles[partIdx2].vx = ((int32_t)particles[partIdx2].vx * coeff + (((int32_t)particles[partIdx2].vx >> 31) & 0xFF)) >> 8; #else // division is faster on ESP32, S2 and S3 particles[partIdx1].vx = ((int32_t)particles[partIdx1].vx * coeff) / 255; particles[partIdx2].vx = ((int32_t)particles[partIdx2].vx * coeff) / 255; #endif } } else { return; // not close enough yet } } // particles have volume, push particles apart if they are too close // note: like in 2D, pushing by a distance makes softer piles collapse, giving particles speed prevents that and looks nicer if (dx_abs < collisiondistance) { // too close, force push particles so they dont collapse int32_t pushamount = 1 + ((collisiondistance - dx_abs) >> 3); // push by eighth of deviation (plus 1 to push at least a little), note: pushing too much leads to pass-throughs and more flickering int32_t addspeed = 1; if (dx < 0) { // particle2.x < particle1.x pushamount = -pushamount; addspeed = -addspeed; } if (absdv < 4) { // low relative speed, add speed to help with the pushing (less collapsing piles) particles[partIdx1].vx -= addspeed; particles[partIdx2].vx += addspeed; } // push only one particle to avoid oscillations bool fairlyrandom = dotProduct & 0x01; if (fairlyrandom) { particles[partIdx1].x -= pushamount; } else { particles[partIdx2].x += pushamount; } } } // update size and pointers (memory location and size can change dynamically) // note: do not access the PS class in FX befor running this function (or it messes up SEGENV.data) void ParticleSystem1D::updateSystem(void) { setSize(SEGMENT.vLength()); // update size updatePSpointers(advPartProps != nullptr); } // set the pointers for the class (this only has to be done once and not on every FX call, only the class pointer needs to be reassigned to SEGENV.data every time) // function returns the pointer to the next byte available for the FX (if it assigned more memory for other stuff using the above allocate function) // FX handles the PSsources, need to tell this function how many there are void ParticleSystem1D::updatePSpointers(bool isadvanced) { // Note on memory alignment: // a pointer MUST be 4 byte aligned. sizeof() in a struct/class is always aligned to the largest element. if it contains a 32bit, it will be padded to 4 bytes, 16bit is padded to 2byte alignment. // The PS is aligned to 4 bytes, a PSparticle is aligned to 2 and a struct containing only byte sized variables is not aligned at all and may need to be padded when dividing the memoryblock. // by making sure that the number of sources and particles is a multiple of 4, padding can be skipped here as alignent is ensured, independent of struct sizes. particles = reinterpret_cast(this + 1); // pointer to particles particleFlags = reinterpret_cast(particles + numParticles); // pointer to particle flags sources = reinterpret_cast(particleFlags + numParticles); // pointer to source(s) PSdataEnd = reinterpret_cast(sources + numSources); // pointer to first available byte after the PS for FX additional data (already aligned to 4 byte boundary) #ifndef WLED_DISABLE_2D if (SEGMENT.is2D() && SEGMENT.map1D2D) { framebuffer = reinterpret_cast(sources + numSources); // use local framebuffer for 1D->2D mapping PSdataEnd = reinterpret_cast(framebuffer + SEGMENT.maxMappingLength()); // pointer to first available byte after the PS for FX additional data (still aligned to 4 byte boundary) } else #endif framebuffer = SEGMENT.getPixels(); // use segment buffer for standard 1D rendering if (isadvanced) { advPartProps = reinterpret_cast(PSdataEnd); PSdataEnd = reinterpret_cast(advPartProps + numParticles); // since numParticles is a multiple of 4, this is always aligned to 4 bytes. No need to add padding bytes here } #ifdef WLED_DEBUG_PS PSPRINTLN(" PS Pointers: "); PSPRINT(" PS : 0x"); Serial.println((uintptr_t)this, HEX); PSPRINT(" Particleflags : 0x"); Serial.println((uintptr_t)particleFlags, HEX); PSPRINT(" Particles : 0x"); Serial.println((uintptr_t)particles, HEX); PSPRINT(" Sources : 0x"); Serial.println((uintptr_t)sources, HEX); #endif } //non class functions to use for initialization, fraction is uint8_t: 255 means 100% uint32_t calculateNumberOfParticles1D(const uint32_t fraction, const bool isadvanced) { uint32_t numberofParticles = SEGMENT.virtualLength(); // one particle per pixel (if possible) uint32_t particlelimit = MAXPARTICLES_1D; // maximum number of paticles allowed numberofParticles = min(numberofParticles, particlelimit); // limit to particlelimit if (isadvanced) // advanced property array needs ram, reduce number of particles to use the same amount numberofParticles = (numberofParticles * sizeof(PSparticle1D)) / (sizeof(PSparticle1D) + sizeof(PSadvancedParticle1D)); numberofParticles = (numberofParticles * (fraction + 1)) >> 8; // calculate fraction of particles numberofParticles = numberofParticles < 10 ? 10 : numberofParticles; // 10 minimum //make sure it is a multiple of 4 for proper memory alignment (easier than using padding bytes) numberofParticles = (numberofParticles+3) & ~0x03; // note: with a separate particle buffer, this is probably unnecessary PSPRINTLN(" calc numparticles:" + String(numberofParticles)); return numberofParticles; } uint32_t calculateNumberOfSources1D(const uint32_t requestedsources) { int numberofSources = max(1, min((int)requestedsources,MAXSOURCES_1D)); // limit // make sure it is a multiple of 4 for proper memory alignment (so minimum is acutally 4) numberofSources = (numberofSources+3) & ~0x03; return numberofSources; } //allocate memory for particle system class, particles, sprays plus additional memory requested by FX bool allocateParticleSystemMemory1D(const uint32_t numparticles, const uint32_t numsources, const bool isadvanced, const uint32_t additionalbytes) { uint32_t requiredmemory = sizeof(ParticleSystem1D); // functions above make sure these are a multiple of 4 bytes (to avoid alignment issues) requiredmemory += sizeof(PSparticleFlags1D) * numparticles; requiredmemory += sizeof(PSparticle1D) * numparticles; requiredmemory += sizeof(PSsource1D) * numsources; #ifndef WLED_DISABLE_2D if (SEGMENT.is2D()) requiredmemory += sizeof(uint32_t) * SEGMENT.maxMappingLength(); // need local buffer for mapped rendering #endif requiredmemory += additionalbytes; if (isadvanced) requiredmemory += sizeof(PSadvancedParticle1D) * numparticles; return(SEGMENT.allocateData(requiredmemory)); } // initialize Particle System, allocate additional bytes if needed (pointer to those bytes can be read from particle system class: PSdataEnd) // note: percentofparticles is in uint8_t, for example 191 means 75%, (deafaults to 255 or 100% meaning one particle per pixel), can be more than 100% (but not recommended, can cause out of memory) bool initParticleSystem1D(ParticleSystem1D *&PartSys, const uint32_t requestedsources, const uint8_t fractionofparticles, const uint32_t additionalbytes, const bool advanced) { if (SEGLEN == 1) { errorFlag = ERR_NOT_IMPL; // TODO: need a better error code if more codes are added SEGMENT.deallocateData(); // deallocate any data to make sure data is null (there is no valid PS in data and data can only be checked for null) return false; // single pixel not supported } uint32_t numparticles = calculateNumberOfParticles1D(fractionofparticles, advanced); uint32_t numsources = calculateNumberOfSources1D(requestedsources); bool allocsuccess = false; while(numparticles >= 10) { // make sure we have at least 10 particles or quit if (allocateParticleSystemMemory1D(numparticles, numsources, advanced, additionalbytes)) { PSPRINT(F("PS 1D alloc succeeded")); allocsuccess = true; break; // allocation succeeded } numparticles = ((numparticles / 2) + 3) & ~0x03; // cut number of particles in half and try again, must be 4 byte aligned PSPRINTLN(F("PS 1D alloc failed, trying with less particles...")); } if (!allocsuccess) { PSPRINTLN(F("PS init failed: memory depleted")); return false; // allocation failed } PartSys = new (SEGENV.data) ParticleSystem1D(SEGMENT.virtualLength(), numparticles, numsources, advanced); // particle system constructor return true; } #endif // WLED_DISABLE_PARTICLESYSTEM1D #if !(defined(WLED_DISABLE_PARTICLESYSTEM2D) && defined(WLED_DISABLE_PARTICLESYSTEM1D)) // not both disabled ////////////////////////////// // Shared Utility Functions // ////////////////////////////// // calculate the delta speed (dV) value and update the counter for force calculation (is used several times, function saves on codesize) // force is in 3.4 fixedpoint notation, +/-127 static int32_t calcForce_dv(const int8_t force, uint8_t &counter) { if (force == 0) return 0; // for small forces, need to use a delay counter int32_t force_abs = abs(force); // absolute value (faster than lots of if's only 7 instructions) int32_t dv = 0; // for small forces, need to use a delay counter, apply force only if it overflows if (force_abs < 16) { counter += force_abs; if (counter > 15) { counter -= 16; dv = force < 0 ? -1 : 1; // force is either 1 or -1 if it is small (zero force is handled above) } } else dv = force / 16; // MSBs, note: cannot use bitshift as dv can be negative return dv; } // check if particle is out of bounds and wrap it around if required, returns false if out of bounds static bool checkBoundsAndWrap(int32_t &position, const int32_t max, const int32_t particleradius, const bool wrap) { if ((uint32_t)position > (uint32_t)max) { // check if particle reached an edge, cast to uint32_t to save negative checking (max is always positive) if (wrap) { position = position % (max + 1); // note: cannot optimize modulo, particles can be far out of bounds when wrap is enabled if (position < 0) position += max + 1; } else if (((position < -particleradius) || (position > max + particleradius))) // particle is leaving boundaries, out of bounds if it has fully left return false; // out of bounds } return true; // particle is in bounds } // this is a fast version for RGB color adding ignoring white channel (PS does not handle white) including scaling of second color // note: function is mainly used to add scaled colors, so checking if one color is black is slower static uint32_t fast_color_scaleAdd(const uint32_t c1, const uint32_t c2, const uint8_t scale) { constexpr uint32_t MASK_RB = 0x00FF00FF; // red and blue mask constexpr uint32_t MASK_G = 0x0000FF00; // green mask uint32_t rb = c2 & MASK_RB; // 0x00RR00BB uint32_t g = c2 & MASK_G; // 0x0000GG00 // scale second color rb = ((rb * scale) >> 8) & MASK_RB; g = ((g * scale) >> 8) & MASK_G; // add colors rb = (c1 & MASK_RB) + rb; g = ((c1 & MASK_G) + g); // check for overflow by looking at the 9th bit of each channel if ((rb | (g >> 8)) & 0x01000100) { // find max among the three 16-bit values g = g >> 8; // shift to get 0x000000GG uint32_t max_val = (rb >> 16); // red max_val = ((rb & 0xFFFF) > max_val) ? rb & 0xFFFF : max_val; // blue max_val = (g > max_val) ? g : max_val; // green // scale down to avoid saturation uint32_t scale_factor = (255 << 8) / max_val; rb = ((rb * scale_factor) >> 8) & MASK_RB; g = (g * scale_factor) & MASK_G; } return rb | g; } #endif // !(defined(WLED_DISABLE_PARTICLESYSTEM2D) && defined(WLED_DISABLE_PARTICLESYSTEM1D)) ================================================ FILE: wled00/FXparticleSystem.h ================================================ /* FXparticleSystem.cpp Particle system with functions for particle generation, particle movement and particle rendering to RGB matrix. by DedeHai (Damian Schneider) 2013-2024 Copyright (c) 2024 Damian Schneider Licensed under the EUPL v. 1.2 or later */ #ifdef WLED_DISABLE_2D #define WLED_DISABLE_PARTICLESYSTEM2D #endif #if !(defined(WLED_DISABLE_PARTICLESYSTEM2D) && defined(WLED_DISABLE_PARTICLESYSTEM1D)) // not both disabled #include #include "wled.h" #define PS_P_MAXSPEED 120 // maximum speed a particle can have (vx/vy is int8), limiting below 127 to avoid overflows in collisions due to rounding errors #define MAX_MEMIDLE 10 // max idle time (in frames) before memory is deallocated (if deallocated during an effect, it will crash!) //#define WLED_DEBUG_PS // note: enabling debug uses ~3k of flash #ifdef WLED_DEBUG_PS #define PSPRINT(x) Serial.print(x) #define PSPRINTLN(x) Serial.println(x) #else #define PSPRINT(x) #define PSPRINTLN(x) #endif // limit speed of particles (used in 1D and 2D) static inline int32_t limitSpeed(const int32_t speed) { return speed > PS_P_MAXSPEED ? PS_P_MAXSPEED : (speed < -PS_P_MAXSPEED ? -PS_P_MAXSPEED : speed); // note: this is slightly faster than using min/max at the cost of 50bytes of flash } #endif #ifndef WLED_DISABLE_PARTICLESYSTEM2D // memory allocation (based on reasonable segment size and available FX memory) #ifdef ESP8266 #define MAXPARTICLES_2D 256 #define MAXSOURCES_2D 24 #define SOURCEREDUCTIONFACTOR 8 #elif ARDUINO_ARCH_ESP32S2 #define MAXPARTICLES_2D 1024 #define MAXSOURCES_2D 64 #define SOURCEREDUCTIONFACTOR 6 #else #define MAXPARTICLES_2D 2048 #define MAXSOURCES_2D 128 #define SOURCEREDUCTIONFACTOR 4 #endif // particle dimensions (subpixel division) #define PS_P_RADIUS 64 // subpixel size, each pixel is divided by this for particle movement (must be a power of 2) #define PS_P_HALFRADIUS (PS_P_RADIUS >> 1) #define PS_P_RADIUS_SHIFT 6 // shift for RADIUS #define PS_P_SURFACE 12 // shift: 2^PS_P_SURFACE = (PS_P_RADIUS)^2 #define PS_P_MINHARDRADIUS 64 // minimum hard surface radius for collisions #define PS_P_MINSURFACEHARDNESS 128 // minimum hardness used in collision impulse calculation, below this hardness, particles become sticky // struct for PS settings (shared for 1D and 2D class) typedef union { struct{ // one byte bit field for 2D settings bool wrapX : 1; bool wrapY : 1; bool bounceX : 1; bool bounceY : 1; bool killoutofbounds : 1; // if set, out of bound particles are killed immediately bool useGravity : 1; // set to 1 if gravity is used, disables bounceY at the top bool useCollisions : 1; bool colorByAge : 1; // if set, particle hue is set by ttl value in render function }; byte asByte; // access as a byte, order is: LSB is first entry in the list above } PSsettings2D; //struct for a single particle typedef struct { // 10 bytes int16_t x; // x position in particle system int16_t y; // y position in particle system uint16_t ttl; // time to live in frames int8_t vx; // horizontal velocity int8_t vy; // vertical velocity uint8_t hue; // color hue uint8_t sat; // particle color saturation } PSparticle; //struct for particle flags note: this is separate from the particle struct to save memory (ram alignment) typedef union { struct { // 1 byte bool outofbounds : 1; // out of bounds flag, set to true if particle is outside of display area bool collide : 1; // if set, particle takes part in collisions bool perpetual : 1; // if set, particle does not age (TTL is not decremented in move function, it still dies from killoutofbounds) bool custom1 : 1; // unused custom flags, can be used by FX to track particle states bool custom2 : 1; bool custom3 : 1; bool custom4 : 1; bool custom5 : 1; }; byte asByte; // access as a byte, order is: LSB is first entry in the list above } PSparticleFlags; // struct for additional particle settings (option) typedef struct { // 2 bytes uint8_t size; // particle size, 255 means 10 pixels in diameter, set perParticleSize = true to enable uint8_t forcecounter; // counter for applying forces to individual particles } PSadvancedParticle; // struct for advanced particle size control (option) typedef struct { // 8 bytes uint8_t asymmetry; // asymmetrical size (0=symmetrical, 255 fully asymmetric) uint8_t asymdir; // direction of asymmetry, 64 is x, 192 is y (0 and 128 is symmetrical) uint8_t maxsize; // target size for growing uint8_t minsize; // target size for shrinking uint8_t sizecounter : 4; // counters used for size contol (grow/shrink/wobble) uint8_t wobblecounter : 4; uint8_t growspeed : 4; uint8_t shrinkspeed : 4; uint8_t wobblespeed : 4; bool grow : 1; // flags bool shrink : 1; bool pulsate : 1; // grows & shrinks & grows & ... bool wobble : 1; // alternate x and y size } PSsizeControl; //struct for a particle source (20 bytes) typedef struct { uint16_t minLife; // minimum ttl of emittet particles uint16_t maxLife; // maximum ttl of emitted particles PSparticle source; // use a particle as the emitter source (speed, position, color) PSparticleFlags sourceFlags; // flags for the source particle int8_t var; // variation of emitted speed (adds random(+/- var) to speed) int8_t vx; // emitting speed int8_t vy; uint8_t size; // particle size (advanced property), global size is added on top to this size } PSsource; // class uses approximately 60 bytes class ParticleSystem2D { public: ParticleSystem2D(const uint32_t width, const uint32_t height, const uint32_t numberofparticles, const uint32_t numberofsources, const bool isadvanced = false, const bool sizecontrol = false); // constructor // note: memory is allcated in the FX function, no deconstructor needed void update(void); //update the particles according to set options and render to the matrix void updateFire(const uint8_t intensity); // update function for fire void updateSystem(void); // call at the beginning of every FX, updates pointers and dimensions void particleMoveUpdate(PSparticle &part, PSparticleFlags &partFlags, PSsettings2D *options = NULL, PSadvancedParticle *advancedproperties = NULL); // move function // particle emitters int32_t sprayEmit(const PSsource &emitter); void flameEmit(const PSsource &emitter); int32_t angleEmit(PSsource& emitter, const uint16_t angle, const int32_t speed); //particle physics void applyGravity(PSparticle &part); // applies gravity to single particle (use this for sources) [[gnu::hot]] void applyForce(PSparticle &part, const int8_t xforce, const int8_t yforce, uint8_t &counter); [[gnu::hot]] void applyForce(const uint32_t particleindex, const int8_t xforce, const int8_t yforce); // use this for advanced property particles void applyForce(const int8_t xforce, const int8_t yforce); // apply a force to all particles void applyAngleForce(PSparticle &part, const int8_t force, const uint16_t angle, uint8_t &counter); void applyAngleForce(const uint32_t particleindex, const int8_t force, const uint16_t angle); // use this for advanced property particles void applyAngleForce(const int8_t force, const uint16_t angle); // apply angular force to all particles void applyFriction(PSparticle &part, const int32_t coefficient); // apply friction to specific particle void applyFriction(const int32_t coefficient); // apply friction to all used particles void pointAttractor(const uint32_t particleindex, PSparticle &attractor, const uint8_t strength, const bool swallow); // set options note: inlining the set function uses more flash so dont optimize void setUsedParticles(const uint8_t percentage); // set the percentage of particles used in the system, 255=100% void setCollisionHardness(const uint8_t hardness); // hardness for particle collisions (255 means full hard) void setWallHardness(const uint8_t hardness); // hardness for bouncing on the wall if bounceXY is set void setWallRoughness(const uint8_t roughness); // wall roughness randomizes wall collisions void setMatrixSize(const uint32_t x, const uint32_t y); void setWrapX(const bool enable); void setWrapY(const bool enable); void setBounceX(const bool enable); void setBounceY(const bool enable); void setKillOutOfBounds(const bool enable); // if enabled, particles outside of matrix instantly die void setSaturation(const uint8_t sat); // set global color saturation void setColorByAge(const bool enable); void setMotionBlur(const uint8_t bluramount); // note: motion blur can only be used if 'particlesize' is set to zero void setSmearBlur(const uint8_t bluramount); // enable 2D smeared blurring of full frame void setParticleSize(const uint8_t size); void setGravity(const int8_t force = 8); void enableParticleCollisions(const bool enable, const uint8_t hardness = 255); PSparticle *particles; // pointer to particle array PSparticleFlags *particleFlags; // pointer to particle flags array PSsource *sources; // pointer to sources PSadvancedParticle *advPartProps; // pointer to advanced particle properties (can be NULL) PSsizeControl *advPartSize; // pointer to advanced particle size control (can be NULL) uint8_t* PSdataEnd; // points to first available byte after the PSmemory, is set in setPointers(). use this for FX custom data int32_t maxX, maxY; // particle system size i.e. width-1 / height-1 in subpixels, Note: all "max" variables must be signed to compare to coordinates (which are signed) int32_t maxXpixel, maxYpixel; // last physical pixel that can be drawn to (FX can read this to read segment size if required), equal to width-1 / height-1 uint32_t numSources; // number of sources uint32_t usedParticles; // number of particles used in animation, is relative to 'numParticles' bool perParticleSize; // if true, uses individual particle sizes from advPartProps if available (disabled when calling setParticleSize()) //note: some variables are 32bit for speed and code size at the cost of ram private: //rendering functions void render(); [[gnu::hot]] void renderParticle(const uint32_t particleindex, const uint8_t brightness, const CRGBW& color, const bool wrapX, const bool wrapY); void renderLargeParticle(const uint32_t size, const uint32_t particleindex, const uint8_t brightness, const CRGBW& color, const bool wrapX, const bool wrapY); //paricle physics applied by system if flags are set void applyGravity(); // applies gravity to all particles void handleCollisions(); void collideParticles(PSparticle &particle1, PSparticle &particle2, int32_t dx, int32_t dy, const uint32_t collDistSq, int32_t massratio1, int32_t massratio2); void fireParticleupdate(); //utility functions void updatePSpointers(const bool isadvanced, const bool sizecontrol); // update the data pointers to current segment data space bool updateSize(PSadvancedParticle *advprops, PSsizeControl *advsize); // advanced size control void getParticleXYsize(PSadvancedParticle *advprops, PSsizeControl *advsize, uint32_t &xsize, uint32_t &ysize); [[gnu::hot]] void bounce(int8_t &incomingspeed, int8_t ¶llelspeed, int32_t &position, const uint32_t maxposition); // bounce on a wall // note: variables that are accessed often are 32bit for speed uint32_t *framebuffer; // frame buffer for rendering. note: using CRGBW as the buffer is slower, ESP compiler seems to optimize this better giving more consistent FPS PSsettings2D particlesettings; // settings used when updating particles (can also used by FX to move sources), do not edit properties directly, use functions above uint32_t numParticles; // total number of particles allocated by this system uint32_t emitIndex; // index to count through particles to emit so searching for dead pixels is faster int32_t collisionHardness; uint32_t wallHardness; uint32_t wallRoughness; // randomizes wall collisions uint32_t particleHardRadius; // hard surface radius of a particle, used for collision detection (32bit for speed) uint16_t collisionStartIdx; // particle array start index for collision detection uint8_t fireIntesity = 0; // fire intensity, used for fire mode (flash use optimization, better than passing an argument to render function) uint8_t forcecounter; // counter for globally applied forces uint8_t gforcecounter; // counter for global gravity int8_t gforce; // gravity strength, default is 8 (negative is allowed, positive is downwards) // global particle properties for basic particles uint8_t particlesize; // global particle size, 0 = 1 pixel, 1 = 2 pixels, 255 = 10 pixels (note: this is also added to individual sized particles, set to 0 or 1 for standard advanced particle rendering) uint8_t motionBlur; // motion blur, values > 100 gives smoother animations. Note: motion blurring does not work if particlesize is > 0 uint8_t smearBlur; // 2D smeared blurring of full frame }; // initialization functions (not part of class) bool initParticleSystem2D(ParticleSystem2D *&PartSys, const uint32_t requestedsources, const uint32_t additionalbytes = 0, const bool advanced = false, const bool sizecontrol = false); uint32_t calculateNumberOfParticles2D(const uint32_t pixels, const bool advanced, const bool sizecontrol); uint32_t calculateNumberOfSources2D(const uint32_t pixels, const uint32_t requestedsources); bool allocateParticleSystemMemory2D(const uint32_t numparticles, const uint32_t numsources, const bool advanced, const bool sizecontrol, const uint32_t additionalbytes); // distance-based brightness for ellipse rendering, returns brightness (0-255) based on distance from ellipse center inline uint8_t calculateEllipseBrightness(int32_t dx, int32_t dy, int32_t rxsq, int32_t rysq, uint8_t maxBrightness) { // square the distances uint32_t dx_sq = dx * dx; uint32_t dy_sq = dy * dy; uint32_t dist_sq = ((dx_sq << 8) / rxsq) + ((dy_sq << 8) / rysq); // normalized squared distance in fixed point: (dx²/rx²) * 256 + (dy²/ry²) * 256 if (dist_sq >= 256) return 0; // pixel is outside the ellipse, unit radius in fixed point: 256 = 1.0 //if (dist_sq <= 96) return maxBrightness; // core at full brightness int32_t falloff = 256 - dist_sq; return (maxBrightness * falloff) >> 8; // linear falloff //return (maxBrightness * falloff * falloff) >> 16; // squared falloff for even softer edges } #endif // WLED_DISABLE_PARTICLESYSTEM2D //////////////////////// // 1D Particle System // //////////////////////// #ifndef WLED_DISABLE_PARTICLESYSTEM1D // memory allocation #ifdef ESP8266 #define MAXPARTICLES_1D 320 #define MAXSOURCES_1D 16 #elif ARDUINO_ARCH_ESP32S2 #define MAXPARTICLES_1D 1300 #define MAXSOURCES_1D 32 #else #define MAXPARTICLES_1D 2600 #define MAXSOURCES_1D 64 #endif // particle dimensions (subpixel division) #define PS_P_RADIUS_1D 32 // subpixel size, each pixel is divided by this for particle movement, if this value is changed, also change the shift defines (next two lines) #define PS_P_HALFRADIUS_1D (PS_P_RADIUS_1D >> 1) #define PS_P_RADIUS_SHIFT_1D 5 // 1 << PS_P_RADIUS_SHIFT = PS_P_RADIUS #define PS_P_SURFACE_1D 5 // shift: 2^PS_P_SURFACE = PS_P_RADIUS_1D #define PS_P_MINHARDRADIUS_1D 32 // minimum hard surface radius note: do not change or hourglass effect will be broken #define PS_P_MINSURFACEHARDNESS_1D 120 // minimum hardness used in collision impulse calculation // struct for PS settings (shared for 1D and 2D class) typedef union { struct{ // one byte bit field for 1D settings bool wrap : 1; bool bounce : 1; bool killoutofbounds : 1; // if set, out of bound particles are killed immediately bool useGravity : 1; // set to 1 if gravity is used, disables bounceY at the top bool useCollisions : 1; bool colorByAge : 1; // if set, particle hue is set by ttl value in render function bool colorByPosition : 1; // if set, particle hue is set by its position in the strip segment bool unused : 1; }; byte asByte; // access as a byte, order is: LSB is first entry in the list above } PSsettings1D; //struct for a single particle (8 bytes) typedef struct { int32_t x; // x position in particle system uint16_t ttl; // time to live in frames int8_t vx; // horizontal velocity uint8_t hue; // color hue } PSparticle1D; //struct for particle flags typedef union { struct { // 1 byte bool outofbounds : 1; // out of bounds flag, set to true if particle is outside of display area bool collide : 1; // if set, particle takes part in collisions bool perpetual : 1; // if set, particle does not age (TTL is not decremented in move function, it still dies from killoutofbounds) bool reversegrav : 1; // if set, gravity is reversed on this particle bool forcedirection : 1; // direction the force was applied, 1 is positive x-direction (used for collision stacking, similar to reversegrav) TODO: not used anymore, can be removed bool fixed : 1; // if set, particle does not move (and collisions make other particles revert direction), bool custom1 : 1; // unused custom flags, can be used by FX to track particle states bool custom2 : 1; }; byte asByte; // access as a byte, order is: LSB is first entry in the list above } PSparticleFlags1D; // struct for additional particle settings (optional) typedef struct { uint8_t sat; // color saturation uint8_t size; // particle size, 255 means 10 pixels in diameter, this overrides global size setting uint8_t forcecounter; // counter for applying forces to individual particles } PSadvancedParticle1D; //struct for a particle source (20 bytes) typedef struct { uint16_t minLife; // minimum ttl of emittet particles uint16_t maxLife; // maximum ttl of emitted particles PSparticle1D source; // use a particle as the emitter source (speed, position, color) PSparticleFlags1D sourceFlags; // flags for the source particle int8_t var; // variation of emitted speed (adds random(+/- var) to speed) int8_t v; // emitting speed uint8_t sat; // color saturation (advanced property) uint8_t size; // particle size (advanced property) // note: there is 3 bytes of padding added here } PSsource1D; class ParticleSystem1D { public: ParticleSystem1D(const uint32_t length, const uint32_t numberofparticles, const uint32_t numberofsources, const bool isadvanced = false); // constructor // note: memory is allcated in the FX function, no deconstructor needed void update(void); //update the particles according to set options and render to the matrix void updateSystem(void); // call at the beginning of every FX, updates pointers and dimensions // particle emitters int32_t sprayEmit(const PSsource1D &emitter); void particleMoveUpdate(PSparticle1D &part, PSparticleFlags1D &partFlags, PSsettings1D *options = NULL, PSadvancedParticle1D *advancedproperties = NULL); // move function //particle physics [[gnu::hot]] void applyForce(PSparticle1D &part, const int8_t xforce, uint8_t &counter); //apply a force to a single particle void applyForce(const int8_t xforce); // apply a force to all particles void applyGravity(PSparticle1D &part, PSparticleFlags1D &partFlags); // applies gravity to single particle (use this for sources) void applyFriction(const int32_t coefficient); // apply friction to all used particles // set options void setUsedParticles(const uint8_t percentage); // set the percentage of particles used in the system, 255=100% void setWallHardness(const uint8_t hardness); // hardness for bouncing on the wall if bounceXY is set void setSize(const uint32_t x); //set particle system size (= strip length) void setWrap(const bool enable); void setBounce(const bool enable); void setKillOutOfBounds(const bool enable); // if enabled, particles outside of matrix instantly die // void setSaturation(uint8_t sat); // set global color saturation void setColorByAge(const bool enable); void setColorByPosition(const bool enable); void setMotionBlur(const uint8_t bluramount); // note: motion blur can only be used if 'particlesize' is set to zero void setSmearBlur(const uint8_t bluramount); // enable 1D smeared blurring of full frame void setParticleSize(const uint8_t size); // particle diameter: size 0 = 1 pixel, size 1 = 2 pixels, size = 255 = 10 pixels, disables per particle size control if called void setGravity(int8_t force = 8); void enableParticleCollisions(bool enable, const uint8_t hardness = 255); PSparticle1D *particles; // pointer to particle array PSparticleFlags1D *particleFlags; // pointer to particle flags array PSsource1D *sources; // pointer to sources PSadvancedParticle1D *advPartProps; // pointer to advanced particle properties (can be NULL) //PSsizeControl *advPartSize; // pointer to advanced particle size control (can be NULL) uint8_t* PSdataEnd; // points to first available byte after the PSmemory, is set in setPointers(). use this for FX custom data int32_t maxX; // particle system size i.e. width-1, Note: all "max" variables must be signed to compare to coordinates (which are signed) int32_t maxXpixel; // last physical pixel that can be drawn to (FX can read this to read segment size if required), equal to width-1 uint32_t numSources; // number of sources uint32_t usedParticles; // number of particles used in animation, is relative to 'numParticles' bool perParticleSize; // if true, uses individual particle sizes from advPartProps if available (disabled when calling setParticleSize()) private: //rendering functions void render(void); void renderParticle(const uint32_t particleindex, const uint8_t brightness, const CRGBW &color, const bool wrap); void renderLargeParticle(const uint32_t size, const uint32_t particleindex, const uint8_t brightness, const CRGBW& color, const bool wrap); //paricle physics applied by system if flags are set void applyGravity(); // applies gravity to all particles void handleCollisions(); void collideParticles(uint32_t partIdx1, uint32_t partIdx2, int32_t dx, uint32_t collisiondistance); //utility functions void updatePSpointers(const bool isadvanced); // update the data pointers to current segment data space //void updateSize(PSadvancedParticle *advprops, PSsizeControl *advsize); // advanced size control [[gnu::hot]] void bounce(int8_t &incomingspeed, int8_t ¶llelspeed, int32_t &position, const uint32_t maxposition); // bounce on a wall // note: variables that are accessed often are 32bit for speed uint32_t *framebuffer; // frame buffer for rendering. note: using CRGBW as the buffer is slower, ESP compiler seems to optimize this better giving more consistent FPS PSsettings1D particlesettings; // settings used when updating particles uint32_t numParticles; // total number of particles allocated by this system uint32_t emitIndex; // index to count through particles to emit so searching for dead pixels is faster int32_t collisionHardness; uint32_t particleHardRadius; // hard surface radius of a particle, used for collision detection uint32_t wallHardness; uint8_t gforcecounter; // counter for global gravity int8_t gforce; // gravity strength, default is 8 (negative is allowed, positive is downwards) uint8_t forcecounter; // counter for globally applied forces uint16_t collisionStartIdx; // particle array start index for collision detection //global particle properties for basic particles uint8_t particlesize; // global particle size, 0 = 1 pixel, 1 = 2 pixels, is overruled by advanced particle size uint8_t motionBlur; // enable motion blur, values > 100 gives smoother animations uint8_t smearBlur; // smeared blurring of full frame }; bool initParticleSystem1D(ParticleSystem1D *&PartSys, const uint32_t requestedsources, const uint8_t fractionofparticles = 255, const uint32_t additionalbytes = 0, const bool advanced = false); uint32_t calculateNumberOfParticles1D(const uint32_t fraction, const bool isadvanced); uint32_t calculateNumberOfSources1D(const uint32_t requestedsources); bool allocateParticleSystemMemory1D(const uint32_t numparticles, const uint32_t numsources, const bool isadvanced, const uint32_t additionalbytes); #endif // WLED_DISABLE_PARTICLESYSTEM1D ================================================ FILE: wled00/NodeStruct.h ================================================ #ifndef WLED_NODESTRUCT_H #define WLED_NODESTRUCT_H /*********************************************************************************************\ * NodeStruct from the ESP Easy project (https://github.com/letscontrolit/ESPEasy) \*********************************************************************************************/ #include #include #define NODE_TYPE_ID_UNDEFINED 0 #define NODE_TYPE_ID_ESP8266 82 // should be 1 #define NODE_TYPE_ID_ESP32 32 // should be 2 #define NODE_TYPE_ID_ESP32S2 33 // etc #define NODE_TYPE_ID_ESP32S3 34 #define NODE_TYPE_ID_ESP32C3 35 // updated node types from the ESP Easy project // https://github.com/letscontrolit/ESPEasy/blob/mega/src/src/DataTypes/NodeTypeID.h //#define NODE_TYPE_ID_ESP32 33 //#define NODE_TYPE_ID_ESP32S2 34 //#define NODE_TYPE_ID_ESP32C3 35 //#define NODE_TYPE_ID_ESP32S3 36 #define NODE_TYPE_ID_ESP32C2 37 #define NODE_TYPE_ID_ESP32H2 38 #define NODE_TYPE_ID_ESP32C6 39 #define NODE_TYPE_ID_ESP32C61 40 #define NODE_TYPE_ID_ESP32C5 41 #define NODE_TYPE_ID_ESP32P4 42 #define NODE_TYPE_ID_ESP32P4r3 45 #define NODE_TYPE_ID_ESP32H21 43 #define NODE_TYPE_ID_ESP32H4 44 /*********************************************************************************************\ * NodeStruct \*********************************************************************************************/ struct NodeStruct { String nodeName; IPAddress ip; uint8_t age; union { uint8_t nodeType; // a waste of space as we only have 5 types struct { uint8_t type : 7; // still a waste of space (4 bits would be enough and future-proof) bool on : 1; }; }; uint32_t build; NodeStruct() : age(0), nodeType(0), build(0) { for (unsigned i = 0; i < 4; ++i) { ip[i] = 0; } } }; typedef std::map NodesMap; #endif // WLED_NODESTRUCT_H ================================================ FILE: wled00/alexa.cpp ================================================ #include "wled.h" /* * Alexa Voice On/Off/Brightness/Color Control. Emulates a Philips Hue bridge to Alexa. * * This was put together from these two excellent projects: * https://github.com/kakopappa/arduino-esp8266-alexa-wemo-switch * https://github.com/probonopd/ESP8266HueEmulator */ #include "src/dependencies/espalexa/EspalexaDevice.h" #ifndef WLED_DISABLE_ALEXA void onAlexaChange(EspalexaDevice* dev); void alexaInit() { if (!alexaEnabled || !WLED_CONNECTED) return; espalexa.removeAllDevices(); // the original configured device for on/off or macros (added first, i.e. index 0) espalexaDevice = new EspalexaDevice(alexaInvocationName, onAlexaChange, EspalexaDeviceType::extendedcolor); espalexa.addDevice(espalexaDevice); // up to 9 devices (added second, third, ... i.e. index 1 to 9) serve for switching on up to nine presets (preset IDs 1 to 9 in WLED), // names are identical as the preset names, switching off can be done by switching off any of them if (alexaNumPresets) { String name = ""; for (unsigned presetIndex = 1; presetIndex <= alexaNumPresets; presetIndex++) { if (!getPresetName(presetIndex, name)) break; // no more presets EspalexaDevice* dev = new EspalexaDevice(name.c_str(), onAlexaChange, EspalexaDeviceType::extendedcolor); espalexa.addDevice(dev); } } espalexa.begin(&server); } void handleAlexa() { if (!alexaEnabled || !WLED_CONNECTED) return; espalexa.loop(); } void onAlexaChange(EspalexaDevice* dev) { EspalexaDeviceProperty m = dev->getLastChangedProperty(); if (m == EspalexaDeviceProperty::on) { if (dev->getId() == 0) // Device 0 is for on/off or macros { if (!macroAlexaOn) { if (bri == 0) { bri = briLast; stateUpdated(CALL_MODE_ALEXA); } } else { applyPreset(macroAlexaOn, CALL_MODE_ALEXA); if (bri == 0) dev->setValue(briLast); //stop Alexa from complaining if macroAlexaOn does not actually turn on } } else // switch-on behavior for preset devices { // turn off other preset devices for (unsigned i = 1; i < espalexa.getDeviceCount(); i++) { if (i == dev->getId()) continue; espalexa.getDevice(i)->setValue(0); // turn off other presets } applyPreset(dev->getId(), CALL_MODE_ALEXA); // in alexaInit() preset 1 device was added second (index 1), preset 2 third (index 2) etc. } } else if (m == EspalexaDeviceProperty::off) { if (!macroAlexaOff) { if (bri > 0) { briLast = bri; bri = 0; stateUpdated(CALL_MODE_ALEXA); } } else { applyPreset(macroAlexaOff, CALL_MODE_ALEXA); // below for loop stops Alexa from complaining if macroAlexaOff does not actually turn off } for (unsigned i = 0; i < espalexa.getDeviceCount(); i++) { espalexa.getDevice(i)->setValue(0); } } else if (m == EspalexaDeviceProperty::bri) { bri = dev->getValue(); stateUpdated(CALL_MODE_ALEXA); } else //color { if (dev->getColorMode() == EspalexaColorMode::ct) //shade of white { byte rgbw[4]; uint16_t ct = dev->getCt(); if (!ct) return; uint16_t k = 1000000 / ct; //mireds to kelvin if (strip.hasCCTBus()) { bool hasManualWhite = strip.getActiveSegsLightCapabilities(true) & SEG_CAPABILITY_W; strip.setCCT(k); if (hasManualWhite) { rgbw[0] = 0; rgbw[1] = 0; rgbw[2] = 0; rgbw[3] = 255; } else { rgbw[0] = 255; rgbw[1] = 255; rgbw[2] = 255; rgbw[3] = 0; dev->setValue(255); } } else if (strip.hasWhiteChannel()) { switch (ct) { //these values empirically look good on RGBW case 199: rgbw[0]=255; rgbw[1]=255; rgbw[2]=255; rgbw[3]=255; break; case 234: rgbw[0]=127; rgbw[1]=127; rgbw[2]=127; rgbw[3]=255; break; case 284: rgbw[0]= 0; rgbw[1]= 0; rgbw[2]= 0; rgbw[3]=255; break; case 350: rgbw[0]=130; rgbw[1]= 90; rgbw[2]= 0; rgbw[3]=255; break; case 383: rgbw[0]=255; rgbw[1]=153; rgbw[2]= 0; rgbw[3]=255; break; default : colorKtoRGB(k, rgbw); } } else { colorKtoRGB(k, rgbw); } strip.getMainSegment().setColor(0, RGBW32(rgbw[0], rgbw[1], rgbw[2], rgbw[3])); } else { uint32_t color = dev->getRGB(); strip.getMainSegment().setColor(0, color); } stateUpdated(CALL_MODE_ALEXA); } } #else void alexaInit(){} void handleAlexa(){} #endif ================================================ FILE: wled00/bus_manager.cpp ================================================ /* * Class implementation for addressing various light types */ #include #include #ifdef ARDUINO_ARCH_ESP32 #include #include "src/dependencies/network/Network.h" // for isConnected() (& WiFi) #include "driver/ledc.h" #include "soc/ledc_struct.h" #endif #ifdef ESP8266 #include "core_esp8266_waveform.h" #endif #include "bus_manager.h" #include "bus_wrapper.h" #include "wled.h" // functions to get/set bits in an array - based on functions created by Brandon for GOL // toDo : make this a class that's completely defined in a header file // note: these functions are automatically inline by the compiler static inline bool getBitFromArray(const uint8_t* byteArray, size_t position) { // get bit value size_t byteIndex = position >> 3; // divide by 8 unsigned bitIndex = position & 0x07; // modulo 8 uint8_t byteValue = byteArray[byteIndex]; return (byteValue >> bitIndex) & 1; } static inline void setBitInArray(uint8_t* byteArray, size_t position, bool value) { // set bit - with error handling for nullptr //if (byteArray == nullptr) return; size_t byteIndex = position >> 3; // divide by 8 unsigned bitIndex = position & 0x07; // modulo 8 if (value) byteArray[byteIndex] |= (1 << bitIndex); else byteArray[byteIndex] &= ~(1 << bitIndex); } static inline size_t getBitArrayBytes(size_t num_bits) { // number of bytes needed for an array with num_bits bits return (num_bits + 7) >> 3; } static inline void setBitArray(uint8_t* byteArray, size_t numBits, bool value) { // set all bits to same value if (byteArray == nullptr) return; size_t len = getBitArrayBytes(numBits); if (value) memset(byteArray, 0xFF, len); else memset(byteArray, 0x00, len); } static ColorOrderMap _colorOrderMap = {}; bool ColorOrderMap::add(uint16_t start, uint16_t len, uint8_t colorOrder) { if (count() >= WLED_MAX_COLOR_ORDER_MAPPINGS || len == 0 || (colorOrder & 0x0F) > COL_ORDER_MAX) return false; // upper nibble contains W swap information _mappings.push_back({start,len,colorOrder}); DEBUGBUS_PRINTF_P(PSTR("Bus: Add COM (%d,%d,%d)\n"), (int)start, (int)len, (int)colorOrder); return true; } uint8_t IRAM_ATTR ColorOrderMap::getPixelColorOrder(uint16_t pix, uint8_t defaultColorOrder) const { // upper nibble contains W swap information // when ColorOrderMap's upper nibble contains value >0 then swap information is used from it, otherwise global swap is used for (const auto& map : _mappings) { if (pix >= map.start && pix < (map.start + map.len)) return map.colorOrder | ((map.colorOrder >> 4) ? 0 : (defaultColorOrder & 0xF0)); } return defaultColorOrder; } void Bus::calculateCCT(uint32_t c, uint8_t &ww, uint8_t &cw) { unsigned cct = 0; //0 - full warm white, 255 - full cold white unsigned w = W(c); if (_cct > -1) { // using RGB? if (_cct >= 1900) cct = (_cct - 1900) >> 5; // convert K in relative format else if (_cct < 256) cct = _cct; // already relative } else { cct = (approximateKelvinFromRGB(c) - 1900) >> 5; // convert K (from RGB value) to relative format } // CCT blending modes (_cctBlend): // blend<0: ww: ▓▓▒░__ | blend=0: ww: ▓▒▒░░ | blend>0 ww: ▓▓▓▒░ // cw: __░▒▓▓ | cw: ░░▒▒▓ | cw: ░▒▓▓▓ int32_t ww_val, cw_val; if (_cctBlend < 0) { uint16_t range = 255 - 2 * (uint16_t)(-_cctBlend); if (range > 255) range = 255; // prevent overflow ww_val = range ? ((int32_t)(255 + _cctBlend - cct) * 255) / range : (cct < 128 ? 255 : 0); // exclusive blending cw_val = 255 - ww_val; } else { ww_val = _cctBlend ? ((int32_t)(255 - cct) * 255) / (255 - _cctBlend) : 255 - cct; // additive blending cw_val = _cctBlend ? ((int32_t) cct * 255) / (255 - _cctBlend) : cct; } ww = (uint8_t)(ww_val < 0 ? 0 : ww_val > 255 ? 255 : ww_val); cw = (uint8_t)(cw_val < 0 ? 0 : cw_val > 255 ? 255 : cw_val); ww = (w * ww) / 255; //brightness scaling cw = (w * cw) / 255; } // calculates white channel and CCT values based on given settings uint32_t Bus::autoWhiteCalc(uint32_t c, uint8_t &ww, uint8_t &cw) const { unsigned aWM = _autoWhiteMode; if (_gAWM < AW_GLOBAL_DISABLED) aWM = _gAWM; CRGBW cIn = c; // save original color for CCT calculation unsigned w = W(c); if (aWM != RGBW_MODE_MANUAL_ONLY) { unsigned r = R(c); // note: using uint8_t generates larger code unsigned g = G(c); unsigned b = B(c); if (aWM == RGBW_MODE_DUAL && w > 0) { //ignore auto-white calculation if w>0 and mode DUAL (DUAL behaves as BRIGHTER if w==0) } else if (aWM == RGBW_MODE_MAX) { w = r > g ? (r > b ? r : b) : (g > b ? g : b); // brightest RGB channel } else { w = r < g ? (r < b ? r : b) : (g < b ? g : b); // darkest RGB channel if (aWM == RGBW_MODE_AUTO_ACCURATE) { r -= w; g -= w; b -= w; } //subtract w in ACCURATE mode } c = RGBW32(r, g, b, w); } if (_hasCCT) { cIn.w = w; // need original rgb values in case CCT is derived from RGB calculateCCT(cIn, ww, cw); } return c; } BusDigital::BusDigital(const BusConfig &bc) : Bus(bc.type, bc.start, bc.autoWhite, bc.count, bc.reversed, (bc.refreshReq || bc.type == TYPE_TM1814)) , _skip(bc.skipAmount) //sacrificial pixels , _colorOrder(bc.colorOrder) , _milliAmpsPerLed(bc.milliAmpsPerLed) , _milliAmpsMax(bc.milliAmpsMax) , _driverType(bc.driverType) // Store driver preference (0=RMT, 1=I2S) { DEBUGBUS_PRINTLN(F("Bus: Creating digital bus.")); if (!isDigital(bc.type) || !bc.count) { DEBUGBUS_PRINTLN(F("Not digial or empty bus!")); return; } _iType = bc.iType; // reuse the iType that was determined by polyBus in getI() in finalizeInit() if (_iType == I_NONE) { DEBUGBUS_PRINTLN(F("Incorrect iType!")); return; } if (!PinManager::allocatePin(bc.pins[0], true, PinOwner::BusDigital)) { DEBUGBUS_PRINTLN(F("Pin 0 allocated!")); return; } _frequencykHz = 0U; _colorSum = 0; _pins[0] = bc.pins[0]; if (is2Pin(bc.type)) { if (!PinManager::allocatePin(bc.pins[1], true, PinOwner::BusDigital)) { cleanup(); DEBUGBUS_PRINTLN(F("Pin 1 allocated!")); return; } _pins[1] = bc.pins[1]; _frequencykHz = bc.frequency ? bc.frequency : 2000U; // 2MHz clock if undefined } _hasRgb = hasRGB(bc.type); _hasWhite = hasWhite(bc.type); _hasCCT = hasCCT(bc.type); uint16_t lenToCreate = bc.count; if (bc.type == TYPE_WS2812_1CH_X3) lenToCreate = NUM_ICS_WS2812_1CH_3X(bc.count); // only needs a third of "RGB" LEDs for NeoPixelBus _busPtr = PolyBus::create(_iType, _pins, lenToCreate + _skip); _valid = (_busPtr != nullptr) && bc.count > 0; // fix for wled#4759 if (_valid) for (unsigned i = 0; i < _skip; i++) { PolyBus::setPixelColor(_busPtr, _iType, i, 0, COL_ORDER_GRB); // set sacrificial pixels to black (CO does not matter here) } else { cleanup(); } DEBUGBUS_PRINTF_P(PSTR("Bus len:%u, type:%u (RGB:%d, W:%d, CCT:%d), pins:%u,%u [itype:%u, driver:%s] mA=%d/%d %s\n"), (int)bc.count, (int)bc.type, (int)_hasRgb, (int)_hasWhite, (int)_hasCCT, (unsigned)_pins[0], is2Pin(bc.type)?(unsigned)_pins[1]:255U, (unsigned)_iType, isI2S() ? "I2S" : "RMT", (int)_milliAmpsPerLed, (int)_milliAmpsMax, _valid ? " " : "FAILED" ); } //DISCLAIMER //The following function attemps to calculate the current LED power usage, //and will limit the brightness to stay below a set amperage threshold. //It is NOT a measurement and NOT guaranteed to stay within the ablMilliampsMax margin. //Stay safe with high amperage and have a reasonable safety margin! //I am NOT to be held liable for burned down garages or houses! // note on ABL implementation: // ABL is set up in finalizeInit() // scaled color channels are summed in BusDigital::setPixelColor() // the used current is estimated and limited in BusManager::show() // if limit is set too low, brightness is limited to 1 to at least show some light // to disable brightness limiter for a bus, set LED current to 0 void BusDigital::estimateCurrent() { uint32_t actualMilliampsPerLed = _milliAmpsPerLed; if (_milliAmpsPerLed == 255) { // use wacky WS2815 power model, see WLED issue #549 _colorSum *= 3; // sum is sum of max value for each color, need to multiply by three to account for clrUnitsPerChannel being 3*255 actualMilliampsPerLed = 12; // from testing an actual strip } // _colorSum has all the values of color channels summed, max would be getLength()*(3*255 + (255 if hasWhite()): convert to milliAmps uint32_t clrUnitsPerChannel = hasWhite() ? 4*255 : 3*255; _milliAmpsTotal = ((uint64_t)_colorSum * actualMilliampsPerLed) / clrUnitsPerChannel + getLength(); // add 1mA standby current per LED to total (WS2812: ~0.7mA, WS2815: ~2mA) } void BusDigital::applyBriLimit(uint8_t newBri) { // a newBri of 0 means calculate per-bus brightness limit _NPBbri = 255; // reset, intermediate value is set below, final value is calculated in bus::show() if (newBri == 0) { if (_milliAmpsLimit == 0 || _milliAmpsTotal == 0) return; // ABL not used for this bus newBri = 255; if (_milliAmpsLimit > getLength()) { // each LED uses about 1mA in standby if (_milliAmpsTotal > _milliAmpsLimit) { // scale brightness down to stay in current limit newBri = ((uint32_t)_milliAmpsLimit * 255) / _milliAmpsTotal + 1; // +1 to avoid 0 brightness _milliAmpsTotal = _milliAmpsLimit; } } else { newBri = 1; // limit too low, set brightness to 1, this will dim down all colors to minimum since we use video scaling _milliAmpsTotal = getLength(); // estimate bus current as minimum } } if (newBri < 255) { _NPBbri = newBri; // store value so it can be updated in show() (must be updated even if ABL is not used) uint16_t wwcw = 0; unsigned hwLen = _len; if (_type == TYPE_WS2812_1CH_X3) hwLen = NUM_ICS_WS2812_1CH_3X(_len); // only needs a third of "RGB" LEDs for NeoPixelBus for (unsigned i = 0; i < hwLen; i++) { uint8_t co = _colorOrderMap.getPixelColorOrder(i+_start, _colorOrder); // need to revert color order for correct color scaling and CCT calc in case white is swapped uint32_t c = PolyBus::getPixelColor(_busPtr, _iType, i, co); // Note: if ABL would be calculated as a seperate loop (as it was before) it is slower but could use original color, making it more color-accurate if (hasCCT()) { uint8_t cctWW, cctCW; Bus::calculateCCT(c, cctWW, cctCW); // calculate CCT before fade (more accurate) | Note: if using "accurate" white calculation mode, approximateKelvinFromRGB can be very inaccurate (white is subtracted) wwcw = ((cctCW + 1) * newBri) & 0xFF00; // apply brightness to CCT (leave it in upper byte for 16bit NeoPixelBus value) wwcw |= ((cctWW + 1) * newBri) >> 8; } c = color_fade(c, newBri, true); // apply additional dimming note: using inline version is a bit faster but overhead of getPixelColor() dominates the speed impact by far PolyBus::setPixelColor(_busPtr, _iType, i, c, co, wwcw); // repaint all pixels with new brightness } } _colorSum = 0; // reset for next frame } void BusDigital::show() { if (!_valid) return; _NPBbri = (_NPBbri * _bri) / 255; // total applied brightness for use in restoreColorLossy (see applyBriLimit()) PolyBus::show(_busPtr, _iType, _skip); // faster if buffer consistency is not important (no skipped LEDs) } bool BusDigital::canShow() const { if (!_valid) return true; return PolyBus::canShow(_busPtr, _iType); } //If LEDs are skipped, it is possible to use the first as a status LED. //TODO only show if no new show due in the next 50ms void BusDigital::setStatusPixel(uint32_t c) { if (_valid && _skip) { PolyBus::setPixelColor(_busPtr, _iType, 0, c, _colorOrderMap.getPixelColorOrder(_start, _colorOrder)); if (canShow()) PolyBus::show(_busPtr, _iType); } } // note: using WLED_O2_ATTR makes this function ~7% faster at the expense of 600 bytes of flash void IRAM_ATTR BusDigital::setPixelColor(unsigned pix, uint32_t c) { if (!_valid) return; if (Bus::_cct >= 1900) c = colorBalanceFromKelvin(Bus::_cct, c); //color correction from CCT uint8_t cctWW = 0, cctCW = 0; uint16_t wwcw = 0; if (hasWhite()) c = autoWhiteCalc(c, cctWW, cctCW); c = color_fade(c, _bri, true); // apply brightness if (hasCCT()) { wwcw = ((cctCW + 1) * _bri) & 0xFF00; // apply brightness to CCT (store CW in upper byte) wwcw |= ((cctWW + 1) * _bri) >> 8; if (_type == TYPE_WS2812_WWA) c = RGBW32(wwcw, wwcw >> 8, 0, W(c)); // ww,cw, 0, w } if (BusManager::_useABL) { // if using ABL, sum all color channels to estimate current and limit brightness in show() uint8_t r = R(c), g = G(c), b = B(c); if (_milliAmpsPerLed < 255) { // normal ABL _colorSum += r + g + b + W(c); } else { // wacky WS2815 power model, ignore white channel, use max of RGB (issue #549) _colorSum += ((r > g) ? ((r > b) ? r : b) : ((g > b) ? g : b)); } } if (_reversed) pix = _len - pix -1; pix += _skip; const uint8_t co = _colorOrderMap.getPixelColorOrder(pix+_start, _colorOrder); if (_type == TYPE_WS2812_1CH_X3) { // map to correct IC, each controls 3 LEDs unsigned pOld = pix; pix = IC_INDEX_WS2812_1CH_3X(pix); uint32_t cOld = PolyBus::getPixelColor(_busPtr, _iType, pix, co); switch (pOld % 3) { // change only the single channel (TODO: this can cause loss because of get/set) case 0: c = RGBW32(R(cOld), W(c) , B(cOld), 0); break; case 1: c = RGBW32(W(c) , G(cOld), B(cOld), 0); break; case 2: c = RGBW32(R(cOld), G(cOld), W(c) , 0); break; } } PolyBus::setPixelColor(_busPtr, _iType, pix, c, co, wwcw); } // returns lossly restored color from bus uint32_t IRAM_ATTR BusDigital::getPixelColor(unsigned pix) const { if (!_valid) return 0; if (_reversed) pix = _len - pix -1; pix += _skip; const uint8_t co = _colorOrderMap.getPixelColorOrder(pix+_start, _colorOrder); uint32_t c = restoreColorLossy(PolyBus::getPixelColor(_busPtr, _iType, (_type==TYPE_WS2812_1CH_X3) ? IC_INDEX_WS2812_1CH_3X(pix) : pix, co),_NPBbri); if (_type == TYPE_WS2812_1CH_X3) { // map to correct IC, each controls 3 LEDs uint8_t r = R(c); uint8_t g = _reversed ? B(c) : G(c); // should G and B be switched if _reversed? uint8_t b = _reversed ? G(c) : B(c); switch (pix % 3) { // get only the single channel case 0: c = RGBW32(g, g, g, g); break; case 1: c = RGBW32(r, r, r, r); break; case 2: c = RGBW32(b, b, b, b); break; } } if (_type == TYPE_WS2812_WWA) { uint8_t w = R(c) | G(c); c = RGBW32(w, w, 0, w); } return c; } size_t BusDigital::getPins(uint8_t* pinArray) const { unsigned numPins = is2Pin(_type) + 1; if (pinArray) for (unsigned i = 0; i < numPins; i++) pinArray[i] = _pins[i]; return numPins; } size_t BusDigital::getBusSize() const { return sizeof(BusDigital) + (isOk() ? PolyBus::getDataSize(_busPtr, _iType) : 0); // does not include common I2S DMA buffer } void BusDigital::setColorOrder(uint8_t colorOrder) { // upper nibble contains W swap information if ((colorOrder & 0x0F) > 5) return; _colorOrder = colorOrder; } // credit @willmmiles & @netmindz https://github.com/wled/WLED/pull/4056 std::vector BusDigital::getLEDTypes() { return { {TYPE_WS2812_RGB, "D", PSTR("WS281x")}, {TYPE_SK6812_RGBW, "D", PSTR("SK6812/WS2814 RGBW")}, {TYPE_TM1814, "D", PSTR("TM1814")}, {TYPE_WS2811_400KHZ, "D", PSTR("400kHz")}, {TYPE_TM1829, "D", PSTR("TM1829")}, {TYPE_UCS8903, "D", PSTR("UCS8903")}, {TYPE_APA106, "D", PSTR("APA106/PL9823")}, {TYPE_TM1914, "D", PSTR("TM1914")}, {TYPE_FW1906, "D", PSTR("FW1906 GRBCW")}, {TYPE_UCS8904, "D", PSTR("UCS8904 RGBW")}, {TYPE_WS2805, "D", PSTR("WS2805 RGBCW")}, {TYPE_SM16825, "D", PSTR("SM16825 RGBCW")}, {TYPE_WS2812_1CH_X3, "D", PSTR("WS2811 White")}, //{TYPE_WS2812_2CH_X3, "D", PSTR("WS281x CCT")}, // not implemented {TYPE_WS2812_WWA, "D", PSTR("WS281x WWA")}, // amber ignored {TYPE_WS2801, "2P", PSTR("WS2801")}, {TYPE_APA102, "2P", PSTR("APA102")}, {TYPE_LPD8806, "2P", PSTR("LPD8806")}, {TYPE_LPD6803, "2P", PSTR("LPD6803")}, {TYPE_P9813, "2P", PSTR("PP9813")}, }; } bool BusDigital::isI2S() { return (_iType & 0x01) == 0; // I2S types have even iType values } void BusDigital::begin() { if (!_valid) return; PolyBus::begin(_busPtr, _iType, _pins, _frequencykHz); } void BusDigital::cleanup() { DEBUGBUS_PRINTLN(F("Digital Cleanup.")); PolyBus::cleanup(_busPtr, _iType); _iType = I_NONE; _valid = false; _busPtr = nullptr; PinManager::deallocatePin(_pins[1], PinOwner::BusDigital); PinManager::deallocatePin(_pins[0], PinOwner::BusDigital); } #ifdef ESP8266 // 1 MHz clock #define CLOCK_FREQUENCY 1000000UL #else // Use XTAL clock if possible to avoid timer frequency error when setting APB clock < 80 Mhz // https://github.com/espressif/arduino-esp32/blob/2.0.2/cores/esp32/esp32-hal-ledc.c #ifdef SOC_LEDC_SUPPORT_XTAL_CLOCK #define CLOCK_FREQUENCY 40000000UL #else #define CLOCK_FREQUENCY 80000000UL #endif #endif #ifdef ESP8266 #define MAX_BIT_WIDTH 10 #else #ifdef SOC_LEDC_TIMER_BIT_WIDE_NUM // C6/H2/P4: 20 bit, S2/S3/C2/C3: 14 bit #define MAX_BIT_WIDTH SOC_LEDC_TIMER_BIT_WIDE_NUM #else // ESP32: 20 bit (but in reality we would never go beyond 16 bit as the frequency would be to low) #define MAX_BIT_WIDTH 14 #endif #endif BusPwm::BusPwm(const BusConfig &bc) : Bus(bc.type, bc.start, bc.autoWhite, 1, bc.reversed, bc.refreshReq) // hijack Off refresh flag to indicate usage of dithering { if (!isPWM(bc.type)) return; const unsigned numPins = numPWMPins(bc.type); [[maybe_unused]] const bool dithering = _needsRefresh; _frequency = bc.frequency ? bc.frequency : WLED_PWM_FREQ; // duty cycle resolution (_depth) can be extracted from this formula: CLOCK_FREQUENCY > _frequency * 2^_depth for (_depth = MAX_BIT_WIDTH; _depth > 8; _depth--) if (((CLOCK_FREQUENCY/_frequency) >> _depth) > 0) break; managed_pin_type pins[numPins]; for (unsigned i = 0; i < numPins; i++) pins[i] = {(int8_t)bc.pins[i], true}; if (PinManager::allocateMultiplePins(pins, numPins, PinOwner::BusPwm)) { #ifdef ESP8266 analogWriteRange((1<<_depth)-1); analogWriteFreq(_frequency); #else // for 2 pin PWM CCT strip pinManager will make sure both LEDC channels are in the same speed group and sharing the same timer _ledcStart = PinManager::allocateLedc(numPins); if (_ledcStart == 255) { //no more free LEDC channels PinManager::deallocateMultiplePins(pins, numPins, PinOwner::BusPwm); DEBUGBUS_PRINTLN(F("No more free LEDC channels!")); return; } // if _needsRefresh is true (UI hack) we are using dithering (credit @dedehai & @zalatnaicsongor) if (dithering) _depth = 12; // fixed 8 bit depth PWM with 4 bit dithering (ESP8266 has no hardware to support dithering) #endif for (unsigned i = 0; i < numPins; i++) { _pins[i] = bc.pins[i]; // store only after allocateMultiplePins() succeeded #ifdef ESP8266 pinMode(_pins[i], OUTPUT); #else unsigned channel = _ledcStart + i; ledcSetup(channel, _frequency, _depth - (dithering*4)); // with dithering _frequency doesn't really matter as resolution is 8 bit ledcAttachPin(_pins[i], channel); // LEDC timer reset credit @dedehai uint8_t group = (channel / 8), timer = ((channel / 2) % 4); // same fromula as in ledcSetup() ledc_timer_rst((ledc_mode_t)group, (ledc_timer_t)timer); // reset timer so all timers are almost in sync (for phase shift) #endif } _hasRgb = hasRGB(bc.type); _hasWhite = hasWhite(bc.type); _hasCCT = hasCCT(bc.type); _valid = true; } DEBUGBUS_PRINTF_P(PSTR("%successfully inited PWM strip with type %u, frequency %u, bit depth %u and pins %u,%u,%u,%u,%u\n"), _valid?"S":"Uns", bc.type, _frequency, _depth, _pins[0], _pins[1], _pins[2], _pins[3], _pins[4]); } void BusPwm::setPixelColor(unsigned pix, uint32_t c) { if (pix != 0 || !_valid) return; //only react to first pixel if (Bus::_cct >= 1900 && (_type == TYPE_ANALOG_3CH || _type == TYPE_ANALOG_4CH)) { c = colorBalanceFromKelvin(Bus::_cct, c); //color correction from CCT } uint8_t cctWW, cctCW; if (_type != TYPE_ANALOG_3CH) c = autoWhiteCalc(c, cctWW, cctCW); uint8_t r = R(c), g = G(c), b = B(c), w = W(c); // note: no color scaling, brightness is applied in show() switch (_type) { case TYPE_ANALOG_1CH: //one channel (white), relies on auto white calculation _data[0] = w; break; case TYPE_ANALOG_2CH: //warm white + cold white if (cctICused) { _data[0] = w; _data[1] = Bus::_cct < 0 || Bus::_cct > 255 ? 127 : Bus::_cct; } else { _data[0] = cctWW; _data[1] = cctCW; } break; case TYPE_ANALOG_5CH: //RGB + warm white + cold white if (cctICused) _data[4] = Bus::_cct < 0 || Bus::_cct > 255 ? 127 : Bus::_cct; else { w = cctWW; _data[4] = cctCW; } // fall through to set RGBW channels case TYPE_ANALOG_4CH: //RGBW _data[3] = w; case TYPE_ANALOG_3CH: //standard dumb RGB _data[0] = r; _data[1] = g; _data[2] = b; break; } } //does no index check uint32_t BusPwm::getPixelColor(unsigned pix) const { if (!_valid) return 0; // TODO getting the reverse from CCT is involved (a quick approximation when CCT blending is ste to 0 implemented) switch (_type) { case TYPE_ANALOG_1CH: //one channel (white), relies on auto white calculation return RGBW32(0, 0, 0, _data[0]); case TYPE_ANALOG_2CH: //warm white + cold white if (cctICused) return RGBW32(0, 0, 0, _data[0]); else return RGBW32(0, 0, 0, _data[0] + _data[1]); case TYPE_ANALOG_5CH: //RGB + warm white + cold white if (cctICused) return RGBW32(_data[0], _data[1], _data[2], _data[3]); else return RGBW32(_data[0], _data[1], _data[2], _data[3] + _data[4]); case TYPE_ANALOG_4CH: //RGBW return RGBW32(_data[0], _data[1], _data[2], _data[3]); case TYPE_ANALOG_3CH: //standard dumb RGB return RGBW32(_data[0], _data[1], _data[2], 0); } return RGBW32(_data[0], _data[0], _data[0], _data[0]); } void BusPwm::show() { if (!_valid) return; const size_t numPins = getPins(); #ifdef ESP8266 const unsigned analogPeriod = F_CPU / _frequency; const unsigned maxBri = analogPeriod; // compute to clock cycle accuracy constexpr bool dithering = false; constexpr unsigned bitShift = 8; // 256 clocks for dead time, ~3us at 80MHz #else // if _needsRefresh is true (UI hack) we are using dithering (credit @dedehai & @zalatnaicsongor) // https://github.com/wled/WLED/pull/4115 and https://github.com/zalatnaicsongor/WLED/pull/1) const bool dithering = _needsRefresh; // avoid working with bitfield const unsigned maxBri = (1<<_depth); // possible values: 16384 (14), 8192 (13), 4096 (12), 2048 (11), 1024 (10), 512 (9) and 256 (8) const unsigned bitShift = dithering * 4; // if dithering, _depth is 12 bit but LEDC channel is set to 8 bit (using 4 fractional bits) #endif // use CIE brightness formula (linear + cubic) to approximate human eye perceived brightness // see: https://en.wikipedia.org/wiki/Lightness unsigned pwmBri = _bri; if (pwmBri < 21) { // linear response for values [0-20] pwmBri = (pwmBri * maxBri + 2300 / 2) / 2300 ; // adding '0.5' before division for correct rounding, 2300 gives a good match to CIE curve } else { // cubic response for values [21-255] float temp = float(pwmBri + 41) / float(255 + 41); // 41 is to match offset & slope to linear part temp = temp * temp * temp * (float)maxBri; pwmBri = (unsigned)temp; // pwmBri is in range [0-maxBri] C } [[maybe_unused]] unsigned hPoint = 0; // phase shift (0 - maxBri) // we will be phase shifting every channel by previous pulse length (plus dead time if required) // phase shifting is only mandatory when using H-bridge to drive reverse-polarity PWM CCT (2 wire) LED type // CCT additive blending must be 0 (WW & CW will not overlap) otherwise signals *will* overlap // for all other cases it will just try to "spread" the load on PSU // Phase shifting requires that LEDC timers are synchronised (see setup()). For PWM CCT (and H-bridge) it is // also mandatory that both channels use the same timer (pinManager takes care of that). for (unsigned i = 0; i < numPins; i++) { unsigned duty = (_data[i] * pwmBri) / 255; unsigned deadTime = 0; if (_type == TYPE_ANALOG_2CH && Bus::_cctBlend <= 0) { // add dead time between signals (when using dithering, two full 8bit pulses are required) deadTime = (1+dithering) << bitShift; // we only need to take care of shortening the signal at (almost) full brightness otherwise pulses may overlap if (_bri >= 254 && duty >= maxBri / 2 && duty < maxBri) { duty -= deadTime << 1; // shorten duty of larger signal except if full on } } if (_reversed) { if (i) hPoint += duty; // align start at time zero duty = maxBri - duty; } #ifdef ESP8266 //stopWaveform(_pins[i]); // can cause the waveform to miss a cycle. instead we risk crossovers. startWaveformClockCycles(_pins[i], duty, analogPeriod - duty, 0, i ? _pins[0] : -1, hPoint, false); #else unsigned channel = _ledcStart + i; unsigned gr = channel/8; // high/low speed group unsigned ch = channel%8; // group channel // directly write to LEDC struct as there is no HAL exposed function for dithering // duty has 20 bit resolution with 4 fractional bits (24 bits in total) LEDC.channel_group[gr].channel[ch].duty.duty = duty << ((!dithering)*4); // lowest 4 bits are used for dithering, shift by 4 bits if not using dithering LEDC.channel_group[gr].channel[ch].hpoint.hpoint = hPoint >> bitShift; // hPoint is at _depth resolution (needs shifting if dithering) ledc_update_duty((ledc_mode_t)gr, (ledc_channel_t)ch); #endif if (!_reversed) hPoint += duty; hPoint += deadTime; // offset to cascade the signals if (hPoint >= maxBri) hPoint -= maxBri; // offset is out of bounds, reset } } size_t BusPwm::getPins(uint8_t* pinArray) const { if (!_valid) return 0; unsigned numPins = numPWMPins(_type); if (pinArray) for (unsigned i = 0; i < numPins; i++) pinArray[i] = _pins[i]; return numPins; } // credit @willmmiles & @netmindz https://github.com/wled/WLED/pull/4056 std::vector BusPwm::getLEDTypes() { return { {TYPE_ANALOG_1CH, "A", PSTR("PWM White")}, {TYPE_ANALOG_2CH, "AA", PSTR("PWM CCT")}, {TYPE_ANALOG_3CH, "AAA", PSTR("PWM RGB")}, {TYPE_ANALOG_4CH, "AAAA", PSTR("PWM RGBW")}, {TYPE_ANALOG_5CH, "AAAAA", PSTR("PWM RGB+CCT")}, //{TYPE_ANALOG_6CH, "AAAAAA", PSTR("PWM RGB+DCCT")}, // unimplementable ATM }; } void BusPwm::deallocatePins() { size_t numPins = getPins(); for (unsigned i = 0; i < numPins; i++) { PinManager::deallocatePin(_pins[i], PinOwner::BusPwm); if (!PinManager::isPinOk(_pins[i])) continue; #ifdef ESP8266 digitalWrite(_pins[i], LOW); //turn off PWM interrupt #else if (_ledcStart < WLED_MAX_ANALOG_CHANNELS) ledcDetachPin(_pins[i]); #endif } #ifdef ARDUINO_ARCH_ESP32 PinManager::deallocateLedc(_ledcStart, numPins); #endif } BusOnOff::BusOnOff(const BusConfig &bc) : Bus(bc.type, bc.start, bc.autoWhite, 1, bc.reversed) , _data(0) { if (!Bus::isOnOff(bc.type)) return; uint8_t currentPin = bc.pins[0]; if (!PinManager::allocatePin(currentPin, true, PinOwner::BusOnOff)) { return; } _pin = currentPin; //store only after allocatePin() succeeds pinMode(_pin, OUTPUT); _hasRgb = false; _hasWhite = false; _hasCCT = false; _valid = true; DEBUGBUS_PRINTF_P(PSTR("%successfully inited On/Off strip with pin %u\n"), _valid?"S":"Uns", _pin); } void BusOnOff::setPixelColor(unsigned pix, uint32_t c) { if (pix != 0 || !_valid) return; //only react to first pixel _data = (c > 0) && bool(_bri) ? 0xFF : 0; // if any color channel is on and brightness is not zero, set to on } uint32_t BusOnOff::getPixelColor(unsigned pix) const { if (!_valid) return 0; return RGBW32(_data, _data, _data, _data); } void BusOnOff::show() { if (!_valid) return; digitalWrite(_pin, _reversed ? !(bool)_data : (bool)_data); } size_t BusOnOff::getPins(uint8_t* pinArray) const { if (!_valid) return 0; if (pinArray) pinArray[0] = _pin; return 1; } // credit @willmmiles & @netmindz https://github.com/wled/WLED/pull/4056 std::vector BusOnOff::getLEDTypes() { return { {TYPE_ONOFF, "", PSTR("On/Off")}, }; } BusNetwork::BusNetwork(const BusConfig &bc) : Bus(bc.type, bc.start, bc.autoWhite, bc.count) , _broadcastLock(false) { switch (bc.type) { case TYPE_NET_ARTNET_RGB: _UDPtype = 2; break; case TYPE_NET_ARTNET_RGBW: _UDPtype = 2; break; case TYPE_NET_E131_RGB: _UDPtype = 1; break; default: // TYPE_NET_DDP_RGB / TYPE_NET_DDP_RGBW _UDPtype = 0; break; } _hasRgb = hasRGB(bc.type); _hasWhite = hasWhite(bc.type); _hasCCT = false; _UDPchannels = _hasWhite + 3; _client = IPAddress(bc.pins[0],bc.pins[1],bc.pins[2],bc.pins[3]); #ifdef ARDUINO_ARCH_ESP32 _hostname = bc.text; resolveHostname(); // resolve hostname to IP address if needed #endif _data = (uint8_t*)d_calloc(_len, _UDPchannels); _valid = (_data != nullptr); DEBUGBUS_PRINTF_P(PSTR("%successfully inited virtual strip with type %u and IP %u.%u.%u.%u\n"), _valid?"S":"Uns", bc.type, bc.pins[0], bc.pins[1], bc.pins[2], bc.pins[3]); } void BusNetwork::setPixelColor(unsigned pix, uint32_t c) { if (!_valid || pix >= _len) return; uint8_t ww, cw; // dummy, unused if (_hasWhite) c = autoWhiteCalc(c, ww, cw); if (Bus::_cct >= 1900) c = colorBalanceFromKelvin(Bus::_cct, c); //color correction from CCT unsigned offset = pix * _UDPchannels; _data[offset] = R(c); _data[offset+1] = G(c); _data[offset+2] = B(c); if (_hasWhite) _data[offset+3] = W(c); } uint32_t BusNetwork::getPixelColor(unsigned pix) const { if (!_valid || pix >= _len) return 0; unsigned offset = pix * _UDPchannels; return RGBW32(_data[offset], _data[offset+1], _data[offset+2], (hasWhite() ? _data[offset+3] : 0)); } void BusNetwork::show() { if (!_valid || !canShow()) return; _broadcastLock = true; realtimeBroadcast(_UDPtype, _client, _len, _data, _bri, hasWhite()); _broadcastLock = false; } size_t BusNetwork::getPins(uint8_t* pinArray) const { if (pinArray) for (unsigned i = 0; i < 4; i++) pinArray[i] = _client[i]; return 4; } #ifdef ARDUINO_ARCH_ESP32 void BusNetwork::resolveHostname() { static unsigned long nextResolve = 0; if (Network.isConnected() && millis() > nextResolve && _hostname.length() > 0) { nextResolve = millis() + 600000; // resolve only every 10 minutes IPAddress clnt; if (strlen(cmDNS) > 0) clnt = MDNS.queryHost(_hostname); else WiFi.hostByName(_hostname.c_str(), clnt); if (clnt != IPAddress()) _client = clnt; } } #endif // credit @willmmiles & @netmindz https://github.com/wled/WLED/pull/4056 std::vector BusNetwork::getLEDTypes() { return { {TYPE_NET_DDP_RGB, "N", PSTR("DDP RGB (network)")}, // should be "NNNN" to determine 4 "pin" fields {TYPE_NET_ARTNET_RGB, "N", PSTR("Art-Net RGB (network)")}, {TYPE_NET_DDP_RGBW, "N", PSTR("DDP RGBW (network)")}, {TYPE_NET_ARTNET_RGBW, "N", PSTR("Art-Net RGBW (network)")}, // hypothetical extensions //{TYPE_VIRTUAL_I2C_W, "V", PSTR("I2C White (virtual)")}, // allows setting I2C address in _pin[0] //{TYPE_VIRTUAL_I2C_CCT, "V", PSTR("I2C CCT (virtual)")}, // allows setting I2C address in _pin[0] //{TYPE_VIRTUAL_I2C_RGB, "VVV", PSTR("I2C RGB (virtual)")}, // allows setting I2C address in _pin[0] and 2 additional values in _pin[1] & _pin[2] //{TYPE_USERMOD, "VVVVV", PSTR("Usermod (virtual)")}, // 5 data fields (see https://github.com/wled/WLED/pull/4123) }; } void BusNetwork::cleanup() { DEBUGBUS_PRINTLN(F("Virtual Cleanup.")); d_free(_data); _data = nullptr; _type = I_NONE; _valid = false; } // *************************************************************************** #ifdef WLED_ENABLE_HUB75MATRIX #warning "HUB75 driver enabled (experimental)" #ifdef ESP8266 #error ESP8266 does not support HUB75 #endif BusHub75Matrix::BusHub75Matrix(const BusConfig &bc) : Bus(bc.type, bc.start, bc.autoWhite) { size_t lastHeap = ESP.getFreeHeap(); _valid = false; _hasRgb = true; _hasWhite = false; virtualDisp = nullptr; // todo: this should be solved properly, can cause memory leak (if omitted here, nothing seems to work) // aliases for easier reading uint8_t panelWidth = bc.pins[0]; uint8_t panelHeight = bc.pins[1]; uint8_t chainLength = bc.pins[2]; _rows = bc.pins[3]; _cols = bc.pins[4]; mxconfig.double_buff = false; // Use our own memory-optimised buffer rather than the driver's own double-buffer // mxconfig.driver = HUB75_I2S_CFG::ICN2038S; // experimental - use specific shift register driver // mxconfig.driver = HUB75_I2S_CFG::FM6124; // try this driver in case you panel stays dark, or when colors look too pastel // mxconfig.latch_blanking = 3; // mxconfig.i2sspeed = HUB75_I2S_CFG::HZ_10M; // experimental - 5MHZ should be enugh, but colours looks slightly better at 10MHz // mxconfig.min_refresh_rate = 90; // mxconfig.min_refresh_rate = 120; mxconfig.clkphase = bc.reversed; // allow chain length up to 4, limit to prevent bad data from preventing boot due to low memory mxconfig.chain_length = max((uint8_t) 1, min(chainLength, (uint8_t) 4)); if (mxconfig.mx_height >= 64 && (mxconfig.chain_length > 1)) { DEBUGBUS_PRINTLN(F("WARNING, only single panel can be used of 64 pixel boards due to memory")); mxconfig.chain_length = 1; } if (bc.type == TYPE_HUB75MATRIX_HS) { mxconfig.mx_width = min((uint8_t) 64, panelWidth); // TODO: UI limit is 128, this limits to 64 mxconfig.mx_height = min((uint8_t) 64, panelHeight); } else if (bc.type == TYPE_HUB75MATRIX_QS) { _isVirtual = true; mxconfig.mx_width = min((uint8_t) 64, panelWidth) * 2; mxconfig.mx_height = min((uint8_t) 64, panelHeight) / 2; } else { DEBUGBUS_PRINTLN("Unknown type"); return; } #if defined(CONFIG_IDF_TARGET_ESP32) || defined(CONFIG_IDF_TARGET_ESP32S2)// classic esp32, or esp32-s2: reduce bitdepth for large panels if (mxconfig.mx_height >= 64) { if (mxconfig.chain_length * mxconfig.mx_width > 192) mxconfig.setPixelColorDepthBits(3); else if (mxconfig.chain_length * mxconfig.mx_width > 64) mxconfig.setPixelColorDepthBits(4); else mxconfig.setPixelColorDepthBits(8); } else mxconfig.setPixelColorDepthBits(8); #endif // HUB75_I2S_CFG::i2s_pins _pins={R1_PIN, G1_PIN, B1_PIN, R2_PIN, G2_PIN, B2_PIN, A_PIN, B_PIN, C_PIN, D_PIN, E_PIN, LAT_PIN, OE_PIN, CLK_PIN}; #if defined(ARDUINO_ADAFRUIT_MATRIXPORTAL_ESP32S3) // MatrixPortal ESP32-S3 // https://www.adafruit.com/product/5778 DEBUGBUS_PRINTLN("MatrixPanel_I2S_DMA - Matrix Portal S3 config"); mxconfig.gpio = { 42, 41, 40, 38, 39, 37, 45, 36, 48, 35, 21, 47, 14, 2 }; #elif defined(CONFIG_IDF_TARGET_ESP32S3) && defined(BOARD_HAS_PSRAM)// ESP32-S3 with PSRAM #if defined(MOONHUB_S3_PINOUT) DEBUGBUS_PRINTLN("MatrixPanel_I2S_DMA - T7 S3 with PSRAM, MOONHUB pinout"); // HUB75_I2S_CFG::i2s_pins _pins={R1_PIN, G1_PIN, B1_PIN, R2_PIN, G2_PIN, B2_PIN, A_PIN, B_PIN, C_PIN, D_PIN, E_PIN, LAT_PIN, OE_PIN, CLK_PIN}; mxconfig.gpio = { 1, 5, 6, 7, 13, 9, 16, 48, 47, 21, 38, 8, 4, 18 }; #else DEBUGBUS_PRINTLN("MatrixPanel_I2S_DMA - S3 with PSRAM"); // HUB75_I2S_CFG::i2s_pins _pins={R1_PIN, G1_PIN, B1_PIN, R2_PIN, G2_PIN, B2_PIN, A_PIN, B_PIN, C_PIN, D_PIN, E_PIN, LAT_PIN, OE_PIN, CLK_PIN}; mxconfig.gpio = {1, 2, 42, 41, 40, 39, 45, 48, 47, 21, 38, 8, 3, 18}; #endif #elif defined(ESP32_FORUM_PINOUT) // Common format for boards designed for SmartMatrix DEBUGBUS_PRINTLN("MatrixPanel_I2S_DMA - ESP32_FORUM_PINOUT"); /* ESP32 with SmartMatrix's default pinout - ESP32_FORUM_PINOUT https://github.com/pixelmatix/SmartMatrix/blob/teensylc/src/MatrixHardware_ESP32_V0.h Can use a board like https://github.com/rorosaurus/esp32-hub75-driver */ mxconfig.gpio = { 2, 15, 4, 16, 27, 17, 5, 18, 19, 21, 12, 26, 25, 22 }; #else DEBUGBUS_PRINTLN("MatrixPanel_I2S_DMA - Default pins"); /* https://github.com/mrfaptastic/ESP32-HUB75-MatrixPanel-DMA?tab=readme-ov-file Boards https://esp32trinity.com/ https://www.electrodragon.com/product/rgb-matrix-panel-drive-interface-board-for-esp32-dma/ */ mxconfig.gpio = { 25, 26, 27, 14, 12, 13, 23, 19, 5, 17, 18, 4, 15, 16 }; #endif int8_t pins[PIN_COUNT]; memcpy(pins, &mxconfig.gpio, sizeof(mxconfig.gpio)); if (!PinManager::allocateMultiplePins(pins, PIN_COUNT, PinOwner::HUB75, true)) { DEBUGBUS_PRINTLN("Failed to allocate pins for HUB75"); return; } if (bc.colorOrder == COL_ORDER_RGB) { DEBUGBUS_PRINTLN("MatrixPanel_I2S_DMA = Default color order (RGB)"); } else if (bc.colorOrder == COL_ORDER_BGR) { DEBUGBUS_PRINTLN("MatrixPanel_I2S_DMA = color order BGR"); int8_t tmpPin; tmpPin = mxconfig.gpio.r1; mxconfig.gpio.r1 = mxconfig.gpio.b1; mxconfig.gpio.b1 = tmpPin; tmpPin = mxconfig.gpio.r2; mxconfig.gpio.r2 = mxconfig.gpio.b2; mxconfig.gpio.b2 = tmpPin; } else { DEBUGBUS_PRINTF("MatrixPanel_I2S_DMA = unsupported color order %u\n", bc.colorOrder); } DEBUGBUS_PRINTF("MatrixPanel_I2S_DMA config - %ux%u length: %u\n", mxconfig.mx_width, mxconfig.mx_height, mxconfig.chain_length); DEBUGBUS_PRINTF("R1_PIN=%u, G1_PIN=%u, B1_PIN=%u, R2_PIN=%u, G2_PIN=%u, B2_PIN=%u, A_PIN=%u, B_PIN=%u, C_PIN=%u, D_PIN=%u, E_PIN=%u, LAT_PIN=%u, OE_PIN=%u, CLK_PIN=%u\n", mxconfig.gpio.r1, mxconfig.gpio.g1, mxconfig.gpio.b1, mxconfig.gpio.r2, mxconfig.gpio.g2, mxconfig.gpio.b2, mxconfig.gpio.a, mxconfig.gpio.b, mxconfig.gpio.c, mxconfig.gpio.d, mxconfig.gpio.e, mxconfig.gpio.lat, mxconfig.gpio.oe, mxconfig.gpio.clk); // OK, now we can create our matrix object display = new MatrixPanel_I2S_DMA(mxconfig); if (display == nullptr) { DEBUGBUS_PRINTLN("****** MatrixPanel_I2S_DMA !KABOOM! driver allocation failed ***********"); DEBUGBUS_PRINT(F("heap usage: ")); DEBUGBUS_PRINTLN(lastHeap - ESP.getFreeHeap()); return; } this->_len = (display->width() * display->height()); // note: this returns correct number of pixels but incorrect dimensions if using virtual display (updated below) DEBUGBUS_PRINTF("Length: %u\n", _len); if (this->_len >= MAX_LEDS) { DEBUGBUS_PRINTLN("MatrixPanel_I2S_DMA Too many LEDS - playing safe"); return; } DEBUGBUS_PRINTLN("MatrixPanel_I2S_DMA created"); // as noted in HUB75_I2S_DMA library, some panels can show ghosting if set higher than 239, so let users override at compile time #ifndef WLED_HUB75_MAX_BRIGHTNESS #define WLED_HUB75_MAX_BRIGHTNESS 255 #endif // let's adjust default brightness (128), brightness scaling is handled by WLED //display->setBrightness8(WLED_HUB75_MAX_BRIGHTNESS); // range is 0-255, 0 - 0%, 255 - 100% delay(24); // experimental DEBUGBUS_PRINT(F("heap usage: ")); DEBUGBUS_PRINTLN(lastHeap - ESP.getFreeHeap()); // Allocate memory and start DMA display if (!display->begin() ) { DEBUGBUS_PRINTLN("****** MatrixPanel_I2S_DMA !KABOOM! I2S memory allocation failed ***********"); DEBUGBUS_PRINT(F("heap usage: ")); DEBUGBUS_PRINTLN(lastHeap - ESP.getFreeHeap()); return; } else { DEBUGBUS_PRINTLN("MatrixPanel_I2S_DMA begin ok"); DEBUGBUS_PRINT(F("heap usage: ")); DEBUGBUS_PRINTLN(lastHeap - ESP.getFreeHeap()); delay(18); // experiment - give the driver a moment (~ one full frame @ 60hz) to settle _valid = true; display->clearScreen(); // initially clear the screen buffer DEBUGBUS_PRINTLN("MatrixPanel_I2S_DMA clear ok"); if (_ledBuffer) d_free(_ledBuffer); // should not happen if (_ledsDirty) d_free(_ledsDirty); // should not happen DEBUGBUS_PRINTLN("MatrixPanel_I2S_DMA allocate memory"); _ledsDirty = (byte*) d_malloc(getBitArrayBytes(_len)); // create LEDs dirty bits DEBUGBUS_PRINTLN("MatrixPanel_I2S_DMA allocate memory ok"); if (_ledsDirty == nullptr) { display->stopDMAoutput(); delete display; display = nullptr; _valid = false; DEBUGBUS_PRINTLN(F("MatrixPanel_I2S_DMA not started - not enough memory for dirty bits!")); DEBUGBUS_PRINT(F("heap usage: ")); DEBUGBUS_PRINTLN(lastHeap - ESP.getFreeHeap()); return; // fail is we cannot get memory for the buffer } setBitArray(_ledsDirty, _len, false); // reset dirty bits if (mxconfig.double_buff == false) { // create LEDs buffer (initialized to BLACK), prefer DRAM if enough heap is available (faster in case global _pixels buffer is in PSRAM as not both will fit the cache) _ledBuffer = static_cast(allocate_buffer(_len * sizeof(CRGB), BFRALLOC_PREFER_DRAM | BFRALLOC_CLEAR)); } } PANEL_CHAIN_TYPE chainType = CHAIN_NONE; // default for quarter-scan panels that do not use chaining // chained panels with cols and rows define need the virtual display driver, so do quarter-scan panels if (chainLength > 1 && (_rows > 1 || _cols > 1) || bc.type == TYPE_HUB75MATRIX_QS) { _isVirtual = true; chainType = CHAIN_BOTTOM_LEFT_UP; // TODO: is there any need to support other chaining types? DEBUGBUS_PRINTF_P(PSTR("Using virtual matrix: %ux%u panels of %ux%u pixels\n"), _cols, _rows, mxconfig.mx_width, mxconfig.mx_height); } else { _isVirtual = false; } if (_isVirtual) { virtualDisp = new VirtualMatrixPanel((*display), _rows, _cols, mxconfig.mx_width, mxconfig.mx_height, chainType); virtualDisp->setRotation(0); if (bc.type == TYPE_HUB75MATRIX_QS) { switch(panelHeight) { case 16: virtualDisp->setPhysicalPanelScanRate(FOUR_SCAN_16PX_HIGH); break; case 32: virtualDisp->setPhysicalPanelScanRate(FOUR_SCAN_32PX_HIGH); break; case 64: virtualDisp->setPhysicalPanelScanRate(FOUR_SCAN_64PX_HIGH); break; default: DEBUGBUS_PRINTLN("Unsupported height"); cleanup(); return; } } } if (_valid) { _panelWidth = virtualDisp ? virtualDisp->width() : display->width(); // cache width - it will never change } DEBUGBUS_PRINT(F("MatrixPanel_I2S_DMA ")); DEBUGBUS_PRINTF_P(PSTR("%sstarted, width=%u, %u pixels.\n"), _valid? "":"not ", _panelWidth, _len); if (_ledBuffer != nullptr) DEBUGBUS_PRINTLN(F("MatrixPanel_I2S_DMA LEDS buffer enabled.")); if (_ledsDirty != nullptr) DEBUGBUS_PRINTLN(F("MatrixPanel_I2S_DMA LEDS dirty bit optimization enabled.")); if ((_ledBuffer != nullptr) || (_ledsDirty != nullptr)) { DEBUGBUS_PRINT(F("MatrixPanel_I2S_DMA LEDS buffer uses ")); DEBUGBUS_PRINT((_ledBuffer? _len*sizeof(CRGB) :0) + (_ledsDirty? getBitArrayBytes(_len) :0)); DEBUGBUS_PRINTLN(F(" bytes.")); } } void IRAM_ATTR BusHub75Matrix::setPixelColor(unsigned pix, uint32_t c) { if (!_valid) return; // note: no need to check pix >= _len as that is checked in containsPixel() // if (_cct >= 1900) c = colorBalanceFromKelvin(_cct, c); //color correction from CCT if (_ledBuffer) { CRGB fastled_col = CRGB(c); if (_ledBuffer[pix] != fastled_col) { _ledBuffer[pix] = fastled_col; setBitInArray(_ledsDirty, pix, true); // flag pixel as "dirty" } } else { if ((c == IS_BLACK) && (getBitFromArray(_ledsDirty, pix) == false)) return; // ignore black if pixel is already black setBitInArray(_ledsDirty, pix, c != IS_BLACK); // dirty = true means "color is not BLACK" uint8_t r = R(c); uint8_t g = G(c); uint8_t b = B(c); if (virtualDisp != nullptr) { int x = pix % _panelWidth; // TODO: check if using & and shift would be faster here, it limits to power-of-2 widths though int y = pix / _panelWidth; virtualDisp->drawPixelRGB888(int16_t(x), int16_t(y), r, g, b); } else { int x = pix % _panelWidth; int y = pix / _panelWidth; display->drawPixelRGB888(int16_t(x), int16_t(y), r, g, b); } } } uint32_t BusHub75Matrix::getPixelColor(unsigned pix) const { if (!_valid) return IS_BLACK; // note: no need to check pix >= _len as that is checked in containsPixel() if (_ledBuffer) return uint32_t(_ledBuffer[pix]); else return getBitFromArray(_ledsDirty, pix) ? IS_DARKGREY: IS_BLACK; // just a hack - we only know if the pixel is black or not } void BusHub75Matrix::setBrightness(uint8_t b) { _bri = b; display->setBrightness(_bri); } void BusHub75Matrix::show(void) { if (!_valid) return; if (_ledBuffer) { // write out buffered LEDs unsigned height = _isVirtual ? virtualDisp->height() : display->height(); unsigned width = _panelWidth; //while(!previousBufferFree) delay(1); // experimental - Wait before we allow any writing to the buffer. Stop flicker. size_t pix = 0; // running pixel index for (int y=0; ydrawPixelRGB888(int16_t(x), int16_t(y), c.r, c.g, c.b); else display->drawPixelRGB888(int16_t(x), int16_t(y), c.r, c.g, c.b); } pix++; } setBitArray(_ledsDirty, _len, false); // buffer shown - reset all dirty bits } } void BusHub75Matrix::cleanup() { if (display && _valid) display->stopDMAoutput(); // terminate DMA driver (display goes black) _valid = false; delay(30); // give some time to finish DMA _panelWidth = 0; deallocatePins(); DEBUGBUS_PRINTLN(F("HUB75 output ended.")); #ifndef CONFIG_IDF_TARGET_ESP32S3 // on ESP32-S3 deleting display/virtualDisp does not work and leads to crash (DMA issues), request reboot from user instead if (virtualDisp != nullptr) delete virtualDisp; // note: in MM there is a warning to not do this but if using "NO_GFX" this is safe if (display != nullptr) delete display; display = nullptr; virtualDisp = nullptr; // note: when not using "NO_GFX" this causes a memory leak #endif if (_ledBuffer != nullptr) d_free(_ledBuffer); _ledBuffer = nullptr; if (_ledsDirty != nullptr) d_free(_ledsDirty); _ledsDirty = nullptr; } void BusHub75Matrix::deallocatePins() { uint8_t pins[PIN_COUNT]; memcpy(pins, &mxconfig.gpio, sizeof(mxconfig.gpio)); PinManager::deallocateMultiplePins(pins, PIN_COUNT, PinOwner::HUB75); } std::vector BusHub75Matrix::getLEDTypes() { return { {TYPE_HUB75MATRIX_HS, "H", PSTR("HUB75 (Half Scan)")}, {TYPE_HUB75MATRIX_QS, "H", PSTR("HUB75 (Quarter Scan)")}, }; } size_t BusHub75Matrix::getPins(uint8_t* pinArray) const { if (pinArray) { pinArray[0] = mxconfig.mx_width; pinArray[1] = mxconfig.mx_height; pinArray[2] = mxconfig.chain_length; pinArray[3] = _rows; pinArray[4] = _cols; } return 5; } #endif // *************************************************************************** BusPlaceholder::BusPlaceholder(const BusConfig &bc) : Bus(bc.type, bc.start, bc.autoWhite, bc.count, bc.reversed, bc.refreshReq) , _colorOrder(bc.colorOrder) , _skipAmount(bc.skipAmount) , _driverType(bc.driverType) , _frequency(bc.frequency) , _milliAmpsPerLed(bc.milliAmpsPerLed) , _milliAmpsMax(bc.milliAmpsMax) , _text(bc.text) { memcpy(_pins, bc.pins, sizeof(_pins)); } size_t BusPlaceholder::getPins(uint8_t* pinArray) const { size_t nPins = Bus::getNumberOfPins(_type); if (pinArray) { for (size_t i = 0; i < nPins; i++) pinArray[i] = _pins[i]; } return nPins; } //utility to get the approx. memory usage of a given BusConfig inclduding segmentbuffer and global buffer (4 bytes per pixel) size_t BusConfig::memUsage() const { size_t mem = (count + skipAmount) * 8; // 8 bytes per pixel for segment + global buffer if (Bus::isVirtual(type)) { mem += sizeof(BusNetwork) + (count * Bus::getNumberOfChannels(type)); // note: getNumberOfChannels() includes CCT channel if applicable but virtual buses do not use CCT channel buffer } else if (Bus::isDigital(type)) { // if any of digital buses uses I2S, there is additional common I2S DMA buffer not accounted for here mem += sizeof(BusDigital) + PolyBus::memUsage(count + skipAmount, iType); } else if (Bus::isOnOff(type)) { mem += sizeof(BusOnOff); } else { mem += sizeof(BusPwm); } return mem; } int BusManager::add(const BusConfig &bc, bool placeholder) { DEBUGBUS_PRINTF_P(PSTR("Bus: Adding bus (p:%d v:%d)\n"), getNumBusses(), getNumVirtualBusses()); unsigned digital = 0; unsigned analog = 0; unsigned twoPin = 0; for (const auto &bus : busses) { if (bus->isPWM()) analog += bus->getPins(); // number of analog channels used if (bus->isDigital() && !bus->is2Pin()) digital++; if (bus->is2Pin()) twoPin++; } digital += (Bus::isDigital(bc.type) && !Bus::is2Pin(bc.type)); analog += (Bus::isPWM(bc.type) ? Bus::numPWMPins(bc.type) : 0); if (digital > WLED_MAX_DIGITAL_CHANNELS || analog > WLED_MAX_ANALOG_CHANNELS) placeholder = true; // TODO: add errorFlag here if (placeholder) { busses.push_back(make_unique(bc)); } else if (Bus::isVirtual(bc.type)) { busses.push_back(make_unique(bc)); #ifdef WLED_ENABLE_HUB75MATRIX } else if (Bus::isHub75(bc.type)) { busses.push_back(make_unique(bc)); #endif } else if (Bus::isDigital(bc.type)) { busses.push_back(make_unique(bc)); } else if (Bus::isOnOff(bc.type)) { busses.push_back(make_unique(bc)); } else { busses.push_back(make_unique(bc)); } return busses.size(); } // credit @willmmiles static String LEDTypesToJson(const std::vector& types) { String json; for (const auto &type : types) { // capabilities follows similar pattern as JSON API int capabilities = Bus::hasRGB(type.id) | Bus::hasWhite(type.id)<<1 | Bus::hasCCT(type.id)<<2 | Bus::is16bit(type.id)<<4 | Bus::mustRefresh(type.id)<<5; char str[256]; sprintf_P(str, PSTR("{i:%d,c:%d,t:\"%s\",n:\"%s\"},"), type.id, capabilities, type.type, type.name); json += str; } return json; } // credit @willmmiles & @netmindz https://github.com/wled/WLED/pull/4056 String BusManager::getLEDTypesJSONString() { String json = "["; json += LEDTypesToJson(BusDigital::getLEDTypes()); json += LEDTypesToJson(BusOnOff::getLEDTypes()); json += LEDTypesToJson(BusPwm::getLEDTypes()); json += LEDTypesToJson(BusNetwork::getLEDTypes()); //json += LEDTypesToJson(BusVirtual::getLEDTypes()); #ifdef WLED_ENABLE_HUB75MATRIX json += LEDTypesToJson(BusHub75Matrix::getLEDTypes()); #endif json.setCharAt(json.length()-1, ']'); // replace last comma with bracket return json; } uint8_t BusManager::getI(uint8_t busType, const uint8_t* pins, uint8_t driverPreference) { return PolyBus::getI(busType, pins, driverPreference); } //do not call this method from system context (network callback) void BusManager::removeAll() { DEBUGBUS_PRINTLN(F("Removing all.")); //prevents crashes due to deleting busses while in use. while (!canAllShow()) yield(); busses.clear(); #ifndef ESP8266 // Reset channel tracking for fresh allocation PolyBus::resetChannelTracking(); #endif } #ifdef ESP32_DATA_IDLE_HIGH // #2478 // If enabled, RMT idle level is set to HIGH when off // to prevent leakage current when using an N-channel MOSFET to toggle LED power // since I2S outputs are known only during config of buses, lets just assume RMT is used for digital buses // unused RMT channels should have no effect void BusManager::esp32RMTInvertIdle() { bool idle_out; unsigned rmt = 0; unsigned u = 0; for (auto &bus : busses) { if (bus->getLength()==0 || !bus->isDigital() || bus->is2Pin()) continue; if (static_cast(bus.get())->isI2S()) continue; if (u >= WLED_MAX_RMT_CHANNELS) return; //assumes that bus number to rmt channel mapping stays 1:1 rmt_channel_t ch = static_cast(rmt); rmt_idle_level_t lvl; rmt_get_idle_level(ch, &idle_out, &lvl); if (lvl == RMT_IDLE_LEVEL_HIGH) lvl = RMT_IDLE_LEVEL_LOW; else if (lvl == RMT_IDLE_LEVEL_LOW) lvl = RMT_IDLE_LEVEL_HIGH; else continue; rmt_set_idle_level(ch, idle_out, lvl); u++; } } #endif void BusManager::on() { #ifdef ESP8266 //Fix for turning off onboard LED breaking bus if (PinManager::getPinOwner(LED_BUILTIN) == PinOwner::BusDigital) { for (auto &bus : busses) { uint8_t pins[2] = {255,255}; if (bus->isDigital() && bus->getPins(pins) && bus->isOk()) { if (pins[0] == LED_BUILTIN || pins[1] == LED_BUILTIN) { BusDigital &b = static_cast(*bus); b.begin(); break; } } } } #else for (auto &bus : busses) if (bus->isVirtual()) { // virtual/network bus should check for IP change if hostname is specified // otherwise there are no endpoints to force DNS resolution BusNetwork &b = static_cast(*bus); b.resolveHostname(); } #endif #ifdef ESP32_DATA_IDLE_HIGH esp32RMTInvertIdle(); #endif } void BusManager::off() { #ifdef ESP8266 // turn off built-in LED if strip is turned off // this will break digital bus so will need to be re-initialised on On if (PinManager::getPinOwner(LED_BUILTIN) == PinOwner::BusDigital) { for (const auto &bus : busses) if (bus->isOffRefreshRequired()) return; pinMode(LED_BUILTIN, OUTPUT); digitalWrite(LED_BUILTIN, HIGH); } #endif #ifdef ESP32_DATA_IDLE_HIGH esp32RMTInvertIdle(); #endif _gMilliAmpsUsed = 0; // reset, assume no LED idle current if relay is off } void BusManager::show() { applyABL(); // apply brightness limit, updates _gMilliAmpsUsed for (auto &bus : busses) { bus->show(); } } void IRAM_ATTR BusManager::setPixelColor(unsigned pix, uint32_t c) { for (auto &bus : busses) { if (!bus->containsPixel(pix)) continue; bus->setPixelColor(pix - bus->getStart(), c); } } void BusManager::setSegmentCCT(int16_t cct, bool allowWBCorrection) { if (cct > 255) cct = 255; if (cct >= 0) { //if white balance correction allowed, save as kelvin value instead of 0-255 if (allowWBCorrection) cct = 1900 + (cct << 5); } else cct = -1; // will use kelvin approximation from RGB Bus::setCCT(cct); } uint32_t BusManager::getPixelColor(unsigned pix) { for (auto &bus : busses) { if (!bus->containsPixel(pix)) continue; return bus->getPixelColor(pix - bus->getStart()); } return 0; } bool BusManager::canAllShow() { for (const auto &bus : busses) if (!bus->canShow()) return false; return true; } void BusManager::initializeABL() { _useABL = false; // reset if (_gMilliAmpsMax > 0) { // check global brightness limit for (auto &bus : busses) { if (bus->isDigital() && bus->getLEDCurrent() > 0) { _useABL = true; // at least one bus has valid LED current return; } } } else { // check per bus brightness limit unsigned numABLbuses = 0; for (auto &bus : busses) { if (bus->isDigital() && bus->getLEDCurrent() > 0 && bus->getMaxCurrent() > 0) numABLbuses++; // count ABL enabled buses } if (numABLbuses > 0) { _useABL = true; // at least one bus has ABL set uint32_t ESPshare = MA_FOR_ESP / numABLbuses; // share of ESP current per ABL bus for (auto &bus : busses) { if (bus->isDigital() && bus->isOk()) { BusDigital &busd = static_cast(*bus); uint32_t busLength = busd.getLength(); uint32_t busDemand = busLength * busd.getLEDCurrent(); uint32_t busMax = busd.getMaxCurrent(); if (busMax > ESPshare) busMax -= ESPshare; if (busMax < busLength) busMax = busLength; // give each LED 1mA, ABL will dim down to minimum if (busDemand == 0) busMax = 0; // no LED current set, disable ABL for this bus busd.setCurrentLimit(busMax); } } } } } void BusManager::applyABL() { if (_useABL) { unsigned milliAmpsSum = 0; // use temporary variable to always return a valid _gMilliAmpsUsed to UI unsigned totalLEDs = 0; for (auto &bus : busses) { if (bus->isDigital() && bus->isOk()) { BusDigital &busd = static_cast(*bus); busd.estimateCurrent(); // sets _milliAmpsTotal, current is estimated for all buses even if they have the limit set to 0 if (_gMilliAmpsMax == 0) busd.applyBriLimit(0); // apply per bus ABL limit, updates _milliAmpsTotal if limit reached milliAmpsSum += busd.getUsedCurrent(); totalLEDs += busd.getLength(); // sum total number of LEDs for global Limit } } // check global current limit and apply global ABL limit, total current is summed above if (_gMilliAmpsMax > 0) { uint8_t newBri = 255; uint32_t globalMax = _gMilliAmpsMax > MA_FOR_ESP ? _gMilliAmpsMax - MA_FOR_ESP : 1; // subtract ESP current consumption, fully limit if too low if (globalMax > totalLEDs) { // check if budget is larger than standby current if (milliAmpsSum > globalMax) { newBri = globalMax * 255 / milliAmpsSum + 1; // scale brightness down to stay in current limit, +1 to avoid 0 brightness milliAmpsSum = globalMax; // update total used current } } else { newBri = 1; // limit too low, set brightness to minimum milliAmpsSum = totalLEDs; // estimate total used current as minimum } // apply brightness limit to each bus, if its 255 it will only reset _colorSum for (auto &bus : busses) { if (bus->isDigital() && bus->isOk()) { BusDigital &busd = static_cast(*bus); if (busd.getLEDCurrent() > 0) // skip buses with LED current set to 0 busd.applyBriLimit(newBri); } } } _gMilliAmpsUsed = milliAmpsSum; } else _gMilliAmpsUsed = 0; // reset, we have no current estimation without ABL } ColorOrderMap& BusManager::getColorOrderMap() { return _colorOrderMap; } #ifndef ESP8266 // PolyBus channel tracking for dynamic allocation bool PolyBus::_useParallelI2S = false; uint8_t PolyBus::_rmtChannelsAssigned = 0; // number of RMT channels assigned durig getI() check uint8_t PolyBus::_rmtChannel = 0; // number of RMT channels actually used during bus creation in create() uint8_t PolyBus::_i2sChannelsAssigned = 0; // number of I2S channels assigned durig getI() check uint8_t PolyBus::_parallelBusItype = 0; // type I_NONE uint8_t PolyBus::_2PchannelsAssigned = 0; #endif // Bus static member definition int16_t Bus::_cct = -1; // -1 means use approximateKelvinFromRGB(), 0-255 is standard, >1900 use colorBalanceFromKelvin() int8_t Bus::_cctBlend = 0; // -128 to +127 uint8_t Bus::_gAWM = 255; uint16_t BusDigital::_milliAmpsTotal = 0; std::vector> BusManager::busses; uint16_t BusManager::_gMilliAmpsUsed = 0; uint16_t BusManager::_gMilliAmpsMax = ABL_MILLIAMPS_DEFAULT; bool BusManager::_useABL = false; ================================================ FILE: wled00/bus_manager.h ================================================ #pragma once #ifndef BusManager_h #define BusManager_h #ifdef WLED_ENABLE_HUB75MATRIX #include #include #include #endif /* * Class for addressing various light types */ #include "const.h" #include "pin_manager.h" #include #include #if __cplusplus >= 201402L using std::make_unique; #else // Really simple C++11 shim for non-array case; implementation from cppreference.com template std::unique_ptr make_unique(Args&&... args) { return std::unique_ptr(new T(std::forward(args)...)); } #endif // enable additional debug output #if defined(WLED_DEBUG_HOST) #include "net_debug.h" #define DEBUGOUT NetDebug #else #define DEBUGOUT Serial #endif #ifdef WLED_DEBUG_BUS #ifndef ESP8266 #include #endif #define DEBUGBUS_PRINT(x) DEBUGOUT.print(x) #define DEBUGBUS_PRINTLN(x) DEBUGOUT.println(x) #define DEBUGBUS_PRINTF(x...) DEBUGOUT.printf(x) #define DEBUGBUS_PRINTF_P(x...) DEBUGOUT.printf_P(x) #else #define DEBUGBUS_PRINT(x) #define DEBUGBUS_PRINTLN(x) #define DEBUGBUS_PRINTF(x...) #define DEBUGBUS_PRINTF_P(x...) #endif //colors.cpp uint16_t approximateKelvinFromRGB(uint32_t rgb); #define GET_BIT(var,bit) (((var)>>(bit))&0x01) #define SET_BIT(var,bit) ((var)|=(uint16_t)(0x0001<<(bit))) #define UNSET_BIT(var,bit) ((var)&=(~(uint16_t)(0x0001<<(bit)))) #define NUM_ICS_WS2812_1CH_3X(len) (((len)+2)/3) // 1 WS2811 IC controls 3 zones (each zone has 1 LED, W) #define IC_INDEX_WS2812_1CH_3X(i) ((i)/3) #define NUM_ICS_WS2812_2CH_3X(len) (((len)+1)*2/3) // 2 WS2811 ICs control 3 zones (each zone has 2 LEDs, CW and WW) #define IC_INDEX_WS2812_2CH_3X(i) ((i)*2/3) #define WS2812_2CH_3X_SPANS_2_ICS(i) ((i)&0x01) // every other LED zone is on two different ICs struct BusConfig; // forward declaration // Defines an LED Strip and its color ordering. typedef struct { uint16_t start; uint16_t len; uint8_t colorOrder; } ColorOrderMapEntry; struct ColorOrderMap { bool add(uint16_t start, uint16_t len, uint8_t colorOrder); inline uint8_t count() const { return _mappings.size(); } inline void reserve(size_t num) { _mappings.reserve(num); } void reset() { _mappings.clear(); _mappings.shrink_to_fit(); } const ColorOrderMapEntry* get(uint8_t n) const { if (n >= count()) return nullptr; return &(_mappings[n]); } [[gnu::hot]] uint8_t getPixelColorOrder(uint16_t pix, uint8_t defaultColorOrder) const; private: std::vector _mappings; }; typedef struct { uint8_t id; const char *type; const char *name; } LEDType; //parent class of BusDigital, BusPwm, and BusNetwork class Bus { public: Bus(uint8_t type, uint16_t start, uint8_t aw, uint16_t len = 1, bool reversed = false, bool refresh = false) : _type(type) , _bri(255) , _NPBbri(255) , _start(start) , _len(std::max(len,(uint16_t)1)) , _reversed(reversed) , _valid(false) , _needsRefresh(refresh) { _autoWhiteMode = Bus::hasWhite(type) ? aw : RGBW_MODE_MANUAL_ONLY; }; virtual ~Bus() {} //throw the bus under the bus virtual void begin() {}; virtual void show() = 0; virtual bool canShow() const { return true; } virtual void setStatusPixel(uint32_t c) {} virtual void setPixelColor(unsigned pix, uint32_t c) = 0; virtual void setBrightness(uint8_t b) { _bri = b; }; virtual void setColorOrder(uint8_t co) {} virtual uint32_t getPixelColor(unsigned pix) const { return 0; } virtual size_t getPins(uint8_t* pinArray = nullptr) const { return 0; } virtual uint16_t getLength() const { return _len; } virtual uint8_t getColorOrder() const { return COL_ORDER_RGB; } virtual unsigned skippedLeds() const { return 0; } virtual uint16_t getFrequency() const { return 0U; } virtual uint16_t getLEDCurrent() const { return 0; } virtual uint16_t getUsedCurrent() const { return 0; } virtual uint16_t getMaxCurrent() const { return 0; } virtual uint8_t getDriverType() const { return 0; } // Default to RMT (0) for non-digital buses virtual size_t getBusSize() const { return sizeof(Bus); } // currently unused virtual const String getCustomText() const { return String(); } inline bool hasRGB() const { return _hasRgb; } inline bool hasWhite() const { return _hasWhite; } inline bool hasCCT() const { return _hasCCT; } inline bool isDigital() const { return isDigital(_type); } inline bool is2Pin() const { return is2Pin(_type); } inline bool isOnOff() const { return isOnOff(_type); } inline bool isPWM() const { return isPWM(_type); } inline bool isVirtual() const { return isVirtual(_type); } inline bool is16bit() const { return is16bit(_type); } virtual bool isPlaceholder() const { return false; } inline bool mustRefresh() const { return mustRefresh(_type); } inline void setReversed(bool reversed) { _reversed = reversed; } inline void setStart(uint16_t start) { _start = start; } inline void setAutoWhiteMode(uint8_t m) { if (m < 5) _autoWhiteMode = m; } inline uint8_t getAutoWhiteMode() const { return _autoWhiteMode; } inline size_t getNumberOfChannels() const { return hasWhite() + 3*hasRGB() + hasCCT(); } inline uint16_t getStart() const { return _start; } inline uint8_t getType() const { return _type; } inline bool isOk() const { return _valid; } inline bool isReversed() const { return _reversed; } inline bool isOffRefreshRequired() const { return _needsRefresh; } inline bool containsPixel(uint16_t pix) const { return pix >= _start && pix < _start + _len; } static inline std::vector getLEDTypes() { return {{TYPE_NONE, "", PSTR("None")}}; } // not used. just for reference for derived classes static constexpr size_t getNumberOfPins(uint8_t type) { return isVirtual(type) ? 4 : isPWM(type) ? numPWMPins(type) : isHub75(type) ? 5 : is2Pin(type) + 1; } // credit @PaoloTK static constexpr size_t getNumberOfChannels(uint8_t type) { return hasWhite(type) + 3*hasRGB(type) + hasCCT(type); } static constexpr bool hasRGB(uint8_t type) { return !((type >= TYPE_WS2812_1CH && type <= TYPE_WS2812_WWA) || type == TYPE_ANALOG_1CH || type == TYPE_ANALOG_2CH || type == TYPE_ONOFF); } static constexpr bool hasWhite(uint8_t type) { return (type >= TYPE_WS2812_1CH && type <= TYPE_WS2812_WWA) || type == TYPE_SK6812_RGBW || type == TYPE_TM1814 || type == TYPE_UCS8904 || type == TYPE_FW1906 || type == TYPE_WS2805 || type == TYPE_SM16825 || // digital types with white channel (type > TYPE_ONOFF && type <= TYPE_ANALOG_5CH && type != TYPE_ANALOG_3CH) || // analog types with white channel type == TYPE_NET_DDP_RGBW || type == TYPE_NET_ARTNET_RGBW; // network types with white channel } static constexpr bool hasCCT(uint8_t type) { return type == TYPE_WS2812_2CH_X3 || type == TYPE_WS2812_WWA || type == TYPE_ANALOG_2CH || type == TYPE_ANALOG_5CH || type == TYPE_FW1906 || type == TYPE_WS2805 || type == TYPE_SM16825; } static constexpr bool isTypeValid(uint8_t type) { return (type > 15 && type < 128); } static constexpr bool isDigital(uint8_t type) { return (type >= TYPE_DIGITAL_MIN && type <= TYPE_DIGITAL_MAX) || is2Pin(type); } static constexpr bool is2Pin(uint8_t type) { return (type >= TYPE_2PIN_MIN && type <= TYPE_2PIN_MAX); } static constexpr bool isOnOff(uint8_t type) { return (type == TYPE_ONOFF); } static constexpr bool isPWM(uint8_t type) { return (type >= TYPE_ANALOG_MIN && type <= TYPE_ANALOG_MAX); } static constexpr bool isVirtual(uint8_t type) { return (type >= TYPE_VIRTUAL_MIN && type <= TYPE_VIRTUAL_MAX); } static constexpr bool isHub75(uint8_t type) { return (type >= TYPE_HUB75MATRIX_MIN && type <= TYPE_HUB75MATRIX_MAX); } static constexpr bool is16bit(uint8_t type) { return type == TYPE_UCS8903 || type == TYPE_UCS8904 || type == TYPE_SM16825; } static constexpr bool mustRefresh(uint8_t type) { return type == TYPE_TM1814; } static constexpr int numPWMPins(uint8_t type) { return (type - 40); } static inline int16_t getCCT() { return _cct; } static inline void setGlobalAWMode(uint8_t m) { if (m < 5) _gAWM = m; else _gAWM = AW_GLOBAL_DISABLED; } static inline uint8_t getGlobalAWMode() { return _gAWM; } static inline void setCCT(int16_t cct) { _cct = cct; } static inline int8_t getCCTBlend() { return (_cctBlend * 100 + (_cctBlend >= 0 ? 64 : -64)) / 127; } // returns -100 to +100, +/-100% = +/-127. +/-64 for rounding static inline void setCCTBlend(int8_t b) { // input is -100 to +100 _cctBlend = (std::max(-100, std::min(100, (int)b)) * 127 + (b >= 0 ? 50 : -50)) / 100; // +/-50 for rounding, b=+/-100% -> +/-127 //compile-time limiter for hardware that can't power both white channels at max #ifdef WLED_MAX_CCT_BLEND if (_cctBlend > WLED_MAX_CCT_BLEND) _cctBlend = WLED_MAX_CCT_BLEND; #endif } static void calculateCCT(uint32_t c, uint8_t &ww, uint8_t &cw); protected: uint8_t _type; uint8_t _bri; // bus brightness uint8_t _NPBbri; // total brightness applied to colors in NPB buffer (_bri + ABL) uint8_t _autoWhiteMode; // global Auto White Calculation override uint16_t _start; uint16_t _len; //struct { //using bitfield struct adds abour 250 bytes to binary size bool _reversed;// : 1; bool _valid;// : 1; bool _needsRefresh;// : 1; bool _hasRgb;// : 1; bool _hasWhite;// : 1; bool _hasCCT;// : 1; //} __attribute__ ((packed)); static uint8_t _gAWM; // _cct has the following meanings (see calculateCCT() & BusManager::setSegmentCCT()): // -1 means to extract approximate CCT value in K from RGB (in calcualteCCT()) // [0,255] is the exact CCT value where 0 means warm and 255 cold // [1900,10060] only for color correction expressed in K (colorBalanceFromKelvin()) static int16_t _cct; // _cctBlend determines WW/CW blending, see calculateCCT() // < 0 - linear blending in center, single white at both ends, single white zone extends with decreased value (-127 min) // 0 - linear (CCT 127 => 50% warm, 50% cold) // 63 - semi additive/nonlinear (CCT 127 => 66% warm, 66% cold) // 127 - additive CCT blending (CCT 127 => 100% warm, 100% cold) static int8_t _cctBlend; uint32_t autoWhiteCalc(uint32_t c, uint8_t &ww, uint8_t &cw) const; }; class BusDigital : public Bus { public: BusDigital(const BusConfig &bc); ~BusDigital() { cleanup(); } void show() override; bool canShow() const override; void setStatusPixel(uint32_t c) override; [[gnu::hot]] void setPixelColor(unsigned pix, uint32_t c) override; void setColorOrder(uint8_t colorOrder) override; [[gnu::hot]] uint32_t getPixelColor(unsigned pix) const override; uint8_t getColorOrder() const override { return _colorOrder; } size_t getPins(uint8_t* pinArray = nullptr) const override; unsigned skippedLeds() const override { return _skip; } uint16_t getFrequency() const override { return _frequencykHz; } uint16_t getLEDCurrent() const override { return _milliAmpsPerLed; } uint16_t getUsedCurrent() const override { return _milliAmpsTotal; } uint16_t getMaxCurrent() const override { return _milliAmpsMax; } uint8_t getDriverType() const override { return _driverType; } void setCurrentLimit(uint16_t milliAmps) { _milliAmpsLimit = milliAmps; } void estimateCurrent(); // estimate used current from summed colors void applyBriLimit(uint8_t newBri); size_t getBusSize() const override; bool isI2S(); // true if this bus uses I2S driver void begin() override; void cleanup(); static std::vector getLEDTypes(); private: uint8_t _skip; uint8_t _colorOrder; uint8_t _pins[2]; uint8_t _iType; uint8_t _driverType; // 0=RMT (default), 1=I2S uint16_t _frequencykHz; uint16_t _milliAmpsMax; uint8_t _milliAmpsPerLed; uint16_t _milliAmpsLimit; uint32_t _colorSum; // total color value for the bus, updated in setPixelColor(), used to estimate current void *_busPtr; static uint16_t _milliAmpsTotal; // is overwitten/recalculated on each show() inline uint32_t restoreColorLossy(uint32_t c, uint8_t restoreBri) const { if (restoreBri < 255) { uint8_t* chan = (uint8_t*) &c; for (uint_fast8_t i=0; i<4; i++) { uint_fast16_t val = chan[i]; chan[i] = ((val << 8) + restoreBri) / (restoreBri + 1); //adding _bri slightly improves recovery / stops degradation on re-scale } } return c; } }; class BusPwm : public Bus { public: BusPwm(const BusConfig &bc); ~BusPwm() { cleanup(); } void setPixelColor(unsigned pix, uint32_t c) override; uint32_t getPixelColor(unsigned pix) const override; //does no index check size_t getPins(uint8_t* pinArray = nullptr) const override; uint16_t getFrequency() const override { return _frequency; } size_t getBusSize() const override { return sizeof(BusPwm); } void show() override; inline void cleanup() { deallocatePins(); } static std::vector getLEDTypes(); private: uint8_t _pins[OUTPUT_MAX_PINS]; uint8_t _data[OUTPUT_MAX_PINS]; #ifdef ARDUINO_ARCH_ESP32 uint8_t _ledcStart; #endif uint8_t _depth; uint16_t _frequency; void deallocatePins(); }; class BusOnOff : public Bus { public: BusOnOff(const BusConfig &bc); ~BusOnOff() { cleanup(); } void setPixelColor(unsigned pix, uint32_t c) override; uint32_t getPixelColor(unsigned pix) const override; size_t getPins(uint8_t* pinArray) const override; size_t getBusSize() const override { return sizeof(BusOnOff); } void show() override; inline void cleanup() { PinManager::deallocatePin(_pin, PinOwner::BusOnOff); } static std::vector getLEDTypes(); private: uint8_t _pin; uint8_t _data; }; class BusNetwork : public Bus { public: BusNetwork(const BusConfig &bc); ~BusNetwork() { cleanup(); } bool canShow() const override { return !_broadcastLock; } // this should be a return value from UDP routine if it is still sending data out [[gnu::hot]] void setPixelColor(unsigned pix, uint32_t c) override; [[gnu::hot]] uint32_t getPixelColor(unsigned pix) const override; size_t getPins(uint8_t* pinArray = nullptr) const override; size_t getBusSize() const override { return sizeof(BusNetwork) + (isOk() ? _len * _UDPchannels : 0); } void show() override; void cleanup(); #ifdef ARDUINO_ARCH_ESP32 void resolveHostname(); const String getCustomText() const override { return _hostname; } #endif static std::vector getLEDTypes(); private: IPAddress _client; uint8_t _UDPtype; uint8_t _UDPchannels; bool _broadcastLock; uint8_t *_data; #ifdef ARDUINO_ARCH_ESP32 String _hostname; #endif }; // Placeholder for buses that we can't construct due to resource limitations // This preserves the configuration so it can be read back to the settings pages // Function calls "mimic" the replaced bus, isPlaceholder() can be used to identify a placeholder class BusPlaceholder : public Bus { public: BusPlaceholder(const BusConfig &bc); // Actual calls are stubbed out void setPixelColor(unsigned pix, uint32_t c) override {}; void show() override {}; // Accessors uint8_t getColorOrder() const override { return _colorOrder; } size_t getPins(uint8_t* pinArray) const override; unsigned skippedLeds() const override { return _skipAmount; } uint16_t getFrequency() const override { return _frequency; } uint16_t getLEDCurrent() const override { return _milliAmpsPerLed; } uint16_t getMaxCurrent() const override { return _milliAmpsMax; } uint8_t getDriverType() const override { return _driverType; } const String getCustomText() const override { return _text; } bool isPlaceholder() const override { return true; } size_t getBusSize() const override { return sizeof(BusPlaceholder); } private: uint8_t _colorOrder; uint8_t _skipAmount; uint8_t _pins[OUTPUT_MAX_PINS]; uint8_t _driverType; uint16_t _frequency; uint8_t _milliAmpsPerLed; uint16_t _milliAmpsMax; String _text; }; #ifdef WLED_ENABLE_HUB75MATRIX class BusHub75Matrix : public Bus { public: BusHub75Matrix(const BusConfig &bc); [[gnu::hot]] void setPixelColor(unsigned pix, uint32_t c) override; [[gnu::hot]] uint32_t getPixelColor(unsigned pix) const override; void show() override; void setBrightness(uint8_t b) override; size_t getPins(uint8_t* pinArray = nullptr) const override; void deallocatePins(); void cleanup(); ~BusHub75Matrix() { cleanup(); } static std::vector getLEDTypes(void); private: MatrixPanel_I2S_DMA *display = nullptr; VirtualMatrixPanel *virtualDisp = nullptr; HUB75_I2S_CFG mxconfig; unsigned _panelWidth = 0; uint8_t _rows = 1; // panels per row uint8_t _cols = 1; // panels per column bool _isVirtual = false; // note: this is not strictly needed but there are padding bytes here anyway CRGB *_ledBuffer = nullptr; // note: using uint32_t buffer is only 2% faster and not worth the extra RAM byte *_ledsDirty = nullptr; // workaround for missing constants on include path for non-MM static constexpr uint32_t IS_BLACK = 0x000000u; static constexpr uint32_t IS_DARKGREY = 0x333333u; static constexpr int PIN_COUNT = 14; }; #endif //temporary struct for passing bus configuration to bus struct BusConfig { uint8_t type; uint16_t count; uint16_t start; uint8_t colorOrder; bool reversed; uint8_t skipAmount; bool refreshReq; uint8_t autoWhite; uint8_t pins[OUTPUT_MAX_PINS] = {255, 255, 255, 255, 255}; uint16_t frequency; uint8_t milliAmpsPerLed; uint16_t milliAmpsMax; uint8_t driverType; // 0=RMT (default), 1=I2S uint8_t iType; // internal bus type (I_*) determined during memory estimation, used for bus creation String text; BusConfig(uint8_t busType, uint8_t* ppins, uint16_t pstart, uint16_t len = 1, uint8_t pcolorOrder = COL_ORDER_GRB, bool rev = false, uint8_t skip = 0, byte aw=RGBW_MODE_MANUAL_ONLY, uint16_t clock_kHz=0U, uint8_t maPerLed=LED_MILLIAMPS_DEFAULT, uint16_t maMax=ABL_MILLIAMPS_DEFAULT, uint8_t driver=0, String sometext = "") : count(std::max(len,(uint16_t)1)) , start(pstart) , colorOrder(pcolorOrder) , reversed(rev) , skipAmount(skip) , autoWhite(aw) , frequency(clock_kHz) , milliAmpsPerLed(maPerLed) , milliAmpsMax(maMax) , driverType(driver) , iType(0) // default to I_NONE , text(sometext) { refreshReq = (bool) GET_BIT(busType,7); type = busType & 0x7F; // bit 7 may be/is hacked to include refresh info (1=refresh in off state, 0=no refresh) size_t nPins = Bus::getNumberOfPins(type); for (size_t i = 0; i < nPins; i++) pins[i] = ppins[i]; DEBUGBUS_PRINTF_P(PSTR("Bus: Config (%d-%d, type:%d, CO:%d, rev:%d, skip:%d, AW:%d kHz:%d, mA:%d/%d, driver:%s)\n"), (int)start, (int)(start+len), (int)type, (int)colorOrder, (int)reversed, (int)skipAmount, (int)autoWhite, (int)frequency, (int)milliAmpsPerLed, (int)milliAmpsMax, driverType == 0 ? "RMT" : "I2S" ); } //validates start and length and extends total if needed bool adjustBounds(uint16_t& total) { if (!count) count = 1; if (!Bus::isVirtual(type) && count > MAX_LEDS_PER_BUS) count = MAX_LEDS_PER_BUS; if (start >= MAX_LEDS) return false; //limit length of strip if it would exceed total permissible LEDs if (start + count > MAX_LEDS) count = MAX_LEDS - start; //extend total count accordingly if (start + count > total) total = start + count; return true; } size_t memUsage() const; }; // milliamps used by ESP (for power estimation) // you can set it to 0 if the ESP is powered by USB and the LEDs by external #ifndef MA_FOR_ESP #ifdef ESP8266 #define MA_FOR_ESP 80 //how much mA does the ESP use (Wemos D1 about 80mA) #else #define MA_FOR_ESP 120 //how much mA does the ESP use (ESP32 about 120mA) #endif #endif namespace BusManager { extern std::vector> busses; //extern std::vector busses; extern uint16_t _gMilliAmpsUsed; extern uint16_t _gMilliAmpsMax; extern bool _useABL; #ifdef ESP32_DATA_IDLE_HIGH void esp32RMTInvertIdle() ; #endif inline size_t getNumVirtualBusses() { size_t j = 0; for (const auto &bus : busses) j += bus->isVirtual(); return j; } inline uint16_t currentMilliamps() { return _gMilliAmpsUsed + MA_FOR_ESP; } //inline uint16_t ablMilliampsMax() { unsigned sum = 0; for (auto &bus : busses) sum += bus->getMaxCurrent(); return sum; } inline uint16_t ablMilliampsMax() { return _gMilliAmpsMax; } // used for compatibility reasons (and enabling virtual global ABL) inline void setMilliampsMax(uint16_t max) { _gMilliAmpsMax = max;} void initializeABL(); // setup automatic brightness limiter parameters, call once after buses are initialized void applyABL(); // apply automatic brightness limiter, global or per bus uint8_t getI(uint8_t busType, const uint8_t* pins, uint8_t driverPreference); // workaround for access to PolyBus function from FX_fcn.cpp //do not call this method from system context (network callback) void removeAll(); int add(const BusConfig &bc, bool placeholder); void on(); void off(); [[gnu::hot]] void setPixelColor(unsigned pix, uint32_t c); [[gnu::hot]] uint32_t getPixelColor(unsigned pix); void show(); bool canAllShow(); inline void setStatusPixel(uint32_t c) { for (auto &bus : busses) bus->setStatusPixel(c);} inline void setBrightness(uint8_t b) { for (auto &bus : busses) bus->setBrightness(b); } // for setSegmentCCT(), cct can only be in [-1,255] range; allowWBCorrection will convert it to K // WARNING: setSegmentCCT() is a misleading name!!! much better would be setGlobalCCT() or just setCCT() void setSegmentCCT(int16_t cct, bool allowWBCorrection = false); inline int16_t getSegmentCCT() { return Bus::getCCT(); } inline Bus* getBus(size_t busNr) { return busNr < busses.size() ? busses[busNr].get() : nullptr; } inline size_t getNumBusses() { return busses.size(); } //semi-duplicate of strip.getLengthTotal() (though that just returns strip._length, calculated in finalizeInit()) inline uint16_t getTotalLength(bool onlyPhysical = false) { unsigned len = 0; for (const auto &bus : busses) if (!(bus->isVirtual() && onlyPhysical)) len += bus->getLength(); return len; } String getLEDTypesJSONString(); ColorOrderMap& getColorOrderMap(); }; #endif ================================================ FILE: wled00/bus_wrapper.h ================================================ #pragma once #ifndef BusWrapper_h #define BusWrapper_h //#define NPB_CONF_4STEP_CADENCE #include "NeoPixelBus.h" //Hardware SPI Pins #define P_8266_HS_MOSI 13 #define P_8266_HS_CLK 14 #define P_32_HS_MOSI 13 #define P_32_HS_CLK 14 #define P_32_VS_MOSI 23 #define P_32_VS_CLK 18 //The dirty list of possible bus types. Quite a lot... #define I_NONE 0 //ESP8266 RGB #define I_8266_U0_NEO_3 1 #define I_8266_U1_NEO_3 2 #define I_8266_DM_NEO_3 3 #define I_8266_BB_NEO_3 4 //RGBW #define I_8266_U0_NEO_4 5 #define I_8266_U1_NEO_4 6 #define I_8266_DM_NEO_4 7 #define I_8266_BB_NEO_4 8 //400Kbps #define I_8266_U0_400_3 9 #define I_8266_U1_400_3 10 #define I_8266_DM_400_3 11 #define I_8266_BB_400_3 12 //TM1814 (RGBW) #define I_8266_U0_TM1_4 13 #define I_8266_U1_TM1_4 14 #define I_8266_DM_TM1_4 15 #define I_8266_BB_TM1_4 16 //TM1829 (RGB) #define I_8266_U0_TM2_3 17 #define I_8266_U1_TM2_3 18 #define I_8266_DM_TM2_3 19 #define I_8266_BB_TM2_3 20 //UCS8903 (RGB) #define I_8266_U0_UCS_3 21 #define I_8266_U1_UCS_3 22 #define I_8266_DM_UCS_3 23 #define I_8266_BB_UCS_3 24 //UCS8904 (RGBW) #define I_8266_U0_UCS_4 25 #define I_8266_U1_UCS_4 26 #define I_8266_DM_UCS_4 27 #define I_8266_BB_UCS_4 28 //FW1906 GRBCW #define I_8266_U0_FW6_5 29 #define I_8266_U1_FW6_5 30 #define I_8266_DM_FW6_5 31 #define I_8266_BB_FW6_5 32 //ESP8266 APA106 #define I_8266_U0_APA106_3 33 #define I_8266_U1_APA106_3 34 #define I_8266_DM_APA106_3 35 #define I_8266_BB_APA106_3 36 //WS2805 (RGBCW) #define I_8266_U0_2805_5 37 #define I_8266_U1_2805_5 38 #define I_8266_DM_2805_5 39 #define I_8266_BB_2805_5 40 //TM1914 (RGB) #define I_8266_U0_TM1914_3 41 #define I_8266_U1_TM1914_3 42 #define I_8266_DM_TM1914_3 43 #define I_8266_BB_TM1914_3 44 //SM16825 (RGBCW) #define I_8266_U0_SM16825_5 45 #define I_8266_U1_SM16825_5 46 #define I_8266_DM_SM16825_5 47 #define I_8266_BB_SM16825_5 48 /*** ESP32 Neopixel methods ***/ //RGB #define I_32_RN_NEO_3 1 #define I_32_I2_NEO_3 2 //RGBW #define I_32_RN_NEO_4 5 #define I_32_I2_NEO_4 6 //400Kbps #define I_32_RN_400_3 9 #define I_32_I2_400_3 10 //TM1814 (RGBW) #define I_32_RN_TM1_4 13 #define I_32_I2_TM1_4 14 //TM1829 (RGB) #define I_32_RN_TM2_3 17 #define I_32_I2_TM2_3 18 //UCS8903 (RGB) #define I_32_RN_UCS_3 21 #define I_32_I2_UCS_3 22 //UCS8904 (RGBW) #define I_32_RN_UCS_4 25 #define I_32_I2_UCS_4 26 //FW1906 GRBCW #define I_32_RN_FW6_5 29 #define I_32_I2_FW6_5 30 //APA106 #define I_32_RN_APA106_3 33 #define I_32_I2_APA106_3 34 //WS2805 (RGBCW) #define I_32_RN_2805_5 37 #define I_32_I2_2805_5 38 //TM1914 (RGB) #define I_32_RN_TM1914_3 41 #define I_32_I2_TM1914_3 42 //SM16825 (RGBCW) #define I_32_RN_SM16825_5 45 #define I_32_I2_SM16825_5 46 //APA102 #define I_HS_DOT_3 101 //hardware SPI #define I_SS_DOT_3 102 //soft SPI //LPD8806 #define I_HS_LPD_3 103 #define I_SS_LPD_3 104 //WS2801 #define I_HS_WS1_3 105 #define I_SS_WS1_3 106 //P9813 #define I_HS_P98_3 107 #define I_SS_P98_3 108 //LPD6803 #define I_HS_LPO_3 109 #define I_SS_LPO_3 110 // In the following NeoGammaNullMethod can be replaced with NeoGammaWLEDMethod to perform Gamma correction implicitly // unfortunately that may apply Gamma correction to pre-calculated palettes which is undesired /*** ESP8266 Neopixel methods ***/ #ifdef ESP8266 //RGB #define B_8266_U0_NEO_3 NeoPixelBus //3 chan, esp8266, gpio1 #define B_8266_U1_NEO_3 NeoPixelBus //3 chan, esp8266, gpio2 #define B_8266_DM_NEO_3 NeoPixelBus //3 chan, esp8266, gpio3 #define B_8266_BB_NEO_3 NeoPixelBus //3 chan, esp8266, bb (any pin but 16) //RGBW #define B_8266_U0_NEO_4 NeoPixelBus //4 chan, esp8266, gpio1 #define B_8266_U1_NEO_4 NeoPixelBus //4 chan, esp8266, gpio2 #define B_8266_DM_NEO_4 NeoPixelBus //4 chan, esp8266, gpio3 #define B_8266_BB_NEO_4 NeoPixelBus //4 chan, esp8266, bb (any pin) //400Kbps #define B_8266_U0_400_3 NeoPixelBus //3 chan, esp8266, gpio1 #define B_8266_U1_400_3 NeoPixelBus //3 chan, esp8266, gpio2 #define B_8266_DM_400_3 NeoPixelBus //3 chan, esp8266, gpio3 #define B_8266_BB_400_3 NeoPixelBus //3 chan, esp8266, bb (any pin) //TM1814 (RGBW) #define B_8266_U0_TM1_4 NeoPixelBus #define B_8266_U1_TM1_4 NeoPixelBus #define B_8266_DM_TM1_4 NeoPixelBus #define B_8266_BB_TM1_4 NeoPixelBus //TM1829 (RGB) #define B_8266_U0_TM2_3 NeoPixelBus #define B_8266_U1_TM2_3 NeoPixelBus #define B_8266_DM_TM2_3 NeoPixelBus #define B_8266_BB_TM2_3 NeoPixelBus //UCS8903 #define B_8266_U0_UCS_3 NeoPixelBus //3 chan, esp8266, gpio1 #define B_8266_U1_UCS_3 NeoPixelBus //3 chan, esp8266, gpio2 #define B_8266_DM_UCS_3 NeoPixelBus //3 chan, esp8266, gpio3 #define B_8266_BB_UCS_3 NeoPixelBus //3 chan, esp8266, bb (any pin but 16) //UCS8904 RGBW #define B_8266_U0_UCS_4 NeoPixelBus //4 chan, esp8266, gpio1 #define B_8266_U1_UCS_4 NeoPixelBus //4 chan, esp8266, gpio2 #define B_8266_DM_UCS_4 NeoPixelBus //4 chan, esp8266, gpio3 #define B_8266_BB_UCS_4 NeoPixelBus //4 chan, esp8266, bb (any pin) //APA106 #define B_8266_U0_APA106_3 NeoPixelBus //3 chan, esp8266, gpio1 #define B_8266_U1_APA106_3 NeoPixelBus //3 chan, esp8266, gpio2 #define B_8266_DM_APA106_3 NeoPixelBus //3 chan, esp8266, gpio3 #define B_8266_BB_APA106_3 NeoPixelBus //3 chan, esp8266, bb (any pin but 16) //FW1906 GRBCW #define B_8266_U0_FW6_5 NeoPixelBus //esp8266, gpio1 #define B_8266_U1_FW6_5 NeoPixelBus //esp8266, gpio2 #define B_8266_DM_FW6_5 NeoPixelBus //esp8266, gpio3 #define B_8266_BB_FW6_5 NeoPixelBus //esp8266, bb //WS2805 GRBCW #define B_8266_U0_2805_5 NeoPixelBus //esp8266, gpio1 #define B_8266_U1_2805_5 NeoPixelBus //esp8266, gpio2 #define B_8266_DM_2805_5 NeoPixelBus //esp8266, gpio3 #define B_8266_BB_2805_5 NeoPixelBus //esp8266, bb //TM1914 (RGB) #define B_8266_U0_TM1914_3 NeoPixelBus #define B_8266_U1_TM1914_3 NeoPixelBus #define B_8266_DM_TM1914_3 NeoPixelBus #define B_8266_BB_TM1914_3 NeoPixelBus //Sm16825 (RGBWC) #define B_8266_U0_SM16825_5 NeoPixelBus #define B_8266_U1_SM16825_5 NeoPixelBus #define B_8266_DM_SM16825_5 NeoPixelBus #define B_8266_BB_SM16825_5 NeoPixelBus #endif /*** ESP32 Neopixel methods ***/ #ifdef ARDUINO_ARCH_ESP32 // C3: I2S0 and I2S1 methods not supported (has one I2S bus) // S2: I2S0 methods supported (single & parallel), I2S1 methods not supported (has one I2S bus) // S3: I2S0 methods not supported, I2S1 supports LCD parallel methods (has two I2S buses) // https://github.com/Makuna/NeoPixelBus/blob/b32f719e95ef3c35c46da5c99538017ef925c026/src/internal/Esp32_i2s.h#L4 // https://github.com/Makuna/NeoPixelBus/blob/b32f719e95ef3c35c46da5c99538017ef925c026/src/internal/NeoEsp32RmtMethod.h#L857 #if defined(CONFIG_IDF_TARGET_ESP32S3) // S3 will always use LCD parallel output typedef X8Ws2812xMethod X1Ws2812xMethod; typedef X8Sk6812Method X1Sk6812Method; typedef X8400KbpsMethod X1400KbpsMethod; typedef X8800KbpsMethod X1800KbpsMethod; typedef X8Tm1814Method X1Tm1814Method; typedef X8Tm1829Method X1Tm1829Method; typedef X8Apa106Method X1Apa106Method; typedef X8Ws2805Method X1Ws2805Method; typedef X8Tm1914Method X1Tm1914Method; #elif defined(CONFIG_IDF_TARGET_ESP32S2) // S2 will use I2S0 typedef NeoEsp32I2s0Ws2812xMethod X1Ws2812xMethod; typedef NeoEsp32I2s0Sk6812Method X1Sk6812Method; typedef NeoEsp32I2s0400KbpsMethod X1400KbpsMethod; typedef NeoEsp32I2s0800KbpsMethod X1800KbpsMethod; typedef NeoEsp32I2s0Tm1814Method X1Tm1814Method; typedef NeoEsp32I2s0Tm1829Method X1Tm1829Method; typedef NeoEsp32I2s0Apa106Method X1Apa106Method; typedef NeoEsp32I2s0Ws2805Method X1Ws2805Method; typedef NeoEsp32I2s0Tm1914Method X1Tm1914Method; #elif !defined(CONFIG_IDF_TARGET_ESP32C3) // regular ESP32 will use I2S1 typedef NeoEsp32I2s1Ws2812xMethod X1Ws2812xMethod; typedef NeoEsp32I2s1Sk6812Method X1Sk6812Method; typedef NeoEsp32I2s1400KbpsMethod X1400KbpsMethod; typedef NeoEsp32I2s1800KbpsMethod X1800KbpsMethod; typedef NeoEsp32I2s1Tm1814Method X1Tm1814Method; typedef NeoEsp32I2s1Tm1829Method X1Tm1829Method; typedef NeoEsp32I2s1Apa106Method X1Apa106Method; typedef NeoEsp32I2s1Ws2805Method X1Ws2805Method; typedef NeoEsp32I2s1Tm1914Method X1Tm1914Method; #endif // RMT driver selection #if !defined(WLED_USE_SHARED_RMT) && !defined(__riscv) #include #define NeoEsp32RmtMethod(x) NeoEsp32RmtHIN ## x ## Method #else #define NeoEsp32RmtMethod(x) NeoEsp32RmtN ## x ## Method #endif //RGB #define B_32_RN_NEO_3 NeoPixelBus // ESP32, S2, S3, C3 //#define B_32_IN_NEO_3 NeoPixelBus // ESP32 (dynamic I2S selection) #define B_32_I2_NEO_3 NeoPixelBus // ESP32, S2, S3 (automatic I2S selection, see typedef above) #define B_32_IP_NEO_3 NeoPixelBus // parallel I2S (ESP32, S2, S3) //RGBW #define B_32_RN_NEO_4 NeoPixelBus #define B_32_I2_NEO_4 NeoPixelBus #define B_32_IP_NEO_4 NeoPixelBus // parallel I2S //400Kbps #define B_32_RN_400_3 NeoPixelBus #define B_32_I2_400_3 NeoPixelBus #define B_32_IP_400_3 NeoPixelBus // parallel I2S //TM1814 (RGBW) #define B_32_RN_TM1_4 NeoPixelBus #define B_32_I2_TM1_4 NeoPixelBus #define B_32_IP_TM1_4 NeoPixelBus // parallel I2S //TM1829 (RGB) #define B_32_RN_TM2_3 NeoPixelBus #define B_32_I2_TM2_3 NeoPixelBus #define B_32_IP_TM2_3 NeoPixelBus // parallel I2S //UCS8903 #define B_32_RN_UCS_3 NeoPixelBus #define B_32_I2_UCS_3 NeoPixelBus #define B_32_IP_UCS_3 NeoPixelBus // parallel I2S //UCS8904 #define B_32_RN_UCS_4 NeoPixelBus #define B_32_I2_UCS_4 NeoPixelBus #define B_32_IP_UCS_4 NeoPixelBus// parallel I2S //APA106 #define B_32_RN_APA106_3 NeoPixelBus #define B_32_I2_APA106_3 NeoPixelBus #define B_32_IP_APA106_3 NeoPixelBus // parallel I2S //FW1906 GRBCW #define B_32_RN_FW6_5 NeoPixelBus #define B_32_I2_FW6_5 NeoPixelBus #define B_32_IP_FW6_5 NeoPixelBus // parallel I2S //WS2805 RGBWC #define B_32_RN_2805_5 NeoPixelBus #define B_32_I2_2805_5 NeoPixelBus #define B_32_IP_2805_5 NeoPixelBus // parallel I2S //TM1914 (RGB) #define B_32_RN_TM1914_3 NeoPixelBus #define B_32_I2_TM1914_3 NeoPixelBus #define B_32_IP_TM1914_3 NeoPixelBus // parallel I2S //Sm16825 (RGBWC) #define B_32_RN_SM16825_5 NeoPixelBus #define B_32_I2_SM16825_5 NeoPixelBus #define B_32_IP_SM16825_5 NeoPixelBus // parallel I2S #endif //APA102 #ifdef WLED_USE_ETHERNET // fix for #2542 (by @BlackBird77) #define B_HS_DOT_3 NeoPixelBus //hardware HSPI (was DotStarEsp32DmaHspi5MhzMethod in NPB @ 2.6.9) #else #define B_HS_DOT_3 NeoPixelBus //hardware VSPI #endif #define B_SS_DOT_3 NeoPixelBus //soft SPI //LPD8806 #define B_HS_LPD_3 NeoPixelBus #define B_SS_LPD_3 NeoPixelBus //LPD6803 #define B_HS_LPO_3 NeoPixelBus #define B_SS_LPO_3 NeoPixelBus //WS2801 #ifdef WLED_USE_ETHERNET #define B_HS_WS1_3 NeoPixelBus>> #else #define B_HS_WS1_3 NeoPixelBus #endif #define B_SS_WS1_3 NeoPixelBus //P9813 #define B_HS_P98_3 NeoPixelBus #define B_SS_P98_3 NeoPixelBus // 48bit & 64bit to 24bit & 32bit RGB(W) conversion #define toRGBW32(c) (RGBW32((c>>40)&0xFF, (c>>24)&0xFF, (c>>8)&0xFF, (c>>56)&0xFF)) #define RGBW32(r,g,b,w) (uint32_t((byte(w) << 24) | (byte(r) << 16) | (byte(g) << 8) | (byte(b)))) //handles pointer type conversion for all possible bus types class PolyBus { private: #ifndef ESP8266 static bool _useParallelI2S; // use parallel I2S/LCD (8 channels) static uint8_t _rmtChannelsAssigned; // RMT channel tracking for dynamic allocation static uint8_t _rmtChannel; // physical RMT channel to use during bus creation static uint8_t _i2sChannelsAssigned; // I2S channel tracking for dynamic allocation static uint8_t _parallelBusItype; // parallel output does not allow mixed LED types, track I_Type static uint8_t _2PchannelsAssigned; // 2-Pin (SPI) channel assigned: first one gets the hardware SPI, others use bit-banged SPI // note on 2-Pin Types: all supported types except WS2801 use start/stop or latch frames, speed is not critical. WS2801 uses a 500us timeout and is prone to flickering if bit-banged too slow. // TODO: according to #4863 using more than one bit-banged output can cause glitches even in APA102. This needs investigation as from a hardware perspective all but WS2801 should be immune to timing issues. #endif public: // initialize SPI bus speed for DotStar methods template static void beginDotStar(void* busPtr, int8_t sck, int8_t miso, int8_t mosi, int8_t ss, uint16_t clock_kHz /* 0 == use default */) { T dotStar_strip = static_cast(busPtr); #ifdef ESP8266 dotStar_strip->Begin(); #else if (sck == -1 && mosi == -1) dotStar_strip->Begin(); else dotStar_strip->Begin(sck, miso, mosi, ss); #endif if (clock_kHz) dotStar_strip->SetMethodSettings(NeoSpiSettings((uint32_t)clock_kHz*1000)); } // Begin & initialize the PixelSettings for TM1814 strips. template static void beginTM1814(void* busPtr) { T tm1814_strip = static_cast(busPtr); tm1814_strip->Begin(); // Max current for each LED (22.5 mA). tm1814_strip->SetPixelSettings(NeoTm1814Settings(/*R*/225, /*G*/225, /*B*/225, /*W*/225)); } template static void beginTM1914(void* busPtr) { T tm1914_strip = static_cast(busPtr); tm1914_strip->Begin(); tm1914_strip->SetPixelSettings(NeoTm1914Settings()); //NeoTm1914_Mode_DinFdinAutoSwitch, NeoTm1914_Mode_DinOnly, NeoTm1914_Mode_FdinOnly } static void begin(void* busPtr, uint8_t busType, uint8_t* pins, uint16_t clock_kHz /* only used by DotStar */) { switch (busType) { case I_NONE: break; #ifdef ESP8266 case I_8266_U0_NEO_3: (static_cast(busPtr))->Begin(); break; case I_8266_U1_NEO_3: (static_cast(busPtr))->Begin(); break; case I_8266_DM_NEO_3: (static_cast(busPtr))->Begin(); break; case I_8266_BB_NEO_3: (static_cast(busPtr))->Begin(); break; case I_8266_U0_NEO_4: (static_cast(busPtr))->Begin(); break; case I_8266_U1_NEO_4: (static_cast(busPtr))->Begin(); break; case I_8266_DM_NEO_4: (static_cast(busPtr))->Begin(); break; case I_8266_BB_NEO_4: (static_cast(busPtr))->Begin(); break; case I_8266_U0_400_3: (static_cast(busPtr))->Begin(); break; case I_8266_U1_400_3: (static_cast(busPtr))->Begin(); break; case I_8266_DM_400_3: (static_cast(busPtr))->Begin(); break; case I_8266_BB_400_3: (static_cast(busPtr))->Begin(); break; case I_8266_U0_TM1_4: beginTM1814(busPtr); break; case I_8266_U1_TM1_4: beginTM1814(busPtr); break; case I_8266_DM_TM1_4: beginTM1814(busPtr); break; case I_8266_BB_TM1_4: beginTM1814(busPtr); break; case I_8266_U0_TM2_3: (static_cast(busPtr))->Begin(); break; case I_8266_U1_TM2_3: (static_cast(busPtr))->Begin(); break; case I_8266_DM_TM2_3: (static_cast(busPtr))->Begin(); break; case I_8266_BB_TM2_3: (static_cast(busPtr))->Begin(); break; case I_HS_DOT_3: beginDotStar(busPtr, -1, -1, -1, -1, clock_kHz); break; case I_HS_LPD_3: beginDotStar(busPtr, -1, -1, -1, -1, clock_kHz); break; case I_HS_LPO_3: beginDotStar(busPtr, -1, -1, -1, -1, clock_kHz); break; case I_HS_WS1_3: beginDotStar(busPtr, -1, -1, -1, -1, clock_kHz); break; case I_HS_P98_3: beginDotStar(busPtr, -1, -1, -1, -1, clock_kHz); break; case I_8266_U0_UCS_3: (static_cast(busPtr))->Begin(); break; case I_8266_U1_UCS_3: (static_cast(busPtr))->Begin(); break; case I_8266_DM_UCS_3: (static_cast(busPtr))->Begin(); break; case I_8266_BB_UCS_3: (static_cast(busPtr))->Begin(); break; case I_8266_U0_UCS_4: (static_cast(busPtr))->Begin(); break; case I_8266_U1_UCS_4: (static_cast(busPtr))->Begin(); break; case I_8266_DM_UCS_4: (static_cast(busPtr))->Begin(); break; case I_8266_BB_UCS_4: (static_cast(busPtr))->Begin(); break; case I_8266_U0_APA106_3: (static_cast(busPtr))->Begin(); break; case I_8266_U1_APA106_3: (static_cast(busPtr))->Begin(); break; case I_8266_DM_APA106_3: (static_cast(busPtr))->Begin(); break; case I_8266_BB_APA106_3: (static_cast(busPtr))->Begin(); break; case I_8266_U0_FW6_5: (static_cast(busPtr))->Begin(); break; case I_8266_U1_FW6_5: (static_cast(busPtr))->Begin(); break; case I_8266_DM_FW6_5: (static_cast(busPtr))->Begin(); break; case I_8266_BB_FW6_5: (static_cast(busPtr))->Begin(); break; case I_8266_U0_2805_5: (static_cast(busPtr))->Begin(); break; case I_8266_U1_2805_5: (static_cast(busPtr))->Begin(); break; case I_8266_DM_2805_5: (static_cast(busPtr))->Begin(); break; case I_8266_BB_2805_5: (static_cast(busPtr))->Begin(); break; case I_8266_U0_TM1914_3: beginTM1914(busPtr); break; case I_8266_U1_TM1914_3: beginTM1914(busPtr); break; case I_8266_DM_TM1914_3: beginTM1914(busPtr); break; case I_8266_BB_TM1914_3: beginTM1914(busPtr); break; case I_8266_U0_SM16825_5: (static_cast(busPtr))->Begin(); break; case I_8266_U1_SM16825_5: (static_cast(busPtr))->Begin(); break; case I_8266_DM_SM16825_5: (static_cast(busPtr))->Begin(); break; case I_8266_BB_SM16825_5: (static_cast(busPtr))->Begin(); break; #endif #ifdef ARDUINO_ARCH_ESP32 // RMT buses case I_32_RN_NEO_3: (static_cast(busPtr))->Begin(); break; case I_32_RN_NEO_4: (static_cast(busPtr))->Begin(); break; case I_32_RN_400_3: (static_cast(busPtr))->Begin(); break; case I_32_RN_TM1_4: beginTM1814(busPtr); break; case I_32_RN_TM2_3: (static_cast(busPtr))->Begin(); break; case I_32_RN_UCS_3: (static_cast(busPtr))->Begin(); break; case I_32_RN_UCS_4: (static_cast(busPtr))->Begin(); break; case I_32_RN_FW6_5: (static_cast(busPtr))->Begin(); break; case I_32_RN_APA106_3: (static_cast(busPtr))->Begin(); break; case I_32_RN_2805_5: (static_cast(busPtr))->Begin(); break; case I_32_RN_TM1914_3: beginTM1914(busPtr); break; case I_32_RN_SM16825_5: (static_cast(busPtr))->Begin(); break; // I2S1 bus or parellel buses #ifndef CONFIG_IDF_TARGET_ESP32C3 case I_32_I2_NEO_3: if (_useParallelI2S) (static_cast(busPtr))->Begin(); else (static_cast(busPtr))->Begin(); break; case I_32_I2_NEO_4: if (_useParallelI2S) (static_cast(busPtr))->Begin(); else (static_cast(busPtr))->Begin(); break; case I_32_I2_400_3: if (_useParallelI2S) (static_cast(busPtr))->Begin(); else (static_cast(busPtr))->Begin(); break; case I_32_I2_TM1_4: if (_useParallelI2S) beginTM1814(busPtr); else beginTM1814(busPtr); break; case I_32_I2_TM2_3: if (_useParallelI2S) (static_cast(busPtr))->Begin(); else (static_cast(busPtr))->Begin(); break; case I_32_I2_UCS_3: if (_useParallelI2S) (static_cast(busPtr))->Begin(); else (static_cast(busPtr))->Begin(); break; case I_32_I2_UCS_4: if (_useParallelI2S) (static_cast(busPtr))->Begin(); else (static_cast(busPtr))->Begin(); break; case I_32_I2_FW6_5: if (_useParallelI2S) (static_cast(busPtr))->Begin(); else (static_cast(busPtr))->Begin(); break; case I_32_I2_APA106_3: if (_useParallelI2S) (static_cast(busPtr))->Begin(); else (static_cast(busPtr))->Begin(); break; case I_32_I2_2805_5: if (_useParallelI2S) (static_cast(busPtr))->Begin(); else (static_cast(busPtr))->Begin(); break; case I_32_I2_TM1914_3: if (_useParallelI2S) beginTM1914(busPtr); else beginTM1914(busPtr); break; case I_32_I2_SM16825_5: if (_useParallelI2S) (static_cast(busPtr))->Begin(); else (static_cast(busPtr))->Begin(); break; #endif // ESP32 can (and should, to avoid inadvertantly driving the chip select signal) specify the pins used for SPI, but only in begin() case I_HS_DOT_3: beginDotStar(busPtr, pins[1], -1, pins[0], -1, clock_kHz); break; case I_HS_LPD_3: beginDotStar(busPtr, pins[1], -1, pins[0], -1, clock_kHz); break; case I_HS_LPO_3: beginDotStar(busPtr, pins[1], -1, pins[0], -1, clock_kHz); break; case I_HS_WS1_3: beginDotStar(busPtr, pins[1], -1, pins[0], -1, clock_kHz); break; case I_HS_P98_3: beginDotStar(busPtr, pins[1], -1, pins[0], -1, clock_kHz); break; #endif case I_SS_DOT_3: (static_cast(busPtr))->Begin(); break; case I_SS_LPD_3: (static_cast(busPtr))->Begin(); break; case I_SS_LPO_3: (static_cast(busPtr))->Begin(); break; case I_SS_WS1_3: (static_cast(busPtr))->Begin(); break; case I_SS_P98_3: (static_cast(busPtr))->Begin(); break; } } static void* create(uint8_t busType, uint8_t* pins, uint16_t len) { void* busPtr = nullptr; switch (busType) { case I_NONE: break; #ifdef ESP8266 case I_8266_U0_NEO_3: busPtr = new B_8266_U0_NEO_3(len, pins[0]); break; case I_8266_U1_NEO_3: busPtr = new B_8266_U1_NEO_3(len, pins[0]); break; case I_8266_DM_NEO_3: busPtr = new B_8266_DM_NEO_3(len, pins[0]); break; case I_8266_BB_NEO_3: busPtr = new B_8266_BB_NEO_3(len, pins[0]); break; case I_8266_U0_NEO_4: busPtr = new B_8266_U0_NEO_4(len, pins[0]); break; case I_8266_U1_NEO_4: busPtr = new B_8266_U1_NEO_4(len, pins[0]); break; case I_8266_DM_NEO_4: busPtr = new B_8266_DM_NEO_4(len, pins[0]); break; case I_8266_BB_NEO_4: busPtr = new B_8266_BB_NEO_4(len, pins[0]); break; case I_8266_U0_400_3: busPtr = new B_8266_U0_400_3(len, pins[0]); break; case I_8266_U1_400_3: busPtr = new B_8266_U1_400_3(len, pins[0]); break; case I_8266_DM_400_3: busPtr = new B_8266_DM_400_3(len, pins[0]); break; case I_8266_BB_400_3: busPtr = new B_8266_BB_400_3(len, pins[0]); break; case I_8266_U0_TM1_4: busPtr = new B_8266_U0_TM1_4(len, pins[0]); break; case I_8266_U1_TM1_4: busPtr = new B_8266_U1_TM1_4(len, pins[0]); break; case I_8266_DM_TM1_4: busPtr = new B_8266_DM_TM1_4(len, pins[0]); break; case I_8266_BB_TM1_4: busPtr = new B_8266_BB_TM1_4(len, pins[0]); break; case I_8266_U0_TM2_3: busPtr = new B_8266_U0_TM2_3(len, pins[0]); break; case I_8266_U1_TM2_3: busPtr = new B_8266_U1_TM2_3(len, pins[0]); break; case I_8266_DM_TM2_3: busPtr = new B_8266_DM_TM2_3(len, pins[0]); break; case I_8266_BB_TM2_3: busPtr = new B_8266_BB_TM2_3(len, pins[0]); break; case I_8266_U0_UCS_3: busPtr = new B_8266_U0_UCS_3(len, pins[0]); break; case I_8266_U1_UCS_3: busPtr = new B_8266_U1_UCS_3(len, pins[0]); break; case I_8266_DM_UCS_3: busPtr = new B_8266_DM_UCS_3(len, pins[0]); break; case I_8266_BB_UCS_3: busPtr = new B_8266_BB_UCS_3(len, pins[0]); break; case I_8266_U0_UCS_4: busPtr = new B_8266_U0_UCS_4(len, pins[0]); break; case I_8266_U1_UCS_4: busPtr = new B_8266_U1_UCS_4(len, pins[0]); break; case I_8266_DM_UCS_4: busPtr = new B_8266_DM_UCS_4(len, pins[0]); break; case I_8266_BB_UCS_4: busPtr = new B_8266_BB_UCS_4(len, pins[0]); break; case I_8266_U0_APA106_3: busPtr = new B_8266_U0_APA106_3(len, pins[0]); break; case I_8266_U1_APA106_3: busPtr = new B_8266_U1_APA106_3(len, pins[0]); break; case I_8266_DM_APA106_3: busPtr = new B_8266_DM_APA106_3(len, pins[0]); break; case I_8266_BB_APA106_3: busPtr = new B_8266_BB_APA106_3(len, pins[0]); break; case I_8266_U0_FW6_5: busPtr = new B_8266_U0_FW6_5(len, pins[0]); break; case I_8266_U1_FW6_5: busPtr = new B_8266_U1_FW6_5(len, pins[0]); break; case I_8266_DM_FW6_5: busPtr = new B_8266_DM_FW6_5(len, pins[0]); break; case I_8266_BB_FW6_5: busPtr = new B_8266_BB_FW6_5(len, pins[0]); break; case I_8266_U0_2805_5: busPtr = new B_8266_U0_2805_5(len, pins[0]); break; case I_8266_U1_2805_5: busPtr = new B_8266_U1_2805_5(len, pins[0]); break; case I_8266_DM_2805_5: busPtr = new B_8266_DM_2805_5(len, pins[0]); break; case I_8266_BB_2805_5: busPtr = new B_8266_BB_2805_5(len, pins[0]); break; case I_8266_U0_TM1914_3: busPtr = new B_8266_U0_TM1914_3(len, pins[0]); break; case I_8266_U1_TM1914_3: busPtr = new B_8266_U1_TM1914_3(len, pins[0]); break; case I_8266_DM_TM1914_3: busPtr = new B_8266_DM_TM1914_3(len, pins[0]); break; case I_8266_BB_TM1914_3: busPtr = new B_8266_BB_TM1914_3(len, pins[0]); break; case I_8266_U0_SM16825_5: busPtr = new B_8266_U0_SM16825_5(len, pins[0]); break; case I_8266_U1_SM16825_5: busPtr = new B_8266_U1_SM16825_5(len, pins[0]); break; case I_8266_DM_SM16825_5: busPtr = new B_8266_DM_SM16825_5(len, pins[0]); break; case I_8266_BB_SM16825_5: busPtr = new B_8266_BB_SM16825_5(len, pins[0]); break; #endif #ifdef ARDUINO_ARCH_ESP32 // RMT buses case I_32_RN_NEO_3: busPtr = new B_32_RN_NEO_3(len, pins[0], (NeoBusChannel)_rmtChannel++); break; case I_32_RN_NEO_4: busPtr = new B_32_RN_NEO_4(len, pins[0], (NeoBusChannel)_rmtChannel++); break; case I_32_RN_400_3: busPtr = new B_32_RN_400_3(len, pins[0], (NeoBusChannel)_rmtChannel++); break; case I_32_RN_TM1_4: busPtr = new B_32_RN_TM1_4(len, pins[0], (NeoBusChannel)_rmtChannel++); break; case I_32_RN_TM2_3: busPtr = new B_32_RN_TM2_3(len, pins[0], (NeoBusChannel)_rmtChannel++); break; case I_32_RN_UCS_3: busPtr = new B_32_RN_UCS_3(len, pins[0], (NeoBusChannel)_rmtChannel++); break; case I_32_RN_UCS_4: busPtr = new B_32_RN_UCS_4(len, pins[0], (NeoBusChannel)_rmtChannel++); break; case I_32_RN_APA106_3: busPtr = new B_32_RN_APA106_3(len, pins[0], (NeoBusChannel)_rmtChannel++); break; case I_32_RN_FW6_5: busPtr = new B_32_RN_FW6_5(len, pins[0], (NeoBusChannel)_rmtChannel++); break; case I_32_RN_2805_5: busPtr = new B_32_RN_2805_5(len, pins[0], (NeoBusChannel)_rmtChannel++); break; case I_32_RN_TM1914_3: busPtr = new B_32_RN_TM1914_3(len, pins[0], (NeoBusChannel)_rmtChannel++); break; case I_32_RN_SM16825_5: busPtr = new B_32_RN_SM16825_5(len, pins[0], (NeoBusChannel)_rmtChannel++); break; // I2S1 bus or paralell buses #ifndef CONFIG_IDF_TARGET_ESP32C3 case I_32_I2_NEO_3: if (_useParallelI2S) busPtr = new B_32_IP_NEO_3(len, pins[0]); else busPtr = new B_32_I2_NEO_3(len, pins[0]); break; case I_32_I2_NEO_4: if (_useParallelI2S) busPtr = new B_32_IP_NEO_4(len, pins[0]); else busPtr = new B_32_I2_NEO_4(len, pins[0]); break; case I_32_I2_400_3: if (_useParallelI2S) busPtr = new B_32_IP_400_3(len, pins[0]); else busPtr = new B_32_I2_400_3(len, pins[0]); break; case I_32_I2_TM1_4: if (_useParallelI2S) busPtr = new B_32_IP_TM1_4(len, pins[0]); else busPtr = new B_32_I2_TM1_4(len, pins[0]); break; case I_32_I2_TM2_3: if (_useParallelI2S) busPtr = new B_32_IP_TM2_3(len, pins[0]); else busPtr = new B_32_I2_TM2_3(len, pins[0]); break; case I_32_I2_UCS_3: if (_useParallelI2S) busPtr = new B_32_IP_UCS_3(len, pins[0]); else busPtr = new B_32_I2_UCS_3(len, pins[0]); break; case I_32_I2_UCS_4: if (_useParallelI2S) busPtr = new B_32_IP_UCS_4(len, pins[0]); else busPtr = new B_32_I2_UCS_4(len, pins[0]); break; case I_32_I2_APA106_3: if (_useParallelI2S) busPtr = new B_32_IP_APA106_3(len, pins[0]); else busPtr = new B_32_I2_APA106_3(len, pins[0]); break; case I_32_I2_FW6_5: if (_useParallelI2S) busPtr = new B_32_IP_FW6_5(len, pins[0]); else busPtr = new B_32_I2_FW6_5(len, pins[0]); break; case I_32_I2_2805_5: if (_useParallelI2S) busPtr = new B_32_IP_2805_5(len, pins[0]); else busPtr = new B_32_I2_2805_5(len, pins[0]); break; case I_32_I2_TM1914_3: if (_useParallelI2S) busPtr = new B_32_IP_TM1914_3(len, pins[0]); else busPtr = new B_32_I2_TM1914_3(len, pins[0]); break; case I_32_I2_SM16825_5: if (_useParallelI2S) busPtr = new B_32_IP_SM16825_5(len, pins[0]); else busPtr = new B_32_I2_SM16825_5(len, pins[0]); break; #endif #endif // for 2-wire: pins[1] is clk, pins[0] is dat. begin expects (len, clk, dat) case I_HS_DOT_3: busPtr = new B_HS_DOT_3(len, pins[1], pins[0]); break; case I_SS_DOT_3: busPtr = new B_SS_DOT_3(len, pins[1], pins[0]); break; case I_HS_LPD_3: busPtr = new B_HS_LPD_3(len, pins[1], pins[0]); break; case I_SS_LPD_3: busPtr = new B_SS_LPD_3(len, pins[1], pins[0]); break; case I_HS_LPO_3: busPtr = new B_HS_LPO_3(len, pins[1], pins[0]); break; case I_SS_LPO_3: busPtr = new B_SS_LPO_3(len, pins[1], pins[0]); break; case I_HS_WS1_3: busPtr = new B_HS_WS1_3(len, pins[1], pins[0]); break; case I_SS_WS1_3: busPtr = new B_SS_WS1_3(len, pins[1], pins[0]); break; case I_HS_P98_3: busPtr = new B_HS_P98_3(len, pins[1], pins[0]); break; case I_SS_P98_3: busPtr = new B_SS_P98_3(len, pins[1], pins[0]); break; } return busPtr; } static void show(void* busPtr, uint8_t busType, bool consistent = true) { switch (busType) { case I_NONE: break; #ifdef ESP8266 case I_8266_U0_NEO_3: (static_cast(busPtr))->Show(consistent); break; case I_8266_U1_NEO_3: (static_cast(busPtr))->Show(consistent); break; case I_8266_DM_NEO_3: (static_cast(busPtr))->Show(consistent); break; case I_8266_BB_NEO_3: (static_cast(busPtr))->Show(consistent); break; case I_8266_U0_NEO_4: (static_cast(busPtr))->Show(consistent); break; case I_8266_U1_NEO_4: (static_cast(busPtr))->Show(consistent); break; case I_8266_DM_NEO_4: (static_cast(busPtr))->Show(consistent); break; case I_8266_BB_NEO_4: (static_cast(busPtr))->Show(consistent); break; case I_8266_U0_400_3: (static_cast(busPtr))->Show(consistent); break; case I_8266_U1_400_3: (static_cast(busPtr))->Show(consistent); break; case I_8266_DM_400_3: (static_cast(busPtr))->Show(consistent); break; case I_8266_BB_400_3: (static_cast(busPtr))->Show(consistent); break; case I_8266_U0_TM1_4: (static_cast(busPtr))->Show(consistent); break; case I_8266_U1_TM1_4: (static_cast(busPtr))->Show(consistent); break; case I_8266_DM_TM1_4: (static_cast(busPtr))->Show(consistent); break; case I_8266_BB_TM1_4: (static_cast(busPtr))->Show(consistent); break; case I_8266_U0_TM2_3: (static_cast(busPtr))->Show(consistent); break; case I_8266_U1_TM2_3: (static_cast(busPtr))->Show(consistent); break; case I_8266_DM_TM2_3: (static_cast(busPtr))->Show(consistent); break; case I_8266_BB_TM2_3: (static_cast(busPtr))->Show(consistent); break; case I_8266_U0_UCS_3: (static_cast(busPtr))->Show(consistent); break; case I_8266_U1_UCS_3: (static_cast(busPtr))->Show(consistent); break; case I_8266_DM_UCS_3: (static_cast(busPtr))->Show(consistent); break; case I_8266_BB_UCS_3: (static_cast(busPtr))->Show(consistent); break; case I_8266_U0_UCS_4: (static_cast(busPtr))->Show(consistent); break; case I_8266_U1_UCS_4: (static_cast(busPtr))->Show(consistent); break; case I_8266_DM_UCS_4: (static_cast(busPtr))->Show(consistent); break; case I_8266_BB_UCS_4: (static_cast(busPtr))->Show(consistent); break; case I_8266_U0_APA106_3: (static_cast(busPtr))->Show(consistent); break; case I_8266_U1_APA106_3: (static_cast(busPtr))->Show(consistent); break; case I_8266_DM_APA106_3: (static_cast(busPtr))->Show(consistent); break; case I_8266_BB_APA106_3: (static_cast(busPtr))->Show(consistent); break; case I_8266_U0_FW6_5: (static_cast(busPtr))->Show(consistent); break; case I_8266_U1_FW6_5: (static_cast(busPtr))->Show(consistent); break; case I_8266_DM_FW6_5: (static_cast(busPtr))->Show(consistent); break; case I_8266_BB_FW6_5: (static_cast(busPtr))->Show(consistent); break; case I_8266_U0_2805_5: (static_cast(busPtr))->Show(consistent); break; case I_8266_U1_2805_5: (static_cast(busPtr))->Show(consistent); break; case I_8266_DM_2805_5: (static_cast(busPtr))->Show(consistent); break; case I_8266_BB_2805_5: (static_cast(busPtr))->Show(consistent); break; case I_8266_U0_TM1914_3: (static_cast(busPtr))->Show(consistent); break; case I_8266_U1_TM1914_3: (static_cast(busPtr))->Show(consistent); break; case I_8266_DM_TM1914_3: (static_cast(busPtr))->Show(consistent); break; case I_8266_BB_TM1914_3: (static_cast(busPtr))->Show(consistent); break; case I_8266_U0_SM16825_5: (static_cast(busPtr))->Show(consistent); break; case I_8266_U1_SM16825_5: (static_cast(busPtr))->Show(consistent); break; case I_8266_DM_SM16825_5: (static_cast(busPtr))->Show(consistent); break; case I_8266_BB_SM16825_5: (static_cast(busPtr))->Show(consistent); break; #endif #ifdef ARDUINO_ARCH_ESP32 // RMT buses case I_32_RN_NEO_3: (static_cast(busPtr))->Show(consistent); break; case I_32_RN_NEO_4: (static_cast(busPtr))->Show(consistent); break; case I_32_RN_400_3: (static_cast(busPtr))->Show(consistent); break; case I_32_RN_TM1_4: (static_cast(busPtr))->Show(consistent); break; case I_32_RN_TM2_3: (static_cast(busPtr))->Show(consistent); break; case I_32_RN_UCS_3: (static_cast(busPtr))->Show(consistent); break; case I_32_RN_UCS_4: (static_cast(busPtr))->Show(consistent); break; case I_32_RN_APA106_3: (static_cast(busPtr))->Show(consistent); break; case I_32_RN_FW6_5: (static_cast(busPtr))->Show(consistent); break; case I_32_RN_2805_5: (static_cast(busPtr))->Show(consistent); break; case I_32_RN_TM1914_3: (static_cast(busPtr))->Show(consistent); break; case I_32_RN_SM16825_5: (static_cast(busPtr))->Show(consistent); break; // I2S1 bus or paralell buses #ifndef CONFIG_IDF_TARGET_ESP32C3 case I_32_I2_NEO_3: if (_useParallelI2S) (static_cast(busPtr))->Show(consistent); else (static_cast(busPtr))->Show(consistent); break; case I_32_I2_NEO_4: if (_useParallelI2S) (static_cast(busPtr))->Show(consistent); else (static_cast(busPtr))->Show(consistent); break; case I_32_I2_400_3: if (_useParallelI2S) (static_cast(busPtr))->Show(consistent); else (static_cast(busPtr))->Show(consistent); break; case I_32_I2_TM1_4: if (_useParallelI2S) (static_cast(busPtr))->Show(consistent); else (static_cast(busPtr))->Show(consistent); break; case I_32_I2_TM2_3: if (_useParallelI2S) (static_cast(busPtr))->Show(consistent); else (static_cast(busPtr))->Show(consistent); break; case I_32_I2_UCS_3: if (_useParallelI2S) (static_cast(busPtr))->Show(consistent); else (static_cast(busPtr))->Show(consistent); break; case I_32_I2_UCS_4: if (_useParallelI2S) (static_cast(busPtr))->Show(consistent); else (static_cast(busPtr))->Show(consistent); break; case I_32_I2_APA106_3: if (_useParallelI2S) (static_cast(busPtr))->Show(consistent); else (static_cast(busPtr))->Show(consistent); break; case I_32_I2_FW6_5: if (_useParallelI2S) (static_cast(busPtr))->Show(consistent); else (static_cast(busPtr))->Show(consistent); break; case I_32_I2_2805_5: if (_useParallelI2S) (static_cast(busPtr))->Show(consistent); else (static_cast(busPtr))->Show(consistent); break; case I_32_I2_TM1914_3: if (_useParallelI2S) (static_cast(busPtr))->Show(consistent); else (static_cast(busPtr))->Show(consistent); break; case I_32_I2_SM16825_5: if (_useParallelI2S) (static_cast(busPtr))->Show(consistent); else (static_cast(busPtr))->Show(consistent); break; #endif #endif case I_HS_DOT_3: (static_cast(busPtr))->Show(consistent); break; case I_SS_DOT_3: (static_cast(busPtr))->Show(consistent); break; case I_HS_LPD_3: (static_cast(busPtr))->Show(consistent); break; case I_SS_LPD_3: (static_cast(busPtr))->Show(consistent); break; case I_HS_LPO_3: (static_cast(busPtr))->Show(consistent); break; case I_SS_LPO_3: (static_cast(busPtr))->Show(consistent); break; case I_HS_WS1_3: (static_cast(busPtr))->Show(consistent); break; case I_SS_WS1_3: (static_cast(busPtr))->Show(consistent); break; case I_HS_P98_3: (static_cast(busPtr))->Show(consistent); break; case I_SS_P98_3: (static_cast(busPtr))->Show(consistent); break; } } static bool canShow(void* busPtr, uint8_t busType) { switch (busType) { case I_NONE: return true; #ifdef ESP8266 case I_8266_U0_NEO_3: return (static_cast(busPtr))->CanShow(); break; case I_8266_U1_NEO_3: return (static_cast(busPtr))->CanShow(); break; case I_8266_DM_NEO_3: return (static_cast(busPtr))->CanShow(); break; case I_8266_BB_NEO_3: return (static_cast(busPtr))->CanShow(); break; case I_8266_U0_NEO_4: return (static_cast(busPtr))->CanShow(); break; case I_8266_U1_NEO_4: return (static_cast(busPtr))->CanShow(); break; case I_8266_DM_NEO_4: return (static_cast(busPtr))->CanShow(); break; case I_8266_BB_NEO_4: return (static_cast(busPtr))->CanShow(); break; case I_8266_U0_400_3: return (static_cast(busPtr))->CanShow(); break; case I_8266_U1_400_3: return (static_cast(busPtr))->CanShow(); break; case I_8266_DM_400_3: return (static_cast(busPtr))->CanShow(); break; case I_8266_BB_400_3: return (static_cast(busPtr))->CanShow(); break; case I_8266_U0_TM1_4: return (static_cast(busPtr))->CanShow(); break; case I_8266_U1_TM1_4: return (static_cast(busPtr))->CanShow(); break; case I_8266_DM_TM1_4: return (static_cast(busPtr))->CanShow(); break; case I_8266_BB_TM1_4: return (static_cast(busPtr))->CanShow(); break; case I_8266_U0_TM2_3: return (static_cast(busPtr))->CanShow(); break; case I_8266_U1_TM2_3: return (static_cast(busPtr))->CanShow(); break; case I_8266_DM_TM2_3: return (static_cast(busPtr))->CanShow(); break; case I_8266_BB_TM2_3: return (static_cast(busPtr))->CanShow(); break; case I_8266_U0_UCS_3: return (static_cast(busPtr))->CanShow(); break; case I_8266_U1_UCS_3: return (static_cast(busPtr))->CanShow(); break; case I_8266_DM_UCS_3: return (static_cast(busPtr))->CanShow(); break; case I_8266_BB_UCS_3: return (static_cast(busPtr))->CanShow(); break; case I_8266_U0_UCS_4: return (static_cast(busPtr))->CanShow(); break; case I_8266_U1_UCS_4: return (static_cast(busPtr))->CanShow(); break; case I_8266_DM_UCS_4: return (static_cast(busPtr))->CanShow(); break; case I_8266_BB_UCS_4: return (static_cast(busPtr))->CanShow(); break; case I_8266_U0_APA106_3: return (static_cast(busPtr))->CanShow(); break; case I_8266_U1_APA106_3: return (static_cast(busPtr))->CanShow(); break; case I_8266_DM_APA106_3: return (static_cast(busPtr))->CanShow(); break; case I_8266_BB_APA106_3: return (static_cast(busPtr))->CanShow(); break; case I_8266_U0_FW6_5: return (static_cast(busPtr))->CanShow(); break; case I_8266_U1_FW6_5: return (static_cast(busPtr))->CanShow(); break; case I_8266_DM_FW6_5: return (static_cast(busPtr))->CanShow(); break; case I_8266_BB_FW6_5: return (static_cast(busPtr))->CanShow(); break; case I_8266_U0_2805_5: return (static_cast(busPtr))->CanShow(); break; case I_8266_U1_2805_5: return (static_cast(busPtr))->CanShow(); break; case I_8266_DM_2805_5: return (static_cast(busPtr))->CanShow(); break; case I_8266_BB_2805_5: return (static_cast(busPtr))->CanShow(); break; case I_8266_U0_TM1914_3: return (static_cast(busPtr))->CanShow(); break; case I_8266_U1_TM1914_3: return (static_cast(busPtr))->CanShow(); break; case I_8266_DM_TM1914_3: return (static_cast(busPtr))->CanShow(); break; case I_8266_BB_TM1914_3: return (static_cast(busPtr))->CanShow(); break; case I_8266_U0_SM16825_5: return (static_cast(busPtr))->CanShow(); break; case I_8266_U1_SM16825_5: return (static_cast(busPtr))->CanShow(); break; case I_8266_DM_SM16825_5: return (static_cast(busPtr))->CanShow(); break; case I_8266_BB_SM16825_5: return (static_cast(busPtr))->CanShow(); break; #endif #ifdef ARDUINO_ARCH_ESP32 // RMT buses case I_32_RN_NEO_3: return (static_cast(busPtr))->CanShow(); break; case I_32_RN_NEO_4: return (static_cast(busPtr))->CanShow(); break; case I_32_RN_400_3: return (static_cast(busPtr))->CanShow(); break; case I_32_RN_TM1_4: return (static_cast(busPtr))->CanShow(); break; case I_32_RN_TM2_3: return (static_cast(busPtr))->CanShow(); break; case I_32_RN_UCS_3: return (static_cast(busPtr))->CanShow(); break; case I_32_RN_UCS_4: return (static_cast(busPtr))->CanShow(); break; case I_32_RN_APA106_3: return (static_cast(busPtr))->CanShow(); break; case I_32_RN_FW6_5: return (static_cast(busPtr))->CanShow(); break; case I_32_RN_2805_5: return (static_cast(busPtr))->CanShow(); break; case I_32_RN_TM1914_3: return (static_cast(busPtr))->CanShow(); break; case I_32_RN_SM16825_5: return (static_cast(busPtr))->CanShow(); break; // I2S1 bus or paralell buses #ifndef CONFIG_IDF_TARGET_ESP32C3 case I_32_I2_NEO_3: if (_useParallelI2S) return (static_cast(busPtr))->CanShow(); else return (static_cast(busPtr))->CanShow(); break; case I_32_I2_NEO_4: if (_useParallelI2S) return (static_cast(busPtr))->CanShow(); else return (static_cast(busPtr))->CanShow(); break; case I_32_I2_400_3: if (_useParallelI2S) return (static_cast(busPtr))->CanShow(); else return (static_cast(busPtr))->CanShow(); break; case I_32_I2_TM1_4: if (_useParallelI2S) return (static_cast(busPtr))->CanShow(); else return (static_cast(busPtr))->CanShow(); break; case I_32_I2_TM2_3: if (_useParallelI2S) return (static_cast(busPtr))->CanShow(); else return (static_cast(busPtr))->CanShow(); break; case I_32_I2_UCS_3: if (_useParallelI2S) return (static_cast(busPtr))->CanShow(); else return (static_cast(busPtr))->CanShow(); break; case I_32_I2_UCS_4: if (_useParallelI2S) return (static_cast(busPtr))->CanShow(); else return (static_cast(busPtr))->CanShow(); break; case I_32_I2_APA106_3: if (_useParallelI2S) return (static_cast(busPtr))->CanShow(); else return (static_cast(busPtr))->CanShow(); break; case I_32_I2_FW6_5: if (_useParallelI2S) return (static_cast(busPtr))->CanShow(); else return (static_cast(busPtr))->CanShow(); break; case I_32_I2_2805_5: if (_useParallelI2S) return (static_cast(busPtr))->CanShow(); else return (static_cast(busPtr))->CanShow(); break; case I_32_I2_TM1914_3: if (_useParallelI2S) return (static_cast(busPtr))->CanShow(); else return (static_cast(busPtr))->CanShow(); break; case I_32_I2_SM16825_5: if (_useParallelI2S) return (static_cast(busPtr))->CanShow(); else return (static_cast(busPtr))->CanShow(); break; #endif #endif case I_HS_DOT_3: return (static_cast(busPtr))->CanShow(); break; case I_SS_DOT_3: return (static_cast(busPtr))->CanShow(); break; case I_HS_LPD_3: return (static_cast(busPtr))->CanShow(); break; case I_SS_LPD_3: return (static_cast(busPtr))->CanShow(); break; case I_HS_LPO_3: return (static_cast(busPtr))->CanShow(); break; case I_SS_LPO_3: return (static_cast(busPtr))->CanShow(); break; case I_HS_WS1_3: return (static_cast(busPtr))->CanShow(); break; case I_SS_WS1_3: return (static_cast(busPtr))->CanShow(); break; case I_HS_P98_3: return (static_cast(busPtr))->CanShow(); break; case I_SS_P98_3: return (static_cast(busPtr))->CanShow(); break; } return true; } [[gnu::hot]] static void setPixelColor(void* busPtr, uint8_t busType, uint16_t pix, uint32_t c, uint8_t co, uint16_t wwcw = 0) { uint8_t r = c >> 16; uint8_t g = c >> 8; uint8_t b = c >> 0; uint8_t w = c >> 24; RgbwColor col; uint8_t cctWW = wwcw & 0xFF, cctCW = (wwcw>>8) & 0xFF; // reorder channels to selected order switch (co & 0x0F) { default: col.G = g; col.R = r; col.B = b; break; //0 = GRB, default case 1: col.G = r; col.R = g; col.B = b; break; //1 = RGB, common for WS2811 case 2: col.G = b; col.R = r; col.B = g; break; //2 = BRG case 3: col.G = r; col.R = b; col.B = g; break; //3 = RBG case 4: col.G = b; col.R = g; col.B = r; break; //4 = BGR case 5: col.G = g; col.R = b; col.B = r; break; //5 = GBR } // upper nibble contains W swap information switch (co >> 4) { default: col.W = w; break; // no swapping case 1: col.W = col.B; col.B = w; break; // swap W & B case 2: col.W = col.G; col.G = w; break; // swap W & G case 3: col.W = col.R; col.R = w; break; // swap W & R case 4: std::swap(cctWW, cctCW); break; // swap WW & CW } switch (busType) { case I_NONE: break; #ifdef ESP8266 case I_8266_U0_NEO_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; case I_8266_U1_NEO_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; case I_8266_DM_NEO_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; case I_8266_BB_NEO_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; case I_8266_U0_NEO_4: (static_cast(busPtr))->SetPixelColor(pix, col); break; case I_8266_U1_NEO_4: (static_cast(busPtr))->SetPixelColor(pix, col); break; case I_8266_DM_NEO_4: (static_cast(busPtr))->SetPixelColor(pix, col); break; case I_8266_BB_NEO_4: (static_cast(busPtr))->SetPixelColor(pix, col); break; case I_8266_U0_400_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; case I_8266_U1_400_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; case I_8266_DM_400_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; case I_8266_BB_400_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; case I_8266_U0_TM1_4: (static_cast(busPtr))->SetPixelColor(pix, col); break; case I_8266_U1_TM1_4: (static_cast(busPtr))->SetPixelColor(pix, col); break; case I_8266_DM_TM1_4: (static_cast(busPtr))->SetPixelColor(pix, col); break; case I_8266_BB_TM1_4: (static_cast(busPtr))->SetPixelColor(pix, col); break; case I_8266_U0_TM2_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; case I_8266_U1_TM2_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; case I_8266_DM_TM2_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; case I_8266_BB_TM2_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; case I_8266_U0_UCS_3: (static_cast(busPtr))->SetPixelColor(pix, Rgb48Color(RgbColor(col))); break; case I_8266_U1_UCS_3: (static_cast(busPtr))->SetPixelColor(pix, Rgb48Color(RgbColor(col))); break; case I_8266_DM_UCS_3: (static_cast(busPtr))->SetPixelColor(pix, Rgb48Color(RgbColor(col))); break; case I_8266_BB_UCS_3: (static_cast(busPtr))->SetPixelColor(pix, Rgb48Color(RgbColor(col))); break; case I_8266_U0_UCS_4: (static_cast(busPtr))->SetPixelColor(pix, Rgbw64Color(col)); break; case I_8266_U1_UCS_4: (static_cast(busPtr))->SetPixelColor(pix, Rgbw64Color(col)); break; case I_8266_DM_UCS_4: (static_cast(busPtr))->SetPixelColor(pix, Rgbw64Color(col)); break; case I_8266_BB_UCS_4: (static_cast(busPtr))->SetPixelColor(pix, Rgbw64Color(col)); break; case I_8266_U0_APA106_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; case I_8266_U1_APA106_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; case I_8266_DM_APA106_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; case I_8266_BB_APA106_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; case I_8266_U0_FW6_5: (static_cast(busPtr))->SetPixelColor(pix, RgbwwColor(col.R, col.G, col.B, cctWW, cctCW)); break; case I_8266_U1_FW6_5: (static_cast(busPtr))->SetPixelColor(pix, RgbwwColor(col.R, col.G, col.B, cctWW, cctCW)); break; case I_8266_DM_FW6_5: (static_cast(busPtr))->SetPixelColor(pix, RgbwwColor(col.R, col.G, col.B, cctWW, cctCW)); break; case I_8266_BB_FW6_5: (static_cast(busPtr))->SetPixelColor(pix, RgbwwColor(col.R, col.G, col.B, cctWW, cctCW)); break; case I_8266_U0_2805_5: (static_cast(busPtr))->SetPixelColor(pix, RgbwwColor(col.R, col.G, col.B, cctWW, cctCW)); break; case I_8266_U1_2805_5: (static_cast(busPtr))->SetPixelColor(pix, RgbwwColor(col.R, col.G, col.B, cctWW, cctCW)); break; case I_8266_DM_2805_5: (static_cast(busPtr))->SetPixelColor(pix, RgbwwColor(col.R, col.G, col.B, cctWW, cctCW)); break; case I_8266_BB_2805_5: (static_cast(busPtr))->SetPixelColor(pix, RgbwwColor(col.R, col.G, col.B, cctWW, cctCW)); break; case I_8266_U0_TM1914_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; case I_8266_U1_TM1914_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; case I_8266_DM_TM1914_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; case I_8266_BB_TM1914_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; case I_8266_U0_SM16825_5: (static_cast(busPtr))->SetPixelColor(pix, Rgbww80Color(col.R*257, col.G*257, col.B*257, cctWW*257, cctCW*257)); break; case I_8266_U1_SM16825_5: (static_cast(busPtr))->SetPixelColor(pix, Rgbww80Color(col.R*257, col.G*257, col.B*257, cctWW*257, cctCW*257)); break; case I_8266_DM_SM16825_5: (static_cast(busPtr))->SetPixelColor(pix, Rgbww80Color(col.R*257, col.G*257, col.B*257, cctWW*257, cctCW*257)); break; case I_8266_BB_SM16825_5: (static_cast(busPtr))->SetPixelColor(pix, Rgbww80Color(col.R*257, col.G*257, col.B*257, cctWW*257, cctCW*257)); break; #endif #ifdef ARDUINO_ARCH_ESP32 // RMT buses case I_32_RN_NEO_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; case I_32_RN_NEO_4: (static_cast(busPtr))->SetPixelColor(pix, col); break; case I_32_RN_400_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; case I_32_RN_TM1_4: (static_cast(busPtr))->SetPixelColor(pix, col); break; case I_32_RN_TM2_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; case I_32_RN_UCS_3: (static_cast(busPtr))->SetPixelColor(pix, Rgb48Color(RgbColor(col))); break; case I_32_RN_UCS_4: (static_cast(busPtr))->SetPixelColor(pix, Rgbw64Color(col)); break; case I_32_RN_APA106_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; case I_32_RN_FW6_5: (static_cast(busPtr))->SetPixelColor(pix, RgbwwColor(col.R, col.G, col.B, cctWW, cctCW)); break; case I_32_RN_2805_5: (static_cast(busPtr))->SetPixelColor(pix, RgbwwColor(col.R, col.G, col.B, cctWW, cctCW)); break; case I_32_RN_TM1914_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; case I_32_RN_SM16825_5: (static_cast(busPtr))->SetPixelColor(pix, Rgbww80Color(col.R*257, col.G*257, col.B*257, cctWW*257, cctCW*257)); break; // I2S1 bus or paralell buses #ifndef CONFIG_IDF_TARGET_ESP32C3 case I_32_I2_NEO_3: if (_useParallelI2S) (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); else (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; case I_32_I2_NEO_4: if (_useParallelI2S) (static_cast(busPtr))->SetPixelColor(pix, col); else (static_cast(busPtr))->SetPixelColor(pix, col); break; case I_32_I2_400_3: if (_useParallelI2S) (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); else (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; case I_32_I2_TM1_4: if (_useParallelI2S) (static_cast(busPtr))->SetPixelColor(pix, col); else (static_cast(busPtr))->SetPixelColor(pix, col); break; case I_32_I2_TM2_3: if (_useParallelI2S) (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); else (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; case I_32_I2_UCS_3: if (_useParallelI2S) (static_cast(busPtr))->SetPixelColor(pix, Rgb48Color(RgbColor(col))); else (static_cast(busPtr))->SetPixelColor(pix, Rgb48Color(RgbColor(col))); break; case I_32_I2_UCS_4: if (_useParallelI2S) (static_cast(busPtr))->SetPixelColor(pix, Rgbw64Color(col)); else (static_cast(busPtr))->SetPixelColor(pix, Rgbw64Color(col)); break; case I_32_I2_APA106_3: if (_useParallelI2S) (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); else (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; case I_32_I2_FW6_5: if (_useParallelI2S) (static_cast(busPtr))->SetPixelColor(pix, RgbwwColor(col.R, col.G, col.B, cctWW, cctCW)); else (static_cast(busPtr))->SetPixelColor(pix, RgbwwColor(col.R, col.G, col.B, cctWW, cctCW)); break; case I_32_I2_2805_5: if (_useParallelI2S) (static_cast(busPtr))->SetPixelColor(pix, RgbwwColor(col.R, col.G, col.B, cctWW, cctCW)); else (static_cast(busPtr))->SetPixelColor(pix, RgbwwColor(col.R, col.G, col.B, cctWW, cctCW)); break; case I_32_I2_TM1914_3: if (_useParallelI2S) (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); else (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; case I_32_I2_SM16825_5: if (_useParallelI2S) (static_cast(busPtr))->SetPixelColor(pix, Rgbww80Color(col.R*257, col.G*257, col.B*257, cctWW*257, cctCW*257)); else (static_cast(busPtr))->SetPixelColor(pix, Rgbww80Color(col.R*257, col.G*257, col.B*257, cctWW*257, cctCW*257)); break; #endif #endif case I_HS_DOT_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; case I_SS_DOT_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; case I_HS_LPD_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; case I_SS_LPD_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; case I_HS_LPO_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; case I_SS_LPO_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; case I_HS_WS1_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; case I_SS_WS1_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; case I_HS_P98_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; case I_SS_P98_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; } } [[gnu::hot]] static uint32_t getPixelColor(void* busPtr, uint8_t busType, uint16_t pix, uint8_t co) { RgbwColor col(0,0,0,0); switch (busType) { case I_NONE: break; #ifdef ESP8266 case I_8266_U0_NEO_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; case I_8266_U1_NEO_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; case I_8266_DM_NEO_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; case I_8266_BB_NEO_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; case I_8266_U0_NEO_4: col = (static_cast(busPtr))->GetPixelColor(pix); break; case I_8266_U1_NEO_4: col = (static_cast(busPtr))->GetPixelColor(pix); break; case I_8266_DM_NEO_4: col = (static_cast(busPtr))->GetPixelColor(pix); break; case I_8266_BB_NEO_4: col = (static_cast(busPtr))->GetPixelColor(pix); break; case I_8266_U0_400_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; case I_8266_U1_400_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; case I_8266_DM_400_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; case I_8266_BB_400_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; case I_8266_U0_TM1_4: col = (static_cast(busPtr))->GetPixelColor(pix); break; case I_8266_U1_TM1_4: col = (static_cast(busPtr))->GetPixelColor(pix); break; case I_8266_DM_TM1_4: col = (static_cast(busPtr))->GetPixelColor(pix); break; case I_8266_BB_TM1_4: col = (static_cast(busPtr))->GetPixelColor(pix); break; case I_8266_U0_TM2_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; case I_8266_U1_TM2_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; case I_8266_DM_TM2_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; case I_8266_BB_TM2_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; case I_8266_U0_UCS_3: { Rgb48Color c = (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R>>8,c.G>>8,c.B>>8,0); } break; case I_8266_U1_UCS_3: { Rgb48Color c = (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R>>8,c.G>>8,c.B>>8,0); } break; case I_8266_DM_UCS_3: { Rgb48Color c = (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R>>8,c.G>>8,c.B>>8,0); } break; case I_8266_BB_UCS_3: { Rgb48Color c = (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R>>8,c.G>>8,c.B>>8,0); } break; case I_8266_U0_UCS_4: { Rgbw64Color c = (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R>>8,c.G>>8,c.B>>8,c.W>>8); } break; case I_8266_U1_UCS_4: { Rgbw64Color c = (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R>>8,c.G>>8,c.B>>8,c.W>>8); } break; case I_8266_DM_UCS_4: { Rgbw64Color c = (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R>>8,c.G>>8,c.B>>8,c.W>>8); } break; case I_8266_BB_UCS_4: { Rgbw64Color c = (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R>>8,c.G>>8,c.B>>8,c.W>>8); } break; case I_8266_U0_APA106_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; case I_8266_U1_APA106_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; case I_8266_DM_APA106_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; case I_8266_BB_APA106_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; case I_8266_U0_FW6_5: { RgbwwColor c = (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R,c.G,c.B,max(c.WW,c.CW)); } break; // will not return original W case I_8266_U1_FW6_5: { RgbwwColor c = (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R,c.G,c.B,max(c.WW,c.CW)); } break; // will not return original W case I_8266_DM_FW6_5: { RgbwwColor c = (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R,c.G,c.B,max(c.WW,c.CW)); } break; // will not return original W case I_8266_BB_FW6_5: { RgbwwColor c = (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R,c.G,c.B,max(c.WW,c.CW)); } break; // will not return original W case I_8266_U0_2805_5: { RgbwwColor c = (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R,c.G,c.B,max(c.WW,c.CW)); } break; // will not return original W case I_8266_U1_2805_5: { RgbwwColor c = (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R,c.G,c.B,max(c.WW,c.CW)); } break; // will not return original W case I_8266_DM_2805_5: { RgbwwColor c = (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R,c.G,c.B,max(c.WW,c.CW)); } break; // will not return original W case I_8266_BB_2805_5: { RgbwwColor c = (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R,c.G,c.B,max(c.WW,c.CW)); } break; // will not return original W case I_8266_U0_TM1914_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; case I_8266_U1_TM1914_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; case I_8266_DM_TM1914_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; case I_8266_BB_TM1914_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; case I_8266_U0_SM16825_5: { Rgbww80Color c = (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R,c.G,c.B,max(c.WW,c.CW)); } break; // will not return original W case I_8266_U1_SM16825_5: { Rgbww80Color c = (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R,c.G,c.B,max(c.WW,c.CW)); } break; // will not return original W case I_8266_DM_SM16825_5: { Rgbww80Color c = (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R,c.G,c.B,max(c.WW,c.CW)); } break; // will not return original W case I_8266_BB_SM16825_5: { Rgbww80Color c = (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R,c.G,c.B,max(c.WW,c.CW)); } break; // will not return original W #endif #ifdef ARDUINO_ARCH_ESP32 // RMT buses case I_32_RN_NEO_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; case I_32_RN_NEO_4: col = (static_cast(busPtr))->GetPixelColor(pix); break; case I_32_RN_400_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; case I_32_RN_TM1_4: col = (static_cast(busPtr))->GetPixelColor(pix); break; case I_32_RN_TM2_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; case I_32_RN_UCS_3: { Rgb48Color c = (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R>>8,c.G>>8,c.B>>8,0); } break; case I_32_RN_UCS_4: { Rgbw64Color c = (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R>>8,c.G>>8,c.B>>8,c.W>>8); } break; case I_32_RN_APA106_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; case I_32_RN_FW6_5: { RgbwwColor c = (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R,c.G,c.B,max(c.WW,c.CW)); } break; // will not return original W case I_32_RN_2805_5: { RgbwwColor c = (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R,c.G,c.B,max(c.WW,c.CW)); } break; // will not return original W case I_32_RN_TM1914_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; case I_32_RN_SM16825_5: { Rgbww80Color c = (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R/257,c.G/257,c.B/257,max(c.WW,c.CW)/257); } break; // will not return original W // I2S1 bus or paralell buses #ifndef CONFIG_IDF_TARGET_ESP32C3 case I_32_I2_NEO_3: col = (_useParallelI2S) ? (static_cast(busPtr))->GetPixelColor(pix) : (static_cast(busPtr))->GetPixelColor(pix); break; case I_32_I2_NEO_4: col = (_useParallelI2S) ? (static_cast(busPtr))->GetPixelColor(pix) : (static_cast(busPtr))->GetPixelColor(pix); break; case I_32_I2_400_3: col = (_useParallelI2S) ? (static_cast(busPtr))->GetPixelColor(pix) : (static_cast(busPtr))->GetPixelColor(pix); break; case I_32_I2_TM1_4: col = (_useParallelI2S) ? (static_cast(busPtr))->GetPixelColor(pix) : (static_cast(busPtr))->GetPixelColor(pix); break; case I_32_I2_TM2_3: col = (_useParallelI2S) ? (static_cast(busPtr))->GetPixelColor(pix) : (static_cast(busPtr))->GetPixelColor(pix); break; case I_32_I2_UCS_3: { Rgb48Color c = (_useParallelI2S) ? (static_cast(busPtr))->GetPixelColor(pix) : (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R/257,c.G/257,c.B/257,0); } break; case I_32_I2_UCS_4: { Rgbw64Color c = (_useParallelI2S) ? (static_cast(busPtr))->GetPixelColor(pix) : (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R/257,c.G/257,c.B/257,c.W/257); } break; case I_32_I2_APA106_3: col = (_useParallelI2S) ? (static_cast(busPtr))->GetPixelColor(pix) : (static_cast(busPtr))->GetPixelColor(pix); break; case I_32_I2_FW6_5: { RgbwwColor c = (_useParallelI2S) ? (static_cast(busPtr))->GetPixelColor(pix) : (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R,c.G,c.B,max(c.WW,c.CW)); } break; // will not return original W case I_32_I2_2805_5: { RgbwwColor c = (_useParallelI2S) ? (static_cast(busPtr))->GetPixelColor(pix) : (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R,c.G,c.B,max(c.WW,c.CW)); } break; // will not return original W case I_32_I2_TM1914_3: col = (_useParallelI2S) ? (static_cast(busPtr))->GetPixelColor(pix) : (static_cast(busPtr))->GetPixelColor(pix); break; case I_32_I2_SM16825_5: { Rgbww80Color c = (_useParallelI2S) ? (static_cast(busPtr))->GetPixelColor(pix) : (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R/257,c.G/257,c.B/257,max(c.WW,c.CW)/257); } break; // will not return original W #endif #endif case I_HS_DOT_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; case I_SS_DOT_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; case I_HS_LPD_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; case I_SS_LPD_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; case I_HS_LPO_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; case I_SS_LPO_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; case I_HS_WS1_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; case I_SS_WS1_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; case I_HS_P98_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; case I_SS_P98_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; } // upper nibble contains W swap information uint8_t w = col.W; switch (co >> 4) { case 1: col.W = col.B; col.B = w; break; // swap W & B case 2: col.W = col.G; col.G = w; break; // swap W & G case 3: col.W = col.R; col.R = w; break; // swap W & R } switch (co & 0x0F) { // W G R B default: return ((col.W << 24) | (col.G << 8) | (col.R << 16) | (col.B)); //0 = GRB, default case 1: return ((col.W << 24) | (col.R << 8) | (col.G << 16) | (col.B)); //1 = RGB, common for WS2811 case 2: return ((col.W << 24) | (col.B << 8) | (col.R << 16) | (col.G)); //2 = BRG case 3: return ((col.W << 24) | (col.B << 8) | (col.G << 16) | (col.R)); //3 = RBG case 4: return ((col.W << 24) | (col.R << 8) | (col.B << 16) | (col.G)); //4 = BGR case 5: return ((col.W << 24) | (col.G << 8) | (col.B << 16) | (col.R)); //5 = GBR } return 0; } static void cleanup(void* busPtr, uint8_t busType) { if (busPtr == nullptr) return; switch (busType) { case I_NONE: break; #ifdef ESP8266 case I_8266_U0_NEO_3: delete (static_cast(busPtr)); break; case I_8266_U1_NEO_3: delete (static_cast(busPtr)); break; case I_8266_DM_NEO_3: delete (static_cast(busPtr)); break; case I_8266_BB_NEO_3: delete (static_cast(busPtr)); break; case I_8266_U0_NEO_4: delete (static_cast(busPtr)); break; case I_8266_U1_NEO_4: delete (static_cast(busPtr)); break; case I_8266_DM_NEO_4: delete (static_cast(busPtr)); break; case I_8266_BB_NEO_4: delete (static_cast(busPtr)); break; case I_8266_U0_400_3: delete (static_cast(busPtr)); break; case I_8266_U1_400_3: delete (static_cast(busPtr)); break; case I_8266_DM_400_3: delete (static_cast(busPtr)); break; case I_8266_BB_400_3: delete (static_cast(busPtr)); break; case I_8266_U0_TM1_4: delete (static_cast(busPtr)); break; case I_8266_U1_TM1_4: delete (static_cast(busPtr)); break; case I_8266_DM_TM1_4: delete (static_cast(busPtr)); break; case I_8266_BB_TM1_4: delete (static_cast(busPtr)); break; case I_8266_U0_TM2_3: delete (static_cast(busPtr)); break; case I_8266_U1_TM2_3: delete (static_cast(busPtr)); break; case I_8266_DM_TM2_3: delete (static_cast(busPtr)); break; case I_8266_BB_TM2_3: delete (static_cast(busPtr)); break; case I_8266_U0_UCS_3: delete (static_cast(busPtr)); break; case I_8266_U1_UCS_3: delete (static_cast(busPtr)); break; case I_8266_DM_UCS_3: delete (static_cast(busPtr)); break; case I_8266_BB_UCS_3: delete (static_cast(busPtr)); break; case I_8266_U0_UCS_4: delete (static_cast(busPtr)); break; case I_8266_U1_UCS_4: delete (static_cast(busPtr)); break; case I_8266_DM_UCS_4: delete (static_cast(busPtr)); break; case I_8266_BB_UCS_4: delete (static_cast(busPtr)); break; case I_8266_U0_APA106_3: delete (static_cast(busPtr)); break; case I_8266_U1_APA106_3: delete (static_cast(busPtr)); break; case I_8266_DM_APA106_3: delete (static_cast(busPtr)); break; case I_8266_BB_APA106_3: delete (static_cast(busPtr)); break; case I_8266_U0_FW6_5: delete (static_cast(busPtr)); break; case I_8266_U1_FW6_5: delete (static_cast(busPtr)); break; case I_8266_DM_FW6_5: delete (static_cast(busPtr)); break; case I_8266_BB_FW6_5: delete (static_cast(busPtr)); break; case I_8266_U0_2805_5: delete (static_cast(busPtr)); break; case I_8266_U1_2805_5: delete (static_cast(busPtr)); break; case I_8266_DM_2805_5: delete (static_cast(busPtr)); break; case I_8266_BB_2805_5: delete (static_cast(busPtr)); break; case I_8266_U0_TM1914_3: delete (static_cast(busPtr)); break; case I_8266_U1_TM1914_3: delete (static_cast(busPtr)); break; case I_8266_DM_TM1914_3: delete (static_cast(busPtr)); break; case I_8266_BB_TM1914_3: delete (static_cast(busPtr)); break; case I_8266_U0_SM16825_5: delete (static_cast(busPtr)); break; case I_8266_U1_SM16825_5: delete (static_cast(busPtr)); break; case I_8266_DM_SM16825_5: delete (static_cast(busPtr)); break; case I_8266_BB_SM16825_5: delete (static_cast(busPtr)); break; #endif #ifdef ARDUINO_ARCH_ESP32 // RMT buses case I_32_RN_NEO_3: delete (static_cast(busPtr)); break; case I_32_RN_NEO_4: delete (static_cast(busPtr)); break; case I_32_RN_400_3: delete (static_cast(busPtr)); break; case I_32_RN_TM1_4: delete (static_cast(busPtr)); break; case I_32_RN_TM2_3: delete (static_cast(busPtr)); break; case I_32_RN_UCS_3: delete (static_cast(busPtr)); break; case I_32_RN_UCS_4: delete (static_cast(busPtr)); break; case I_32_RN_APA106_3: delete (static_cast(busPtr)); break; case I_32_RN_FW6_5: delete (static_cast(busPtr)); break; case I_32_RN_2805_5: delete (static_cast(busPtr)); break; case I_32_RN_TM1914_3: delete (static_cast(busPtr)); break; case I_32_RN_SM16825_5: delete (static_cast(busPtr)); break; // I2S1 bus or paralell buses #ifndef CONFIG_IDF_TARGET_ESP32C3 case I_32_I2_NEO_3: if (_useParallelI2S) delete (static_cast(busPtr)); else delete (static_cast(busPtr)); break; case I_32_I2_NEO_4: if (_useParallelI2S) delete (static_cast(busPtr)); else delete (static_cast(busPtr)); break; case I_32_I2_400_3: if (_useParallelI2S) delete (static_cast(busPtr)); else delete (static_cast(busPtr)); break; case I_32_I2_TM1_4: if (_useParallelI2S) delete (static_cast(busPtr)); else delete (static_cast(busPtr)); break; case I_32_I2_TM2_3: if (_useParallelI2S) delete (static_cast(busPtr)); else delete (static_cast(busPtr)); break; case I_32_I2_UCS_3: if (_useParallelI2S) delete (static_cast(busPtr)); else delete (static_cast(busPtr)); break; case I_32_I2_UCS_4: if (_useParallelI2S) delete (static_cast(busPtr)); else delete (static_cast(busPtr)); break; case I_32_I2_APA106_3: if (_useParallelI2S) delete (static_cast(busPtr)); else delete (static_cast(busPtr)); break; case I_32_I2_FW6_5: if (_useParallelI2S) delete (static_cast(busPtr)); else delete (static_cast(busPtr)); break; case I_32_I2_2805_5: if (_useParallelI2S) delete (static_cast(busPtr)); else delete (static_cast(busPtr)); break; case I_32_I2_TM1914_3: if (_useParallelI2S) delete (static_cast(busPtr)); else delete (static_cast(busPtr)); break; case I_32_I2_SM16825_5: if (_useParallelI2S) delete (static_cast(busPtr)); else delete (static_cast(busPtr)); break; #endif #endif case I_HS_DOT_3: delete (static_cast(busPtr)); break; case I_SS_DOT_3: delete (static_cast(busPtr)); break; case I_HS_LPD_3: delete (static_cast(busPtr)); break; case I_SS_LPD_3: delete (static_cast(busPtr)); break; case I_HS_LPO_3: delete (static_cast(busPtr)); break; case I_SS_LPO_3: delete (static_cast(busPtr)); break; case I_HS_WS1_3: delete (static_cast(busPtr)); break; case I_SS_WS1_3: delete (static_cast(busPtr)); break; case I_HS_P98_3: delete (static_cast(busPtr)); break; case I_SS_P98_3: delete (static_cast(busPtr)); break; } } static unsigned getDataSize(void* busPtr, uint8_t busType) { unsigned size = 0; #ifdef ARDUINO_ARCH_ESP32 size = 100; // ~100bytes for NPB internal structures (measured for both I2S and RMT, much smaller and more variable on ESP8266) #endif switch (busType) { case I_NONE: break; #ifdef ESP8266 case I_8266_U0_NEO_3: size = (static_cast(busPtr))->PixelsSize(); break; case I_8266_U1_NEO_3: size = (static_cast(busPtr))->PixelsSize(); break; case I_8266_DM_NEO_3: size = (static_cast(busPtr))->PixelsSize()*5; break; case I_8266_BB_NEO_3: size = (static_cast(busPtr))->PixelsSize(); break; case I_8266_U0_NEO_4: size = (static_cast(busPtr))->PixelsSize(); break; case I_8266_U1_NEO_4: size = (static_cast(busPtr))->PixelsSize(); break; case I_8266_DM_NEO_4: size = (static_cast(busPtr))->PixelsSize()*5; break; case I_8266_BB_NEO_4: size = (static_cast(busPtr))->PixelsSize(); break; case I_8266_U0_400_3: size = (static_cast(busPtr))->PixelsSize(); break; case I_8266_U1_400_3: size = (static_cast(busPtr))->PixelsSize(); break; case I_8266_DM_400_3: size = (static_cast(busPtr))->PixelsSize()*5; break; case I_8266_BB_400_3: size = (static_cast(busPtr))->PixelsSize(); break; case I_8266_U0_TM1_4: size = (static_cast(busPtr))->PixelsSize(); break; case I_8266_U1_TM1_4: size = (static_cast(busPtr))->PixelsSize(); break; case I_8266_DM_TM1_4: size = (static_cast(busPtr))->PixelsSize()*5; break; case I_8266_BB_TM1_4: size = (static_cast(busPtr))->PixelsSize(); break; case I_8266_U0_TM2_3: size = (static_cast(busPtr))->PixelsSize(); break; case I_8266_U1_TM2_3: size = (static_cast(busPtr))->PixelsSize(); break; case I_8266_DM_TM2_3: size = (static_cast(busPtr))->PixelsSize()*5; break; case I_8266_BB_TM2_3: size = (static_cast(busPtr))->PixelsSize(); break; case I_8266_U0_UCS_3: size = (static_cast(busPtr))->PixelsSize(); break; case I_8266_U1_UCS_3: size = (static_cast(busPtr))->PixelsSize(); break; case I_8266_DM_UCS_3: size = (static_cast(busPtr))->PixelsSize()*5; break; case I_8266_BB_UCS_3: size = (static_cast(busPtr))->PixelsSize(); break; case I_8266_U0_UCS_4: size = (static_cast(busPtr))->PixelsSize(); break; case I_8266_U1_UCS_4: size = (static_cast(busPtr))->PixelsSize(); break; case I_8266_DM_UCS_4: size = (static_cast(busPtr))->PixelsSize()*5; break; case I_8266_BB_UCS_4: size = (static_cast(busPtr))->PixelsSize(); break; case I_8266_U0_APA106_3: size = (static_cast(busPtr))->PixelsSize(); break; case I_8266_U1_APA106_3: size = (static_cast(busPtr))->PixelsSize(); break; case I_8266_DM_APA106_3: size = (static_cast(busPtr))->PixelsSize()*5; break; case I_8266_BB_APA106_3: size = (static_cast(busPtr))->PixelsSize(); break; case I_8266_U0_FW6_5: size = (static_cast(busPtr))->PixelsSize(); break; case I_8266_U1_FW6_5: size = (static_cast(busPtr))->PixelsSize(); break; case I_8266_DM_FW6_5: size = (static_cast(busPtr))->PixelsSize()*5; break; case I_8266_BB_FW6_5: size = (static_cast(busPtr))->PixelsSize(); break; case I_8266_U0_2805_5: size = (static_cast(busPtr))->PixelsSize(); break; case I_8266_U1_2805_5: size = (static_cast(busPtr))->PixelsSize(); break; case I_8266_DM_2805_5: size = (static_cast(busPtr))->PixelsSize()*5; break; case I_8266_BB_2805_5: size = (static_cast(busPtr))->PixelsSize(); break; case I_8266_U0_TM1914_3: size = (static_cast(busPtr))->PixelsSize(); break; case I_8266_U1_TM1914_3: size = (static_cast(busPtr))->PixelsSize(); break; case I_8266_DM_TM1914_3: size = (static_cast(busPtr))->PixelsSize()*5; break; case I_8266_BB_TM1914_3: size = (static_cast(busPtr))->PixelsSize(); break; case I_8266_U0_SM16825_5: size = (static_cast(busPtr))->PixelsSize(); break; case I_8266_U1_SM16825_5: size = (static_cast(busPtr))->PixelsSize(); break; case I_8266_DM_SM16825_5: size = (static_cast(busPtr))->PixelsSize()*5; break; case I_8266_BB_SM16825_5: size = (static_cast(busPtr))->PixelsSize(); break; #endif #ifdef ARDUINO_ARCH_ESP32 // RMT buses (front + back + small system managed RMT) case I_32_RN_NEO_3: size += (static_cast(busPtr))->PixelsSize()*2; break; case I_32_RN_NEO_4: size += (static_cast(busPtr))->PixelsSize()*2; break; case I_32_RN_400_3: size += (static_cast(busPtr))->PixelsSize()*2; break; case I_32_RN_TM1_4: size += (static_cast(busPtr))->PixelsSize()*2; break; case I_32_RN_TM2_3: size += (static_cast(busPtr))->PixelsSize()*2; break; case I_32_RN_UCS_3: size += (static_cast(busPtr))->PixelsSize()*2; break; case I_32_RN_UCS_4: size += (static_cast(busPtr))->PixelsSize()*2; break; case I_32_RN_APA106_3: size += (static_cast(busPtr))->PixelsSize()*2; break; case I_32_RN_FW6_5: size += (static_cast(busPtr))->PixelsSize()*2; break; case I_32_RN_2805_5: size += (static_cast(busPtr))->PixelsSize()*2; break; case I_32_RN_TM1914_3: size += (static_cast(busPtr))->PixelsSize()*2; break; case I_32_RN_SM16825_5: size += (static_cast(busPtr))->PixelsSize()*2; break; // I2S1 bus or paralell buses (front + DMA; DMA = front * cadence, aligned to 4 bytes) not: for parallel I2S only the largest bus counts for DMA memory, this is not done correctly here, also assumes 3-step cadence #ifndef CONFIG_IDF_TARGET_ESP32C3 case I_32_I2_NEO_3: size += (_useParallelI2S) ? (static_cast(busPtr))->PixelsSize()*4 : (static_cast(busPtr))->PixelsSize()*4; break; case I_32_I2_NEO_4: size += (_useParallelI2S) ? (static_cast(busPtr))->PixelsSize()*4 : (static_cast(busPtr))->PixelsSize()*4; break; case I_32_I2_400_3: size += (_useParallelI2S) ? (static_cast(busPtr))->PixelsSize()*4 : (static_cast(busPtr))->PixelsSize()*4; break; case I_32_I2_TM1_4: size += (_useParallelI2S) ? (static_cast(busPtr))->PixelsSize()*4 : (static_cast(busPtr))->PixelsSize()*4; break; case I_32_I2_TM2_3: size += (_useParallelI2S) ? (static_cast(busPtr))->PixelsSize()*4 : (static_cast(busPtr))->PixelsSize()*4; break; case I_32_I2_UCS_3: size += (_useParallelI2S) ? (static_cast(busPtr))->PixelsSize()*4 : (static_cast(busPtr))->PixelsSize()*4; break; case I_32_I2_UCS_4: size += (_useParallelI2S) ? (static_cast(busPtr))->PixelsSize()*4 : (static_cast(busPtr))->PixelsSize()*4; break; case I_32_I2_APA106_3: size += (_useParallelI2S) ? (static_cast(busPtr))->PixelsSize()*4 : (static_cast(busPtr))->PixelsSize()*4; break; case I_32_I2_FW6_5: size += (_useParallelI2S) ? (static_cast(busPtr))->PixelsSize()*4 : (static_cast(busPtr))->PixelsSize()*4; break; case I_32_I2_2805_5: size += (_useParallelI2S) ? (static_cast(busPtr))->PixelsSize()*4 : (static_cast(busPtr))->PixelsSize()*4; break; case I_32_I2_TM1914_3: size += (_useParallelI2S) ? (static_cast(busPtr))->PixelsSize()*4 : (static_cast(busPtr))->PixelsSize()*4; break; case I_32_I2_SM16825_5: size += (_useParallelI2S) ? (static_cast(busPtr))->PixelsSize()*4 : (static_cast(busPtr))->PixelsSize()*4; break; #endif #endif case I_HS_DOT_3: size = (static_cast(busPtr))->PixelsSize()*2; break; case I_SS_DOT_3: size = (static_cast(busPtr))->PixelsSize()*2; break; case I_HS_LPD_3: size = (static_cast(busPtr))->PixelsSize()*2; break; case I_SS_LPD_3: size = (static_cast(busPtr))->PixelsSize()*2; break; case I_HS_LPO_3: size = (static_cast(busPtr))->PixelsSize()*2; break; case I_SS_LPO_3: size = (static_cast(busPtr))->PixelsSize()*2; break; case I_HS_WS1_3: size = (static_cast(busPtr))->PixelsSize()*2; break; case I_SS_WS1_3: size = (static_cast(busPtr))->PixelsSize()*2; break; case I_HS_P98_3: size = (static_cast(busPtr))->PixelsSize()*2; break; case I_SS_P98_3: size = (static_cast(busPtr))->PixelsSize()*2; break; } return size; } static unsigned memUsage(unsigned count, unsigned busType) { unsigned size = count*3; // let's assume 3 channels, we will add count or 2*count below for 4 channels or 5 channels switch (busType) { case I_NONE: size = 0; break; #ifdef ESP8266 // UART methods have front + back buffers + small UART case I_8266_U0_NEO_4 : // fallthrough case I_8266_U1_NEO_4 : // fallthrough case I_8266_BB_NEO_4 : // fallthrough case I_8266_U0_TM1_4 : // fallthrough case I_8266_U1_TM1_4 : // fallthrough case I_8266_BB_TM1_4 : size = (size + count); break; // 4 channels case I_8266_U0_UCS_3 : // fallthrough case I_8266_U1_UCS_3 : // fallthrough case I_8266_BB_UCS_3 : size *= 2; break; // 16 bit case I_8266_U0_UCS_4 : // fallthrough case I_8266_U1_UCS_4 : // fallthrough case I_8266_BB_UCS_4 : size = (size + count)*2; break; // 16 bit 4 channels case I_8266_U0_FW6_5 : // fallthrough case I_8266_U1_FW6_5 : // fallthrough case I_8266_BB_FW6_5 : // fallthrough case I_8266_U0_2805_5 : // fallthrough case I_8266_U1_2805_5 : // fallthrough case I_8266_BB_2805_5 : size = (size + 2*count); break; // 5 channels case I_8266_U0_SM16825_5: // fallthrough case I_8266_U1_SM16825_5: // fallthrough case I_8266_BB_SM16825_5: size = (size + 2*count)*2; break; // 16 bit 5 channels // DMA methods have front + DMA buffer = ((1+(3+1)) * channels; exact value is a bit of mistery - needs a dig into NPB) case I_8266_DM_NEO_3 : // fallthrough case I_8266_DM_400_3 : // fallthrough case I_8266_DM_TM2_3 : // fallthrough case I_8266_DM_APA106_3 : // fallthrough case I_8266_DM_TM1914_3 : size *= 5; break; case I_8266_DM_NEO_4 : // fallthrough case I_8266_DM_TM1_4 : size = (size + count)*5; break; case I_8266_DM_UCS_3 : size *= 2*5; break; case I_8266_DM_UCS_4 : size = (size + count)*2*5; break; case I_8266_DM_FW6_5 : // fallthrough case I_8266_DM_2805_5 : size = (size + 2*count)*5; break; case I_8266_DM_SM16825_5: size = (size + 2*count)*2*5; break; #else // note: RMT and I2S buses use ~100 bytes of internal NPB memory each, not included here for simplicity // RMT buses (1x front and 1x back buffer, does not include small RMT buffer) case I_32_RN_NEO_4 : // fallthrough case I_32_RN_TM1_4 : size = (size + count)*2; break; // 4 channels case I_32_RN_UCS_3 : size *= 2*2; break; // 16bit case I_32_RN_UCS_4 : size = (size + count)*2*2; break; // 16bit, 4 channels case I_32_RN_FW6_5 : // fallthrough case I_32_RN_2805_5 : size = (size + 2*count)*2; break; // 5 channels case I_32_RN_SM16825_5: size = (size + 2*count)*2*2; break; // 16bit, 5 channels // I2S bus or paralell I2S buses (1x front, does not include DMA buffer which is front*cadence, a bit(?) more for LCD) #ifndef CONFIG_IDF_TARGET_ESP32C3 case I_32_I2_NEO_3 : // fallthrough case I_32_I2_400_3 : // fallthrough case I_32_I2_TM2_3 : // fallthrough case I_32_I2_APA106_3 : break; // do nothing, I2S uses single buffer + DMA buffer case I_32_I2_NEO_4 : // fallthrough case I_32_I2_TM1_4 : size = (size + count); break; // 4 channels case I_32_I2_UCS_3 : size *= 2; break; // 16 bit case I_32_I2_UCS_4 : size = (size + count)*2; break; // 16 bit, 4 channels case I_32_I2_FW6_5 : // fallthrough case I_32_I2_2805_5 : size = (size + 2*count); break; // 5 channels case I_32_I2_SM16825_5: size = (size + 2*count)*2; break; // 16 bit, 5 channels #endif default : size *= 2; break; // everything else uses 2 buffers #endif } return size; } #ifndef ESP8266 // Reset channel tracking (call before adding buses) static void resetChannelTracking() { _useParallelI2S = false; _rmtChannelsAssigned = 0; _rmtChannel = 0; _i2sChannelsAssigned = 0; _parallelBusItype = I_NONE; _2PchannelsAssigned = 0; } #endif // reserves and gives back the internal type index (I_XX_XXX_X above) for the input based on bus type and pins static uint8_t getI(uint8_t busType, const uint8_t* pins, uint8_t driverPreference) { if (!Bus::isDigital(busType)) return I_NONE; uint8_t t = I_NONE; if (Bus::is2Pin(busType)) { //SPI LED chips bool isHSPI = false; #ifdef ESP8266 if (pins[0] == P_8266_HS_MOSI && pins[1] == P_8266_HS_CLK) isHSPI = true; #else if (_2PchannelsAssigned == 0) isHSPI = true; // first 2-pin channel uses hardware SPI _2PchannelsAssigned++; #endif switch (busType) { case TYPE_APA102: t = I_SS_DOT_3; break; case TYPE_LPD8806: t = I_SS_LPD_3; break; case TYPE_LPD6803: t = I_SS_LPO_3; break; case TYPE_WS2801: t = I_SS_WS1_3; break; case TYPE_P9813: t = I_SS_P98_3; break; } if (t > I_NONE && isHSPI) t--; //hardware SPI has one smaller ID than software } else { #ifdef ESP8266 uint8_t offset = pins[0] -1; //for driver: 0 = uart0, 1 = uart1, 2 = dma, 3 = bitbang if (offset > 3) offset = 3; switch (busType) { case TYPE_WS2812_1CH_X3: case TYPE_WS2812_2CH_X3: case TYPE_WS2812_RGB: case TYPE_WS2812_WWA: t = I_8266_U0_NEO_3 + offset; break; case TYPE_SK6812_RGBW: t = I_8266_U0_NEO_4 + offset; break; case TYPE_WS2811_400KHZ: t = I_8266_U0_400_3 + offset; break; case TYPE_TM1814: t = I_8266_U0_TM1_4 + offset; break; case TYPE_TM1829: t = I_8266_U0_TM2_3 + offset; break; case TYPE_UCS8903: t = I_8266_U0_UCS_3 + offset; break; case TYPE_UCS8904: t = I_8266_U0_UCS_4 + offset; break; case TYPE_APA106: t = I_8266_U0_APA106_3 + offset; break; case TYPE_FW1906: t = I_8266_U0_FW6_5 + offset; break; case TYPE_WS2805: t = I_8266_U0_2805_5 + offset; break; case TYPE_TM1914: t = I_8266_U0_TM1914_3 + offset; break; case TYPE_SM16825: t = I_8266_U0_SM16825_5 + offset; break; } #else //ESP32 // dynamic channel allocation based on driver preference // determine which driver to use based on preference and availability. First I2S bus locks the I2S type, all subsequent I2S buses are assigned the same type (hardware restriction) uint8_t offset = 0; // 0 = RMT, 1 = I2S/LCD if (driverPreference == 0 && _rmtChannelsAssigned < WLED_MAX_RMT_CHANNELS) { _rmtChannelsAssigned++; } else if (_i2sChannelsAssigned < WLED_MAX_I2S_CHANNELS) { offset = 1; // I2S requested or RMT full _i2sChannelsAssigned++; } else { return I_NONE; // No channels available } // Now determine actual bus type with the chosen offset switch (busType) { case TYPE_WS2812_1CH_X3: case TYPE_WS2812_2CH_X3: case TYPE_WS2812_RGB: case TYPE_WS2812_WWA: t = I_32_RN_NEO_3 + offset; break; case TYPE_SK6812_RGBW: t = I_32_RN_NEO_4 + offset; break; case TYPE_WS2811_400KHZ: t = I_32_RN_400_3 + offset; break; case TYPE_TM1814: t = I_32_RN_TM1_4 + offset; break; case TYPE_TM1829: t = I_32_RN_TM2_3 + offset; break; case TYPE_UCS8903: t = I_32_RN_UCS_3 + offset; break; case TYPE_UCS8904: t = I_32_RN_UCS_4 + offset; break; case TYPE_APA106: t = I_32_RN_APA106_3 + offset; break; case TYPE_FW1906: t = I_32_RN_FW6_5 + offset; break; case TYPE_WS2805: t = I_32_RN_2805_5 + offset; break; case TYPE_TM1914: t = I_32_RN_TM1914_3 + offset; break; case TYPE_SM16825: t = I_32_RN_SM16825_5 + offset; break; } // If using parallel I2S, set the type accordingly if (_i2sChannelsAssigned == 1 && offset == 1) { // first I2S channel request, lock the type _parallelBusItype = t; #ifdef CONFIG_IDF_TARGET_ESP32S3 _useParallelI2S = true; // ESP32-S3 always uses parallel I2S (LCD method) #endif } else if (offset == 1) { // not first I2S channel, use locked type and enable parallel flag _useParallelI2S = true; t = _parallelBusItype; } #endif } return t; } }; #endif ================================================ FILE: wled00/button.cpp ================================================ #include "wled.h" /* * Physical IO */ #define WLED_DEBOUNCE_THRESHOLD 50 // only consider button input of at least 50ms as valid (debouncing) #define WLED_LONG_PRESS 600 // long press if button is released after held for at least 600ms #define WLED_DOUBLE_PRESS 350 // double press if another press within 350ms after a short press #define WLED_LONG_REPEATED_ACTION 400 // how often a repeated action (e.g. dimming) is fired on long press on button IDs >0 #define WLED_LONG_AP 5000 // how long button 0 needs to be held to activate WLED-AP #define WLED_LONG_FACTORY_RESET 10000 // how long button 0 needs to be held to trigger a factory reset #define WLED_LONG_BRI_STEPS 16 // how much to increase/decrease the brightness with each long press repetition static const char _mqtt_topic_button[] PROGMEM = "%s/button/%d"; // optimize flash usage static bool buttonBriDirection = false; // true: increase brightness, false: decrease brightness void shortPressAction(uint8_t b) { if (!buttons[b].macroButton) { switch (b) { case 0: toggleOnOff(); stateUpdated(CALL_MODE_BUTTON); break; case 1: ++effectCurrent %= strip.getModeCount(); stateChanged = true; colorUpdated(CALL_MODE_BUTTON); break; } } else { applyPreset(buttons[b].macroButton, CALL_MODE_BUTTON_PRESET); } #ifndef WLED_DISABLE_MQTT // publish MQTT message if (buttonPublishMqtt && WLED_MQTT_CONNECTED) { char subuf[MQTT_MAX_TOPIC_LEN + 32]; sprintf_P(subuf, _mqtt_topic_button, mqttDeviceTopic, (int)b); mqtt->publish(subuf, 0, false, "short"); } #endif } void longPressAction(uint8_t b) { if (!buttons[b].macroLongPress) { switch (b) { case 0: setRandomColor(colPri); colorUpdated(CALL_MODE_BUTTON); break; case 1: if(buttonBriDirection) { if (bri == 255) break; // avoid unnecessary updates to brightness if (bri >= 255 - WLED_LONG_BRI_STEPS) bri = 255; else bri += WLED_LONG_BRI_STEPS; } else { if (bri == 1) break; // avoid unnecessary updates to brightness if (bri <= WLED_LONG_BRI_STEPS) bri = 1; else bri -= WLED_LONG_BRI_STEPS; } stateUpdated(CALL_MODE_BUTTON); buttons[b].pressedTime = millis(); break; // repeatable action } } else { applyPreset(buttons[b].macroLongPress, CALL_MODE_BUTTON_PRESET); } #ifndef WLED_DISABLE_MQTT // publish MQTT message if (buttonPublishMqtt && WLED_MQTT_CONNECTED) { char subuf[MQTT_MAX_TOPIC_LEN + 32]; sprintf_P(subuf, _mqtt_topic_button, mqttDeviceTopic, (int)b); mqtt->publish(subuf, 0, false, "long"); } #endif } void doublePressAction(uint8_t b) { if (!buttons[b].macroDoublePress) { switch (b) { //case 0: toggleOnOff(); colorUpdated(CALL_MODE_BUTTON); break; //instant short press on button 0 if no macro set case 1: ++effectPalette %= getPaletteCount(); colorUpdated(CALL_MODE_BUTTON); break; } } else { applyPreset(buttons[b].macroDoublePress, CALL_MODE_BUTTON_PRESET); } #ifndef WLED_DISABLE_MQTT // publish MQTT message if (buttonPublishMqtt && WLED_MQTT_CONNECTED) { char subuf[MQTT_MAX_TOPIC_LEN + 32]; sprintf_P(subuf, _mqtt_topic_button, mqttDeviceTopic, (int)b); mqtt->publish(subuf, 0, false, "double"); } #endif } bool isButtonPressed(uint8_t b) { if (buttons[b].pin < 0) return false; unsigned pin = buttons[b].pin; switch (buttons[b].type) { case BTN_TYPE_NONE: case BTN_TYPE_RESERVED: break; case BTN_TYPE_PUSH: case BTN_TYPE_SWITCH: if (digitalRead(pin) == LOW) return true; break; case BTN_TYPE_PUSH_ACT_HIGH: case BTN_TYPE_PIR_SENSOR: if (digitalRead(pin) == HIGH) return true; break; case BTN_TYPE_TOUCH: case BTN_TYPE_TOUCH_SWITCH: #if defined(ARDUINO_ARCH_ESP32) && !defined(CONFIG_IDF_TARGET_ESP32C3) #ifdef SOC_TOUCH_VERSION_2 //ESP32 S2 and S3 provide a function to check touch state (state is updated in interrupt) if (touchInterruptGetLastStatus(pin)) return true; #else if (digitalPinToTouchChannel(pin) >= 0 && touchRead(pin) <= touchThreshold) return true; #endif #endif break; } return false; } void handleSwitch(uint8_t b) { // isButtonPressed() handles inverted/noninverted logic if (buttons[b].pressedBefore != isButtonPressed(b)) { DEBUG_PRINTF_P(PSTR("Switch: State changed %u\n"), b); buttons[b].pressedTime = millis(); buttons[b].pressedBefore = !buttons[b].pressedBefore; // toggle pressed state } if (buttons[b].longPressed == buttons[b].pressedBefore) return; if (millis() - buttons[b].pressedTime > WLED_DEBOUNCE_THRESHOLD) { //fire edge event only after 50ms without change (debounce) DEBUG_PRINTF_P(PSTR("Switch: Activating %u\n"), b); if (!buttons[b].pressedBefore) { // on -> off DEBUG_PRINTF_P(PSTR("Switch: On -> Off (%u)\n"), b); if (buttons[b].macroButton) applyPreset(buttons[b].macroButton, CALL_MODE_BUTTON_PRESET); else { //turn on if (!bri) {toggleOnOff(); stateUpdated(CALL_MODE_BUTTON);} } } else { // off -> on DEBUG_PRINTF_P(PSTR("Switch: Off -> On (%u)\n"), b); if (buttons[b].macroLongPress) applyPreset(buttons[b].macroLongPress, CALL_MODE_BUTTON_PRESET); else { //turn off if (bri) {toggleOnOff(); stateUpdated(CALL_MODE_BUTTON);} } } #ifndef WLED_DISABLE_MQTT // publish MQTT message if (buttonPublishMqtt && WLED_MQTT_CONNECTED) { char subuf[MQTT_MAX_TOPIC_LEN + 32]; if (buttons[b].type == BTN_TYPE_PIR_SENSOR) sprintf_P(subuf, PSTR("%s/motion/%d"), mqttDeviceTopic, (int)b); else sprintf_P(subuf, _mqtt_topic_button, mqttDeviceTopic, (int)b); mqtt->publish(subuf, 0, false, !buttons[b].pressedBefore ? "off" : "on"); } #endif buttons[b].longPressed = buttons[b].pressedBefore; //save the last "long term" switch state } } #define ANALOG_BTN_READ_CYCLE 250 // min time between two analog reading cycles #define STRIP_WAIT_TIME 6 // max wait time in case of strip.isUpdating() #define POT_SMOOTHING 0.25f // smoothing factor for raw potentiometer readings #define POT_SENSITIVITY 4 // changes below this amount are noise (POT scratching, or ADC noise) void handleAnalog(uint8_t b) { static uint8_t oldRead[WLED_MAX_BUTTONS] = {0}; static float filteredReading[WLED_MAX_BUTTONS] = {0.0f}; unsigned rawReading; // raw value from analogRead, scaled to 12bit DEBUG_PRINTF_P(PSTR("Analog: Reading button %u\n"), b); #ifdef ESP8266 rawReading = analogRead(A0) << 2; // convert 10bit read to 12bit #else if ((buttons[b].pin < 0) /*|| (digitalPinToAnalogChannel(buttons[b].pin) < 0)*/) return; // pin must support analog ADC - newer esp32 frameworks throw lots of warnings otherwise rawReading = analogRead(buttons[b].pin); // collect at full 12bit resolution #endif yield(); // keep WiFi task running - analog read may take several millis on ESP8266 filteredReading[b] += POT_SMOOTHING * ((float(rawReading) / 16.0f) - filteredReading[b]); // filter raw input, and scale to [0..255] unsigned aRead = max(min(int(filteredReading[b]), 255), 0); // squash into 8bit if (aRead <= POT_SENSITIVITY) aRead = 0; // make sure that 0 and 255 are used if (aRead >= 255-POT_SENSITIVITY) aRead = 255; if (buttons[b].type == BTN_TYPE_ANALOG_INVERTED) aRead = 255 - aRead; // remove noise & reduce frequency of UI updates if (abs(int(aRead) - int(oldRead[b])) <= POT_SENSITIVITY) return; // no significant change in reading DEBUG_PRINTF_P(PSTR("Analog: Raw = %u\n"), rawReading); DEBUG_PRINTF_P(PSTR(" Filtered = %u\n"), aRead); // Unomment the next lines if you still see flickering related to potentiometer // This waits until strip finishes updating (why: strip was not updating at the start of handleButton() but may have started during analogRead()?) //unsigned long wait_started = millis(); //while(strip.isUpdating() && (millis() - wait_started < STRIP_WAIT_TIME)) { // delay(1); //} oldRead[b] = aRead; // if no macro for "short press" and "long press" is defined use brightness control if (!buttons[b].macroButton && !buttons[b].macroLongPress) { DEBUG_PRINTF_P(PSTR("Analog: Action = %u\n"), buttons[b].macroDoublePress); // if "double press" macro defines which option to change if (buttons[b].macroDoublePress >= 250) { // global brightness if (aRead == 0) { briLast = bri; bri = 0; } else { if (bri == 0) strip.restartRuntime(); bri = aRead; } } else if (buttons[b].macroDoublePress == 249) { // effect speed effectSpeed = aRead; } else if (buttons[b].macroDoublePress == 248) { // effect intensity effectIntensity = aRead; } else if (buttons[b].macroDoublePress == 247) { // selected palette effectPalette = map(aRead, 0, 252, 0, getPaletteCount()-1); effectPalette = constrain(effectPalette, 0, getPaletteCount()-1); // map is allowed to "overshoot", so we need to contrain the result } else if (buttons[b].macroDoublePress == 200) { // primary color, hue, full saturation colorHStoRGB(aRead*256, 255, colPri); } else { // otherwise use "double press" for segment selection Segment& seg = strip.getSegment(buttons[b].macroDoublePress); if (aRead == 0) { seg.on = false; // do not use transition //seg.setOption(SEG_OPTION_ON, false); // off (use transition) } else { seg.opacity = aRead; // set brightness (opacity) of segment seg.on = true; //seg.setOpacity(aRead); //seg.setOption(SEG_OPTION_ON, true); // on (use transition) } // this will notify clients of update (websockets,mqtt,etc) updateInterfaces(CALL_MODE_BUTTON); } } else { DEBUG_PRINTLN(F("Analog: No action")); //TODO: // we can either trigger a preset depending on the level (between short and long entries) // or use it for RGBW direct control } colorUpdated(CALL_MODE_BUTTON); } void handleButton() { static unsigned long lastAnalogRead = 0UL; static unsigned long lastRun = 0UL; unsigned long now = millis(); if (strip.isUpdating() && (now - lastRun < ANALOG_BTN_READ_CYCLE+1)) return; // don't interfere with strip update (unless strip is updating continuously, e.g. very long strips) lastRun = now; for (unsigned b = 0; b < buttons.size(); b++) { #ifdef ESP8266 if ((buttons[b].pin < 0 && !(buttons[b].type == BTN_TYPE_ANALOG || buttons[b].type == BTN_TYPE_ANALOG_INVERTED)) || buttons[b].type == BTN_TYPE_NONE) continue; #else if (buttons[b].pin < 0 || buttons[b].type == BTN_TYPE_NONE) continue; #endif if (UsermodManager::handleButton(b)) continue; // did usermod handle buttons if (buttons[b].type == BTN_TYPE_ANALOG || buttons[b].type == BTN_TYPE_ANALOG_INVERTED) { // button is not a button but a potentiometer if (now - lastAnalogRead > ANALOG_BTN_READ_CYCLE) { handleAnalog(b); } continue; } // button is not momentary, but switch. This is only suitable on pins whose on-boot state does not matter (NOT gpio0) if (buttons[b].type == BTN_TYPE_SWITCH || buttons[b].type == BTN_TYPE_TOUCH_SWITCH || buttons[b].type == BTN_TYPE_PIR_SENSOR) { handleSwitch(b); continue; } // momentary button logic if (isButtonPressed(b)) { // pressed // if all macros are the same, fire action immediately on rising edge if (buttons[b].macroButton && buttons[b].macroButton == buttons[b].macroLongPress && buttons[b].macroButton == buttons[b].macroDoublePress) { if (!buttons[b].pressedBefore) shortPressAction(b); buttons[b].pressedBefore = true; buttons[b].pressedTime = now; // continually update (for debouncing to work in release handler) continue; } if (!buttons[b].pressedBefore) buttons[b].pressedTime = now; buttons[b].pressedBefore = true; if (now - buttons[b].pressedTime > WLED_LONG_PRESS) { //long press if (!buttons[b].longPressed) { buttonBriDirection = !buttonBriDirection; //toggle brightness direction on long press longPressAction(b); } else if (b) { //repeatable action (~5 times per s) on button > 0 longPressAction(b); buttons[b].pressedTime = now - WLED_LONG_REPEATED_ACTION; //200ms } buttons[b].longPressed = true; } } else if (buttons[b].pressedBefore) { //released long dur = now - buttons[b].pressedTime; // released after rising-edge short press action if (buttons[b].macroButton && buttons[b].macroButton == buttons[b].macroLongPress && buttons[b].macroButton == buttons[b].macroDoublePress) { if (dur > WLED_DEBOUNCE_THRESHOLD) buttons[b].pressedBefore = false; // debounce, blocks button for 50 ms once it has been released continue; } if (dur < WLED_DEBOUNCE_THRESHOLD) {buttons[b].pressedBefore = false; continue;} // too short "press", debounce bool doublePress = buttons[b].waitTime; //did we have a short press before? buttons[b].waitTime = 0; if (b == 0 && dur > WLED_LONG_AP) { // long press on button 0 (when released) if (dur > WLED_LONG_FACTORY_RESET) { // factory reset if pressed > 10 seconds WLED_FS.format(); doReboot = true; } else { WLED::instance().initAP(true); } } else if (!buttons[b].longPressed) { //short press //NOTE: this interferes with double click handling in usermods so usermod needs to implement full button handling if (b != 1 && !buttons[b].macroDoublePress) { //don't wait for double press on buttons without a default action if no double press macro set shortPressAction(b); } else { //double press if less than 350 ms between current press and previous short press release (buttonWaitTime!=0) if (doublePress) { doublePressAction(b); } else { buttons[b].waitTime = now; } } } buttons[b].pressedBefore = false; buttons[b].longPressed = false; } //if 350ms elapsed since last short press release it is a short press if (buttons[b].waitTime && now - buttons[b].waitTime > WLED_DOUBLE_PRESS && !buttons[b].pressedBefore) { buttons[b].waitTime = 0; shortPressAction(b); } } if (now - lastAnalogRead > ANALOG_BTN_READ_CYCLE) { lastAnalogRead = now; } } // handleIO() happens *after* handleTransitions() (see wled.cpp) which may change bri/briT but *before* strip.service() // where actual LED painting occurrs // this is important for relay control and in the event of turning off on-board LED void handleIO() { handleButton(); // if we want to control on-board LED (ESP8266) or relay we have to do it here as the final show() may not happen until // next loop() cycle handleOnOff(); } void handleOnOff(bool forceOff) { if (strip.getBrightness() && !forceOff) { lastOnTime = millis(); if (offMode) { BusManager::on(); if (rlyPin>=0) { // note: pinMode is set in first call to handleOnOff(true) in beginStrip() digitalWrite(rlyPin, rlyMde); // set to on state delay(RELAY_DELAY); // let power stabilize before sending LED data (#346 #812 #3581 #3955) } offMode = false; } } else if ((millis() - lastOnTime > 600 && !strip.needsUpdate()) || forceOff) { // for turning LED or relay off we need to wait until strip no longer needs updates (strip.trigger()) if (!offMode) { BusManager::off(); if (rlyPin>=0) { digitalWrite(rlyPin, !rlyMde); // set output before disabling high-z state to avoid output glitches pinMode(rlyPin, rlyOpenDrain ? OUTPUT_OPEN_DRAIN : OUTPUT); } offMode = true; } } } void IRAM_ATTR touchButtonISR() { // used for ESP32 S2 and S3: nothing to do, ISR is just used to update registers of HAL driver } ================================================ FILE: wled00/cfg.cpp ================================================ #include "wled.h" #include "wled_ethernet.h" /* * Serializes and parses the cfg.json and wsec.json settings files, stored in internal FS. * The structure of the JSON is not to be considered an official API and may change without notice. */ #ifndef PIXEL_COUNTS #define PIXEL_COUNTS DEFAULT_LED_COUNT #endif #ifndef DATA_PINS #define DATA_PINS DEFAULT_LED_PIN #endif #ifndef LED_TYPES #define LED_TYPES DEFAULT_LED_TYPE #endif #ifndef DEFAULT_LED_COLOR_ORDER #define DEFAULT_LED_COLOR_ORDER COL_ORDER_GRB //default to GRB #endif static constexpr unsigned sumPinsRequired(const unsigned* current, size_t count) { return (count > 0) ? (Bus::getNumberOfPins(*current) + sumPinsRequired(current+1,count-1)) : 0; } static constexpr bool validatePinsAndTypes(const unsigned* types, unsigned numTypes, unsigned numPins ) { // Pins provided < pins required -> always invalid // Pins provided = pins required -> always valid // Pins provided > pins required -> valid if excess pins are a product of last type pins since it will be repeated return (sumPinsRequired(types, numTypes) > numPins) ? false : (numPins - sumPinsRequired(types, numTypes)) % Bus::getNumberOfPins(types[numTypes-1]) == 0; } //simple macro for ArduinoJSON's or syntax #define CJSON(a,b) a = b | a static inline void getStringFromJson(char* dest, const char* src, size_t len) { if (src != nullptr) strlcpy(dest, src, len); } bool deserializeConfig(JsonObject doc, bool fromFS) { bool needsSave = false; //int rev_major = doc["rev"][0]; // 1 //int rev_minor = doc["rev"][1]; // 0 //long vid = doc[F("vid")]; // 2010020 JsonObject id = doc["id"]; getStringFromJson(cmDNS, id[F("mdns")], 33); getStringFromJson(serverDescription, id[F("name")], 33); #ifndef WLED_DISABLE_ALEXA getStringFromJson(alexaInvocationName, id[F("inv")], 33); #endif CJSON(simplifiedUI, id[F("sui")]); JsonObject nw = doc["nw"]; #ifndef WLED_DISABLE_ESPNOW CJSON(enableESPNow, nw[F("espnow")]); linked_remotes.clear(); JsonVariant lrem = nw[F("linked_remote")]; if (!lrem.isNull()) { if (lrem.is()) { for (size_t i = 0; i < lrem.size(); i++) { std::array entry{}; getStringFromJson(entry.data(), lrem[i], 13); entry[12] = '\0'; linked_remotes.emplace_back(entry); } } else { // legacy support for single MAC address in config std::array entry{}; getStringFromJson(entry.data(), lrem, 13); entry[12] = '\0'; linked_remotes.emplace_back(entry); } } #endif size_t n = 0; JsonArray nw_ins = nw["ins"]; if (!nw_ins.isNull()) { // as password are stored separately in wsec.json when reading configuration vector resize happens there, but for dynamic config we need to resize if necessary if (nw_ins.size() > 1 && nw_ins.size() > multiWiFi.size()) multiWiFi.resize(nw_ins.size()); // resize constructs objects while resizing for (JsonObject wifi : nw_ins) { JsonArray ip = wifi["ip"]; JsonArray gw = wifi["gw"]; JsonArray sn = wifi["sn"]; char ssid[33] = ""; char pass[65] = ""; char bssid[13] = ""; IPAddress nIP = (uint32_t)0U, nGW = (uint32_t)0U, nSN = (uint32_t)0x00FFFFFF; // little endian getStringFromJson(ssid, wifi[F("ssid")], 33); getStringFromJson(pass, wifi["psk"], 65); // password is not normally present but if it is, use it getStringFromJson(bssid, wifi[F("bssid")], 13); for (size_t i = 0; i < 4; i++) { CJSON(nIP[i], ip[i]); CJSON(nGW[i], gw[i]); CJSON(nSN[i], sn[i]); } if (strlen(ssid) > 0) strlcpy(multiWiFi[n].clientSSID, ssid, 33); // this will keep old SSID intact if not present in JSON if (strlen(pass) > 0) strlcpy(multiWiFi[n].clientPass, pass, 65); // this will keep old password intact if not present in JSON if (strlen(bssid) > 0) fillStr2MAC(multiWiFi[n].bssid, bssid); multiWiFi[n].staticIP = nIP; multiWiFi[n].staticGW = nGW; multiWiFi[n].staticSN = nSN; #ifdef WLED_ENABLE_WPA_ENTERPRISE byte encType = WIFI_ENCRYPTION_TYPE_PSK; char anonIdent[65] = ""; char ident[65] = ""; CJSON(encType, wifi[F("enc_type")]); getStringFromJson(anonIdent, wifi["e_anon_ident"], 65); getStringFromJson(ident, wifi["e_ident"], 65); multiWiFi[n].encryptionType = encType; strlcpy(multiWiFi[n].enterpriseAnonIdentity, anonIdent, 65); strlcpy(multiWiFi[n].enterpriseIdentity, ident, 65); #endif if (++n >= WLED_MAX_WIFI_COUNT) break; } } JsonArray dns = nw[F("dns")]; if (!dns.isNull()) { for (size_t i = 0; i < 4; i++) { CJSON(dnsAddress[i], dns[i]); } } // https://github.com/wled/WLED/issues/5247 #ifdef WLED_USE_ETHERNET JsonObject ethernet = doc[F("eth")]; CJSON(ethernetType, ethernet["type"]); // NOTE: Ethernet configuration takes priority over other use of pins initEthernet(); #endif JsonObject ap = doc["ap"]; getStringFromJson(apSSID, ap[F("ssid")], 33); getStringFromJson(apPass, ap["psk"] , 65); //normally not present due to security //int ap_pskl = ap[F("pskl")]; CJSON(apChannel, ap[F("chan")]); if (apChannel > 13 || apChannel < 1) apChannel = 1; CJSON(apHide, ap[F("hide")]); if (apHide > 1) apHide = 1; CJSON(apBehavior, ap[F("behav")]); /* JsonArray ap_ip = ap["ip"]; for (unsigned i = 0; i < 4; i++) { apIP[i] = ap_ip; } */ JsonObject wifi = doc[F("wifi")]; noWifiSleep = !(wifi[F("sleep")] | !noWifiSleep); // inverted //noWifiSleep = !noWifiSleep; CJSON(force802_3g, wifi[F("phy")]); //force phy mode g? #ifdef ARDUINO_ARCH_ESP32 CJSON(txPower, wifi[F("txpwr")]); txPower = min(max((int)txPower, (int)WIFI_POWER_2dBm), (int)WIFI_POWER_19_5dBm); #endif JsonObject hw = doc[F("hw")]; // initialize LED pins and lengths prior to other HW (except for ethernet) JsonObject hw_led = hw["led"]; uint16_t total = hw_led[F("total")] | strip.getLengthTotal(); uint16_t ablMilliampsMax = hw_led[F("maxpwr")] | BusManager::ablMilliampsMax(); BusManager::setMilliampsMax(ablMilliampsMax); Bus::setGlobalAWMode(hw_led[F("rgbwm")] | AW_GLOBAL_DISABLED); CJSON(strip.correctWB, hw_led["cct"]); CJSON(strip.cctFromRgb, hw_led[F("cr")]); CJSON(cctICused, hw_led[F("ic")]); uint8_t cctBlending = hw_led[F("cb")] | Bus::getCCTBlend(); Bus::setCCTBlend(cctBlending); unsigned targetFPS = hw_led["fps"] | WLED_FPS; strip.setTargetFps(targetFPS); //unlimited if 0, default 42 FPS #ifndef WLED_DISABLE_2D // 2D Matrix Settings JsonObject matrix = hw_led[F("matrix")]; if (!matrix.isNull()) { strip.isMatrix = true; unsigned numPanels = matrix[F("mpc")] | 1; numPanels = constrain(numPanels, 1, WLED_MAX_PANELS); strip.panel.clear(); JsonArray panels = matrix[F("panels")]; unsigned s = 0; if (!panels.isNull()) { strip.panel.reserve(numPanels); // pre-allocate default 8x8 panels for (JsonObject pnl : panels) { WS2812FX::Panel p; CJSON(p.bottomStart, pnl["b"]); CJSON(p.rightStart, pnl["r"]); CJSON(p.vertical, pnl["v"]); CJSON(p.serpentine, pnl["s"]); CJSON(p.xOffset, pnl["x"]); CJSON(p.yOffset, pnl["y"]); CJSON(p.height, pnl["h"]); CJSON(p.width, pnl["w"]); strip.panel.push_back(p); if (++s >= numPanels) break; // max panels reached } } strip.panel.shrink_to_fit(); // release unused memory (just in case) // cannot call strip.deserializeLedmap()/strip.setUpMatrix() here due to already locked JSON buffer //if (!fromFS) doInit2D = true; // if called at boot (fromFS==true), WLED::beginStrip() will take care of setting up matrix } #endif DEBUG_PRINTF_P(PSTR("Heap before buses: %d\n"), getFreeHeapSize()); JsonArray ins = hw_led["ins"]; if (!ins.isNull()) { int s = 0; // bus iterator for (JsonObject elm : ins) { if (s >= WLED_MAX_BUSSES) break; // only counts physical buses uint8_t pins[OUTPUT_MAX_PINS] = {255, 255, 255, 255, 255}; JsonArray pinArr = elm["pin"]; if (pinArr.size() == 0) continue; //pins[0] = pinArr[0]; unsigned i = 0; for (int p : pinArr) { pins[i++] = p; if (i>4) break; } uint16_t length = elm["len"] | 1; uint8_t colorOrder = (int)elm[F("order")]; // contains white channel swap option in upper nibble uint8_t skipFirst = elm[F("skip")]; uint16_t start = elm["start"] | 0; if (length==0 || start + length > MAX_LEDS) continue; // zero length or we reached max. number of LEDs, just stop uint8_t ledType = elm["type"] | TYPE_WS2812_RGB; bool reversed = elm["rev"]; bool refresh = elm["ref"] | false; uint16_t freqkHz = elm[F("freq")] | 0; // will be in kHz for DotStar and Hz for PWM uint8_t AWmode = elm[F("rgbwm")] | RGBW_MODE_MANUAL_ONLY; uint8_t maPerLed = elm[F("ledma")] | LED_MILLIAMPS_DEFAULT; uint16_t maMax = elm[F("maxpwr")] | (ablMilliampsMax * length) / total; // rough (incorrect?) per strip ABL calculation when no config exists // To disable brightness limiter we either set output max current to 0 or single LED current to 0 (we choose output max current) if (Bus::isPWM(ledType) || Bus::isOnOff(ledType) || Bus::isVirtual(ledType)) { // analog and virtual maPerLed = 0; maMax = 0; } ledType |= refresh << 7; // hack bit 7 to indicate strip requires off refresh uint8_t driverType = elm[F("drv")] | 0; // 0=RMT (default), 1=I2S note: polybus may override this if driver is not available String host = elm[F("text")] | String(); busConfigs.emplace_back(ledType, pins, start, length, colorOrder, reversed, skipFirst, AWmode, freqkHz, maPerLed, maMax, driverType, host); doInitBusses = true; // finalization done in beginStrip() if (!Bus::isVirtual(ledType)) s++; // have as many virtual buses as you want } } else if (fromFS) { //if busses failed to load, add default (fresh install, FS issue, ...) BusManager::removeAll(); busConfigs.clear(); DEBUG_PRINTLN(F("No busses, init default")); constexpr unsigned defDataTypes[] = {LED_TYPES}; constexpr unsigned defDataPins[] = {DATA_PINS}; constexpr unsigned defCounts[] = {PIXEL_COUNTS}; constexpr unsigned defNumTypes = (sizeof(defDataTypes) / sizeof(defDataTypes[0])); constexpr unsigned defNumPins = (sizeof(defDataPins) / sizeof(defDataPins[0])); constexpr unsigned defNumCounts = (sizeof(defCounts) / sizeof(defCounts[0])); static_assert(validatePinsAndTypes(defDataTypes, defNumTypes, defNumPins), "The default pin list defined in DATA_PINS does not match the pin requirements for the default buses defined in LED_TYPES"); unsigned pinsIndex = 0; for (unsigned i = 0; i < WLED_MAX_BUSSES; i++) { uint8_t defPin[OUTPUT_MAX_PINS]; // if we have less types than requested outputs and they do not align, use last known type to set current type unsigned dataType = defDataTypes[(i < defNumTypes) ? i : defNumTypes -1]; unsigned busPins = Bus::getNumberOfPins(dataType); // if we need more pins than available all outputs have been configured if (pinsIndex + busPins > defNumPins) break; // Assign all pins first so we can check for conflicts on this bus for (unsigned j = 0; j < busPins && j < OUTPUT_MAX_PINS; j++) defPin[j] = defDataPins[pinsIndex + j]; for (unsigned j = 0; j < busPins && j < OUTPUT_MAX_PINS; j++) { bool validPin = true; // When booting without config (1st boot) we need to make sure GPIOs defined for LED output don't clash with hardware // i.e. DEBUG (GPIO1), DMX (2), SPI RAM/FLASH (16&17 on ESP32-WROVER/PICO), read/only pins, etc. // Pin should not be already allocated, read/only or defined for current bus while (PinManager::isPinAllocated(defPin[j]) || !PinManager::isPinOk(defPin[j],true)) { if (validPin) { DEBUG_PRINTLN(F("Some of the provided pins cannot be used to configure this LED output.")); defPin[j] = 1; // start with GPIO1 and work upwards validPin = false; } else if (defPin[j] < WLED_NUM_PINS) { defPin[j]++; } else { DEBUG_PRINTLN(F("No available pins left! Can't configure output.")); break; } // is the newly assigned pin already defined or used previously? // try next in line until there are no clashes or we run out of pins bool clash; do { clash = false; // check for conflicts on current bus for (const auto &pin : defPin) { if (&pin != &defPin[j] && pin == defPin[j]) { clash = true; break; } } // We already have a clash on current bus, no point checking next buses if (!clash) { // check for conflicts in defined pins for (const auto &pin : defDataPins) { if (pin == defPin[j]) { clash = true; break; } } } if (clash) defPin[j]++; if (defPin[j] >= WLED_NUM_PINS) break; } while (clash); } } pinsIndex += busPins; // if we have less counts than pins and they do not align, use last known count to set current count unsigned count = defCounts[(i < defNumCounts) ? i : defNumCounts -1]; unsigned start = 0; // analog always has length 1 if (Bus::isPWM(dataType) || Bus::isOnOff(dataType)) count = 1; busConfigs.emplace_back(dataType, defPin, start, count, DEFAULT_LED_COLOR_ORDER, false, 0, RGBW_MODE_MANUAL_ONLY, 0, LED_MILLIAMPS_DEFAULT, ABL_MILLIAMPS_DEFAULT, 0); // driver=0 (RMT default) doInitBusses = true; // finalization done in beginStrip() } } if (hw_led["rev"] && BusManager::getNumBusses()) BusManager::getBus(0)->setReversed(true); //set 0.11 global reversed setting for first bus // read color order map configuration JsonArray hw_com = hw[F("com")]; if (!hw_com.isNull()) { BusManager::getColorOrderMap().reserve(std::min(hw_com.size(), (size_t)WLED_MAX_COLOR_ORDER_MAPPINGS)); for (JsonObject entry : hw_com) { uint16_t start = entry["start"] | 0; uint16_t len = entry["len"] | 0; uint8_t colorOrder = (int)entry[F("order")]; if (!BusManager::getColorOrderMap().add(start, len, colorOrder)) break; } } // read multiple button configuration JsonObject btn_obj = hw["btn"]; CJSON(touchThreshold, btn_obj[F("tt")]); bool pull = btn_obj[F("pull")] | (!disablePullUp); // if true, pullup is enabled disablePullUp = !pull; JsonArray hw_btn_ins = btn_obj["ins"]; if (!hw_btn_ins.isNull()) { // deallocate existing button pins for (const auto &button : buttons) PinManager::deallocatePin(button.pin, PinOwner::Button); // does nothing if trying to deallocate a pin with PinOwner != Button buttons.clear(); // clear existing buttons unsigned s = 0; for (JsonObject btn : hw_btn_ins) { uint8_t type = btn["type"] | BTN_TYPE_NONE; int8_t pin = btn["pin"][0] | -1; if (pin > -1 && PinManager::allocatePin(pin, false, PinOwner::Button)) { #ifdef ARDUINO_ARCH_ESP32 // ESP32 only: check that analog button pin is a valid ADC gpio if ((type == BTN_TYPE_ANALOG) || (type == BTN_TYPE_ANALOG_INVERTED)) { if (digitalPinToAnalogChannel(pin) < 0) { // not an ADC analog pin DEBUG_PRINTF_P(PSTR("PIN ALLOC error: GPIO%d for analog button #%d is not an analog pin!\n"), pin, s); PinManager::deallocatePin(pin, PinOwner::Button); pin = -1; continue; } else { analogReadResolution(12); // see #4040 } } else if ((type == BTN_TYPE_TOUCH || type == BTN_TYPE_TOUCH_SWITCH)) { if (digitalPinToTouchChannel(pin) < 0) { // not a touch pin DEBUG_PRINTF_P(PSTR("PIN ALLOC error: GPIO%d for touch button #%d is not a touch pin!\n"), pin, s); PinManager::deallocatePin(pin, PinOwner::Button); pin = -1; continue; } //if touch pin, enable the touch interrupt on ESP32 S2 & S3 #ifdef SOC_TOUCH_VERSION_2 // ESP32 S2 and S3 have a function to check touch state but need to attach an interrupt to do so else touchAttachInterrupt(pin, touchButtonISR, touchThreshold << 4); // threshold on Touch V2 is much higher (1500 is a value given by Espressif example, I measured changes of over 5000) #endif } else #endif { // regular buttons and switches if (disablePullUp) { pinMode(pin, INPUT); } else { #ifdef ESP32 pinMode(pin, type==BTN_TYPE_PUSH_ACT_HIGH ? INPUT_PULLDOWN : INPUT_PULLUP); #else pinMode(pin, INPUT_PULLUP); #endif } } JsonArray hw_btn_ins_0_macros = btn["macros"]; uint8_t press = hw_btn_ins_0_macros[0] | 0; uint8_t longPress = hw_btn_ins_0_macros[1] | 0; uint8_t doublePress = hw_btn_ins_0_macros[2] | 0; buttons.emplace_back(pin, type, press, longPress, doublePress); // add button to vector } if (++s >= WLED_MAX_BUTTONS) break; // max buttons reached } } else if (fromFS) { // new install/missing configuration (button 0 has defaults) // relies upon only being called once with fromFS == true, which is currently true. constexpr uint8_t defTypes[] = {BTNTYPE}; constexpr int8_t defPins[] = {BTNPIN}; constexpr unsigned numTypes = (sizeof(defTypes) / sizeof(defTypes[0])); constexpr unsigned numPins = (sizeof(defPins) / sizeof(defPins[0])); // check if the number of pins and types are valid; count of pins must be greater than or equal to types static_assert(numTypes <= numPins, "The default button pins defined in BTNPIN do not match the button types defined in BTNTYPE"); uint8_t type = BTN_TYPE_NONE; buttons.clear(); // clear existing buttons (just in case) for (size_t s = 0; s < WLED_MAX_BUTTONS && s < numPins; s++) { type = defTypes[s < numTypes ? s : numTypes - 1]; // use last known type to set current type if types less than pins if (type == BTN_TYPE_NONE || defPins[s] < 0 || !PinManager::allocatePin(defPins[s], false, PinOwner::Button)) { if (buttons.size() == 0) buttons.emplace_back(-1, BTN_TYPE_NONE); // add disabled button to vector (so we have at least one button defined) continue; // pin not available or invalid, skip configuring this GPIO } if (disablePullUp) { pinMode(defPins[s], INPUT); } else { #ifdef ESP32 pinMode(defPins[s], type==BTN_TYPE_PUSH_ACT_HIGH ? INPUT_PULLDOWN : INPUT_PULLUP); #else pinMode(defPins[s], INPUT_PULLUP); #endif } buttons.emplace_back(defPins[s], type); // add button to vector } } CJSON(buttonPublishMqtt, btn_obj["mqtt"]); #ifndef WLED_DISABLE_INFRARED int hw_ir_pin = hw["ir"]["pin"] | -2; // 4 if (hw_ir_pin > -2) { PinManager::deallocatePin(irPin, PinOwner::IR); if (PinManager::allocatePin(hw_ir_pin, false, PinOwner::IR)) { irPin = hw_ir_pin; } else { irPin = -1; } } CJSON(irEnabled, hw["ir"]["type"]); #endif CJSON(irApplyToAllSelected, hw["ir"]["sel"]); JsonObject relay = hw[F("relay")]; rlyOpenDrain = relay[F("odrain")] | rlyOpenDrain; int hw_relay_pin = relay["pin"] | -2; if (hw_relay_pin > -2) { PinManager::deallocatePin(rlyPin, PinOwner::Relay); if (PinManager::allocatePin(hw_relay_pin,true, PinOwner::Relay)) { rlyPin = hw_relay_pin; pinMode(rlyPin, rlyOpenDrain ? OUTPUT_OPEN_DRAIN : OUTPUT); } else { rlyPin = -1; } } if (relay.containsKey("rev")) { rlyMde = !relay["rev"]; } CJSON(serialBaud, hw[F("baud")]); if (serialBaud < 96 || serialBaud > 15000) serialBaud = 1152; updateBaudRate(serialBaud *100); JsonArray hw_if_i2c = hw[F("if")][F("i2c-pin")]; CJSON(i2c_sda, hw_if_i2c[0]); CJSON(i2c_scl, hw_if_i2c[1]); PinManagerPinType i2c[2] = { { i2c_sda, true }, { i2c_scl, true } }; if (i2c_scl >= 0 && i2c_sda >= 0 && PinManager::allocateMultiplePins(i2c, 2, PinOwner::HW_I2C)) { #ifdef ESP32 if (!Wire.setPins(i2c_sda, i2c_scl)) { i2c_scl = i2c_sda = -1; } // this will fail if Wire is initialised (Wire.begin() called prior) else Wire.begin(); #else Wire.begin(i2c_sda, i2c_scl); #endif } else { i2c_sda = -1; i2c_scl = -1; } JsonArray hw_if_spi = hw[F("if")][F("spi-pin")]; CJSON(spi_mosi, hw_if_spi[0]); CJSON(spi_sclk, hw_if_spi[1]); CJSON(spi_miso, hw_if_spi[2]); PinManagerPinType spi[3] = { { spi_mosi, true }, { spi_miso, true }, { spi_sclk, true } }; if (spi_mosi >= 0 && spi_sclk >= 0 && PinManager::allocateMultiplePins(spi, 3, PinOwner::HW_SPI)) { #ifdef ESP32 SPI.begin(spi_sclk, spi_miso, spi_mosi); // SPI global uses VSPI on ESP32 and FSPI on C3, S3 #else SPI.begin(); #endif } else { spi_mosi = -1; spi_miso = -1; spi_sclk = -1; } //int hw_status_pin = hw[F("status")]["pin"]; // -1 JsonObject light = doc[F("light")]; CJSON(briMultiplier, light[F("scale-bri")]); CJSON(paletteBlend, light[F("pal-mode")]); CJSON(strip.autoSegments, light[F("aseg")]); CJSON(gammaCorrectVal, light["gc"]["val"]); // default 2.2 float light_gc_bri = light["gc"]["bri"] | 1.0f; // default to 1.0 (false) float light_gc_col = light["gc"]["col"] | gammaCorrectVal; // default to gammaCorrectVal (true) if (light_gc_bri != 1.0f) gammaCorrectBri = true; else gammaCorrectBri = false; if (light_gc_col != 1.0f) gammaCorrectCol = true; else gammaCorrectCol = false; if (gammaCorrectVal < 0.1f || gammaCorrectVal > 3) { gammaCorrectVal = 1.0f; // no gamma correction gammaCorrectBri = false; gammaCorrectCol = false; } NeoGammaWLEDMethod::calcGammaTable(gammaCorrectVal); // fill look-up tables JsonObject light_tr = light["tr"]; int tdd = light_tr["dur"] | -1; if (tdd >= 0) transitionDelay = transitionDelayDefault = tdd * 100; strip.setTransition(transitionDelayDefault); CJSON(randomPaletteChangeTime, light_tr[F("rpc")]); CJSON(useHarmonicRandomPalette, light_tr[F("hrp")]); JsonObject light_nl = light["nl"]; CJSON(nightlightMode, light_nl["mode"]); byte prev = nightlightDelayMinsDefault; CJSON(nightlightDelayMinsDefault, light_nl["dur"]); if (nightlightDelayMinsDefault != prev) nightlightDelayMins = nightlightDelayMinsDefault; CJSON(nightlightTargetBri, light_nl[F("tbri")]); CJSON(macroNl, light_nl["macro"]); JsonObject def = doc["def"]; CJSON(bootPreset, def["ps"]); CJSON(turnOnAtBoot, def["on"]); // true CJSON(briS, def["bri"]); // 128 JsonObject interfaces = doc["if"]; JsonObject if_sync = interfaces["sync"]; CJSON(udpPort, if_sync[F("port0")]); // 21324 CJSON(udpPort2, if_sync[F("port1")]); // 65506 #ifndef WLED_DISABLE_ESPNOW CJSON(useESPNowSync, if_sync[F("espnow")]); #endif JsonObject if_sync_recv = if_sync[F("recv")]; CJSON(receiveNotificationBrightness, if_sync_recv["bri"]); CJSON(receiveNotificationColor, if_sync_recv["col"]); CJSON(receiveNotificationEffects, if_sync_recv["fx"]); CJSON(receiveNotificationPalette, if_sync_recv["pal"]); CJSON(receiveGroups, if_sync_recv["grp"]); CJSON(receiveSegmentOptions, if_sync_recv["seg"]); CJSON(receiveSegmentBounds, if_sync_recv["sb"]); JsonObject if_sync_send = if_sync[F("send")]; CJSON(sendNotifications, if_sync_send["en"]); sendNotificationsRT = sendNotifications; CJSON(notifyDirect, if_sync_send[F("dir")]); CJSON(notifyButton, if_sync_send["btn"]); CJSON(notifyAlexa, if_sync_send["va"]); CJSON(notifyHue, if_sync_send["hue"]); CJSON(syncGroups, if_sync_send["grp"]); if (if_sync_send[F("twice")]) udpNumRetries = 1; // import setting from 0.13 and earlier CJSON(udpNumRetries, if_sync_send["ret"]); JsonObject if_nodes = interfaces["nodes"]; CJSON(nodeListEnabled, if_nodes[F("list")]); CJSON(nodeBroadcastEnabled, if_nodes[F("bcast")]); JsonObject if_live = interfaces["live"]; CJSON(receiveDirect, if_live["en"]); // UDP/Hyperion realtime CJSON(useMainSegmentOnly, if_live[F("mso")]); CJSON(realtimeRespectLedMaps, if_live[F("rlm")]); CJSON(e131Port, if_live["port"]); // 5568 if (e131Port == DDP_DEFAULT_PORT) e131Port = E131_DEFAULT_PORT; // prevent double DDP port allocation CJSON(e131Multicast, if_live[F("mc")]); JsonObject if_live_dmx = if_live["dmx"]; CJSON(e131Universe, if_live_dmx[F("uni")]); CJSON(e131SkipOutOfSequence, if_live_dmx[F("seqskip")]); CJSON(DMXAddress, if_live_dmx[F("addr")]); if (!DMXAddress || DMXAddress > 510) DMXAddress = 1; CJSON(DMXSegmentSpacing, if_live_dmx[F("dss")]); if (DMXSegmentSpacing > 150) DMXSegmentSpacing = 0; CJSON(e131Priority, if_live_dmx[F("e131prio")]); if (e131Priority > 200) e131Priority = 200; CJSON(DMXMode, if_live_dmx["mode"]); tdd = if_live[F("timeout")] | -1; if (tdd >= 0) realtimeTimeoutMs = tdd * 100; #ifdef WLED_ENABLE_DMX_INPUT CJSON(dmxInputTransmitPin, if_live_dmx[F("inputRxPin")]); CJSON(dmxInputReceivePin, if_live_dmx[F("inputTxPin")]); CJSON(dmxInputEnablePin, if_live_dmx[F("inputEnablePin")]); CJSON(dmxInputPort, if_live_dmx[F("dmxInputPort")]); #endif CJSON(arlsForceMaxBri, if_live[F("maxbri")]); CJSON(arlsDisableGammaCorrection, if_live[F("no-gc")]); // false CJSON(arlsOffset, if_live[F("offset")]); // 0 #ifndef WLED_DISABLE_ALEXA CJSON(alexaEnabled, interfaces["va"][F("alexa")]); // false CJSON(macroAlexaOn, interfaces["va"]["macros"][0]); CJSON(macroAlexaOff, interfaces["va"]["macros"][1]); CJSON(alexaNumPresets, interfaces["va"]["p"]); #endif #ifndef WLED_DISABLE_MQTT JsonObject if_mqtt = interfaces["mqtt"]; CJSON(mqttEnabled, if_mqtt["en"]); getStringFromJson(mqttServer, if_mqtt[F("broker")], MQTT_MAX_SERVER_LEN+1); CJSON(mqttPort, if_mqtt["port"]); // 1883 getStringFromJson(mqttUser, if_mqtt[F("user")], 41); getStringFromJson(mqttPass, if_mqtt["psk"], 65); //normally not present due to security getStringFromJson(mqttClientID, if_mqtt[F("cid")], 41); getStringFromJson(mqttDeviceTopic, if_mqtt[F("topics")][F("device")], MQTT_MAX_TOPIC_LEN+1); // "wled/test" getStringFromJson(mqttGroupTopic, if_mqtt[F("topics")][F("group")], MQTT_MAX_TOPIC_LEN+1); // "" CJSON(retainMqttMsg, if_mqtt[F("rtn")]); #endif #ifndef WLED_DISABLE_HUESYNC JsonObject if_hue = interfaces["hue"]; CJSON(huePollingEnabled, if_hue["en"]); CJSON(huePollLightId, if_hue["id"]); tdd = if_hue[F("iv")] | -1; if (tdd >= 2) huePollIntervalMs = tdd * 100; JsonObject if_hue_recv = if_hue["recv"]; CJSON(hueApplyOnOff, if_hue_recv["on"]); CJSON(hueApplyBri, if_hue_recv["bri"]); CJSON(hueApplyColor, if_hue_recv["col"]); JsonArray if_hue_ip = if_hue["ip"]; for (unsigned i = 0; i < 4; i++) CJSON(hueIP[i], if_hue_ip[i]); #endif JsonObject if_ntp = interfaces[F("ntp")]; CJSON(ntpEnabled, if_ntp["en"]); getStringFromJson(ntpServerName, if_ntp[F("host")], 33); // "1.wled.pool.ntp.org" CJSON(currentTimezone, if_ntp[F("tz")]); CJSON(utcOffsetSecs, if_ntp[F("offset")]); CJSON(useAMPM, if_ntp[F("ampm")]); CJSON(longitude, if_ntp[F("ln")]); CJSON(latitude, if_ntp[F("lt")]); JsonObject ol = doc[F("ol")]; CJSON(overlayCurrent ,ol[F("clock")]); // 0 CJSON(countdownMode, ol[F("cntdwn")]); CJSON(overlayMin, ol["min"]); CJSON(overlayMax, ol[F("max")]); CJSON(analogClock12pixel, ol[F("o12pix")]); CJSON(analogClock5MinuteMarks, ol[F("o5m")]); CJSON(analogClockSecondsTrail, ol[F("osec")]); CJSON(analogClockSolidBlack, ol[F("osb")]); //timed macro rules JsonObject tm = doc[F("timers")]; JsonObject cntdwn = tm[F("cntdwn")]; JsonArray cntdwn_goal = cntdwn[F("goal")]; CJSON(countdownYear, cntdwn_goal[0]); CJSON(countdownMonth, cntdwn_goal[1]); CJSON(countdownDay, cntdwn_goal[2]); CJSON(countdownHour, cntdwn_goal[3]); CJSON(countdownMin, cntdwn_goal[4]); CJSON(countdownSec, cntdwn_goal[5]); CJSON(macroCountdown, cntdwn["macro"]); setCountdown(); JsonArray timers = tm["ins"]; uint8_t it = 0; for (JsonObject timer : timers) { if (it > 9) break; if (it<8 && timer[F("hour")]==255) it=8; // hour==255 -> sunrise/sunset CJSON(timerHours[it], timer[F("hour")]); CJSON(timerMinutes[it], timer["min"]); CJSON(timerMacro[it], timer["macro"]); byte dowPrev = timerWeekday[it]; //note: act is currently only 0 or 1. //the reason we are not using bool is that the on-disk type in 0.11.0 was already int int actPrev = timerWeekday[it] & 0x01; CJSON(timerWeekday[it], timer[F("dow")]); if (timerWeekday[it] != dowPrev) { //present in JSON timerWeekday[it] <<= 1; //add active bit int act = timer["en"] | actPrev; if (act) timerWeekday[it]++; } if (it<8) { JsonObject start = timer["start"]; byte startm = start["mon"]; if (startm) timerMonth[it] = (startm << 4); CJSON(timerDay[it], start["day"]); JsonObject end = timer["end"]; CJSON(timerDayEnd[it], end["day"]); byte endm = end["mon"]; if (startm) timerMonth[it] += endm & 0x0F; if (!(timerMonth[it] & 0x0F)) timerMonth[it] += 12; //default end month to 12 } it++; } JsonObject ota = doc["ota"]; const char* pwd = ota["psk"]; //normally not present due to security bool pwdCorrect = !otaLock; //always allow access if ota not locked if (pwd != nullptr && strncmp(otaPass, pwd, 33) == 0) pwdCorrect = true; if (pwdCorrect) { //only accept these values from cfg.json if ota is unlocked (else from wsec.json) CJSON(otaLock, ota[F("lock")]); CJSON(wifiLock, ota[F("lock-wifi")]); #ifndef WLED_DISABLE_OTA CJSON(aOtaEnabled, ota[F("aota")]); #endif getStringFromJson(otaPass, pwd, 33); //normally not present due to security CJSON(otaSameSubnet, ota[F("same-subnet")]); } #ifdef WLED_ENABLE_DMX JsonObject dmx = doc["dmx"]; CJSON(DMXChannels, dmx[F("chan")]); CJSON(DMXGap,dmx[F("gap")]); CJSON(DMXStart, dmx["start"]); CJSON(DMXStartLED,dmx[F("start-led")]); JsonArray dmx_fixmap = dmx[F("fixmap")]; for (int i = 0; i < dmx_fixmap.size(); i++) { if (i > 14) break; CJSON(DMXFixtureMap[i],dmx_fixmap[i]); } CJSON(e131ProxyUniverse, dmx[F("e131proxy")]); #endif DEBUG_PRINTLN(F("Starting usermod config.")); JsonObject usermods_settings = doc["um"]; if (!usermods_settings.isNull()) { needsSave = !UsermodManager::readFromConfig(usermods_settings); } if (fromFS) return needsSave; // if from /json/cfg doReboot = doc[F("rb")] | doReboot; if (doInitBusses) return false; // no save needed, will do after bus init in wled.cpp loop return (doc["sv"] | true); } static const char s_cfg_json[] PROGMEM = "/cfg.json"; bool backupConfig() { return backupFile(s_cfg_json); } bool restoreConfig() { return restoreFile(s_cfg_json); } bool verifyConfig() { return validateJsonFile(s_cfg_json); } bool configBackupExists() { return checkBackupExists(s_cfg_json); } // rename config file and reboot // if the cfg file doesn't exist, such as after a reset, do nothing void resetConfig() { if (WLED_FS.exists(s_cfg_json)) { DEBUG_PRINTLN(F("Reset config")); char backupname[32]; snprintf_P(backupname, sizeof(backupname), PSTR("/rst.%s"), &s_cfg_json[1]); WLED_FS.rename(s_cfg_json, backupname); doReboot = true; } } bool deserializeConfigFromFS() { [[maybe_unused]] bool success = deserializeConfigSec(); if (!requestJSONBufferLock(JSON_LOCK_CFG_DES)) return false; DEBUG_PRINTLN(F("Reading settings from /cfg.json...")); success = readObjectFromFile(s_cfg_json, nullptr, pDoc); // NOTE: This routine deserializes *and* applies the configuration // Therefore, must also initialize ethernet from this function JsonObject root = pDoc->as(); bool needsSave = deserializeConfig(root, true); releaseJSONBufferLock(); return needsSave; } void serializeConfigToFS() { serializeConfigSec(); backupConfig(); // backup before writing new config DEBUG_PRINTLN(F("Writing settings to /cfg.json...")); if (!requestJSONBufferLock(JSON_LOCK_CFG_SER)) return; JsonObject root = pDoc->to(); serializeConfig(root); File f = WLED_FS.open(FPSTR(s_cfg_json), "w"); if (f) serializeJson(root, f); f.close(); releaseJSONBufferLock(); configNeedsWrite = false; } void serializeConfig(JsonObject root) { JsonArray rev = root.createNestedArray("rev"); rev.add(1); //major settings revision rev.add(0); //minor settings revision root[F("vid")] = VERSION; JsonObject id = root.createNestedObject("id"); id[F("mdns")] = cmDNS; id[F("name")] = serverDescription; #ifndef WLED_DISABLE_ALEXA id[F("inv")] = alexaInvocationName; #endif id[F("sui")] = simplifiedUI; JsonObject nw = root.createNestedObject("nw"); #ifndef WLED_DISABLE_ESPNOW nw[F("espnow")] = enableESPNow; JsonArray lrem = nw.createNestedArray(F("linked_remote")); for (size_t i = 0; i < linked_remotes.size(); i++) { lrem.add(linked_remotes[i].data()); } #endif JsonArray nw_ins = nw.createNestedArray("ins"); for (size_t n = 0; n < multiWiFi.size(); n++) { JsonObject wifi = nw_ins.createNestedObject(); wifi[F("ssid")] = multiWiFi[n].clientSSID; wifi[F("pskl")] = strlen(multiWiFi[n].clientPass); char bssid[13]; fillMAC2Str(bssid, multiWiFi[n].bssid); wifi[F("bssid")] = bssid; JsonArray wifi_ip = wifi.createNestedArray("ip"); JsonArray wifi_gw = wifi.createNestedArray("gw"); JsonArray wifi_sn = wifi.createNestedArray("sn"); for (size_t i = 0; i < 4; i++) { wifi_ip.add(multiWiFi[n].staticIP[i]); wifi_gw.add(multiWiFi[n].staticGW[i]); wifi_sn.add(multiWiFi[n].staticSN[i]); } #ifdef WLED_ENABLE_WPA_ENTERPRISE wifi[F("enc_type")] = multiWiFi[n].encryptionType; if (multiWiFi[n].encryptionType == WIFI_ENCRYPTION_TYPE_ENTERPRISE) { wifi[F("e_anon_ident")] = multiWiFi[n].enterpriseAnonIdentity; wifi[F("e_ident")] = multiWiFi[n].enterpriseIdentity; } #endif } JsonArray dns = nw.createNestedArray(F("dns")); for (size_t i = 0; i < 4; i++) { dns.add(dnsAddress[i]); } JsonObject ap = root.createNestedObject("ap"); ap[F("ssid")] = apSSID; ap[F("pskl")] = strlen(apPass); ap[F("chan")] = apChannel; ap[F("hide")] = apHide; ap[F("behav")] = apBehavior; JsonArray ap_ip = ap.createNestedArray("ip"); ap_ip.add(4); ap_ip.add(3); ap_ip.add(2); ap_ip.add(1); JsonObject wifi = root.createNestedObject(F("wifi")); wifi[F("sleep")] = !noWifiSleep; wifi[F("phy")] = force802_3g; #ifdef ARDUINO_ARCH_ESP32 wifi[F("txpwr")] = txPower; #endif #if defined(ARDUINO_ARCH_ESP32) && defined(WLED_USE_ETHERNET) JsonObject ethernet = root.createNestedObject("eth"); ethernet["type"] = ethernetType; if (ethernetType != WLED_ETH_NONE && ethernetType < WLED_NUM_ETH_TYPES) { JsonArray pins = ethernet.createNestedArray("pin"); for (unsigned p=0; p=0) pins.add(ethernetBoards[ethernetType].eth_power); if (ethernetBoards[ethernetType].eth_mdc>=0) pins.add(ethernetBoards[ethernetType].eth_mdc); if (ethernetBoards[ethernetType].eth_mdio>=0) pins.add(ethernetBoards[ethernetType].eth_mdio); switch (ethernetBoards[ethernetType].eth_clk_mode) { case ETH_CLOCK_GPIO0_IN: case ETH_CLOCK_GPIO0_OUT: pins.add(0); break; case ETH_CLOCK_GPIO16_OUT: pins.add(16); break; case ETH_CLOCK_GPIO17_OUT: pins.add(17); break; } } #endif JsonObject hw = root.createNestedObject(F("hw")); JsonObject hw_led = hw.createNestedObject("led"); hw_led[F("total")] = strip.getLengthTotal(); //provided for compatibility on downgrade and per-output ABL hw_led[F("maxpwr")] = BusManager::ablMilliampsMax(); // hw_led[F("ledma")] = 0; // no longer used hw_led["cct"] = strip.correctWB; hw_led[F("cr")] = strip.cctFromRgb; hw_led[F("ic")] = cctICused; hw_led[F("cb")] = Bus::getCCTBlend(); hw_led["fps"] = strip.getTargetFps(); hw_led[F("rgbwm")] = Bus::getGlobalAWMode(); // global auto white mode override #ifndef WLED_DISABLE_2D // 2D Matrix Settings if (strip.isMatrix) { JsonObject matrix = hw_led.createNestedObject(F("matrix")); matrix[F("mpc")] = strip.panel.size(); JsonArray panels = matrix.createNestedArray(F("panels")); for (size_t i = 0; i < strip.panel.size(); i++) { JsonObject pnl = panels.createNestedObject(); pnl["b"] = strip.panel[i].bottomStart; pnl["r"] = strip.panel[i].rightStart; pnl["v"] = strip.panel[i].vertical; pnl["s"] = strip.panel[i].serpentine; pnl["x"] = strip.panel[i].xOffset; pnl["y"] = strip.panel[i].yOffset; pnl["h"] = strip.panel[i].height; pnl["w"] = strip.panel[i].width; } } #endif JsonArray hw_led_ins = hw_led.createNestedArray("ins"); for (size_t s = 0; s < BusManager::getNumBusses(); s++) { DEBUG_PRINTF_P(PSTR("Cfg: Saving bus #%u\n"), s); const Bus *bus = BusManager::getBus(s); if (!bus) break; // Memory corruption, iterator invalid DEBUG_PRINTF_P(PSTR(" (%d-%d, type:%d, CO:%d, rev:%d, skip:%d, AW:%d kHz:%d, mA:%d/%d)\n"), (int)bus->getStart(), (int)(bus->getStart()+bus->getLength()), (int)(bus->getType() & 0x7F), (int)bus->getColorOrder(), (int)bus->isReversed(), (int)bus->skippedLeds(), (int)bus->getAutoWhiteMode(), (int)bus->getFrequency(), (int)bus->getLEDCurrent(), (int)bus->getMaxCurrent() ); JsonObject ins = hw_led_ins.createNestedObject(); ins["start"] = bus->getStart(); ins["len"] = bus->getLength(); JsonArray ins_pin = ins.createNestedArray("pin"); uint8_t pins[5]; uint8_t nPins = bus->getPins(pins); for (int i = 0; i < nPins; i++) ins_pin.add(pins[i]); ins[F("order")] = bus->getColorOrder(); ins["rev"] = bus->isReversed(); ins[F("skip")] = bus->skippedLeds(); ins["type"] = bus->getType() & 0x7F; ins["ref"] = bus->isOffRefreshRequired(); ins[F("rgbwm")] = bus->getAutoWhiteMode(); ins[F("freq")] = bus->getFrequency(); ins[F("maxpwr")] = bus->getMaxCurrent(); ins[F("ledma")] = bus->getLEDCurrent(); ins[F("drv")] = bus->getDriverType(); ins[F("text")] = bus->getCustomText(); } JsonArray hw_com = hw.createNestedArray(F("com")); const ColorOrderMap& com = BusManager::getColorOrderMap(); for (size_t s = 0; s < com.count(); s++) { const ColorOrderMapEntry *entry = com.get(s); if (!entry || !entry->len) break; JsonObject co = hw_com.createNestedObject(); co["start"] = entry->start; co["len"] = entry->len; co[F("order")] = entry->colorOrder; } // button(s) JsonObject hw_btn = hw.createNestedObject("btn"); hw_btn["max"] = WLED_MAX_BUTTONS; // just information about max number of buttons (not actually used) hw_btn[F("pull")] = !disablePullUp; JsonArray hw_btn_ins = hw_btn.createNestedArray("ins"); // configuration for all buttons for (const auto &button : buttons) { JsonObject hw_btn_ins_0 = hw_btn_ins.createNestedObject(); hw_btn_ins_0["type"] = button.type; JsonArray hw_btn_ins_0_pin = hw_btn_ins_0.createNestedArray("pin"); hw_btn_ins_0_pin.add(button.pin); JsonArray hw_btn_ins_0_macros = hw_btn_ins_0.createNestedArray("macros"); hw_btn_ins_0_macros.add(button.macroButton); hw_btn_ins_0_macros.add(button.macroLongPress); hw_btn_ins_0_macros.add(button.macroDoublePress); } hw_btn[F("tt")] = touchThreshold; hw_btn["mqtt"] = buttonPublishMqtt; JsonObject hw_ir = hw.createNestedObject("ir"); #ifndef WLED_DISABLE_INFRARED hw_ir["pin"] = irPin; hw_ir["type"] = irEnabled; // the byte 'irEnabled' does contain the IR-Remote Type ( 0=disabled ) #endif hw_ir["sel"] = irApplyToAllSelected; JsonObject hw_relay = hw.createNestedObject(F("relay")); hw_relay["pin"] = rlyPin; hw_relay["rev"] = !rlyMde; hw_relay[F("odrain")] = rlyOpenDrain; hw[F("baud")] = serialBaud; JsonObject hw_if = hw.createNestedObject(F("if")); JsonArray hw_if_i2c = hw_if.createNestedArray("i2c-pin"); hw_if_i2c.add(i2c_sda); hw_if_i2c.add(i2c_scl); JsonArray hw_if_spi = hw_if.createNestedArray("spi-pin"); hw_if_spi.add(spi_mosi); hw_if_spi.add(spi_sclk); hw_if_spi.add(spi_miso); //JsonObject hw_status = hw.createNestedObject("status"); //hw_status["pin"] = -1; JsonObject light = root.createNestedObject(F("light")); light[F("scale-bri")] = briMultiplier; light[F("pal-mode")] = paletteBlend; light[F("aseg")] = strip.autoSegments; JsonObject light_gc = light.createNestedObject("gc"); light_gc["bri"] = (gammaCorrectBri) ? gammaCorrectVal : 1.0f; // keep compatibility light_gc["col"] = (gammaCorrectCol) ? gammaCorrectVal : 1.0f; // keep compatibility light_gc["val"] = gammaCorrectVal; JsonObject light_tr = light.createNestedObject("tr"); light_tr["dur"] = transitionDelayDefault / 100; light_tr[F("rpc")] = randomPaletteChangeTime; light_tr[F("hrp")] = useHarmonicRandomPalette; JsonObject light_nl = light.createNestedObject("nl"); light_nl["mode"] = nightlightMode; light_nl["dur"] = nightlightDelayMinsDefault; light_nl[F("tbri")] = nightlightTargetBri; light_nl["macro"] = macroNl; JsonObject def = root.createNestedObject("def"); def["ps"] = bootPreset; def["on"] = turnOnAtBoot; def["bri"] = briS; JsonObject interfaces = root.createNestedObject("if"); JsonObject if_sync = interfaces.createNestedObject("sync"); if_sync[F("port0")] = udpPort; if_sync[F("port1")] = udpPort2; #ifndef WLED_DISABLE_ESPNOW if_sync[F("espnow")] = useESPNowSync; #endif JsonObject if_sync_recv = if_sync.createNestedObject(F("recv")); if_sync_recv["bri"] = receiveNotificationBrightness; if_sync_recv["col"] = receiveNotificationColor; if_sync_recv["fx"] = receiveNotificationEffects; if_sync_recv["pal"] = receiveNotificationPalette; if_sync_recv["grp"] = receiveGroups; if_sync_recv["seg"] = receiveSegmentOptions; if_sync_recv["sb"] = receiveSegmentBounds; JsonObject if_sync_send = if_sync.createNestedObject(F("send")); if_sync_send["en"] = sendNotifications; if_sync_send[F("dir")] = notifyDirect; if_sync_send["btn"] = notifyButton; if_sync_send["va"] = notifyAlexa; if_sync_send["hue"] = notifyHue; if_sync_send["grp"] = syncGroups; if_sync_send["ret"] = udpNumRetries; JsonObject if_nodes = interfaces.createNestedObject("nodes"); if_nodes[F("list")] = nodeListEnabled; if_nodes[F("bcast")] = nodeBroadcastEnabled; JsonObject if_live = interfaces.createNestedObject("live"); if_live["en"] = receiveDirect; // UDP/Hyperion realtime if_live[F("mso")] = useMainSegmentOnly; if_live[F("rlm")] = realtimeRespectLedMaps; if_live["port"] = e131Port; if_live[F("mc")] = e131Multicast; JsonObject if_live_dmx = if_live.createNestedObject("dmx"); if_live_dmx[F("uni")] = e131Universe; if_live_dmx[F("seqskip")] = e131SkipOutOfSequence; if_live_dmx[F("e131prio")] = e131Priority; if_live_dmx[F("addr")] = DMXAddress; if_live_dmx[F("dss")] = DMXSegmentSpacing; if_live_dmx["mode"] = DMXMode; #ifdef WLED_ENABLE_DMX_INPUT if_live_dmx[F("inputRxPin")] = dmxInputTransmitPin; if_live_dmx[F("inputTxPin")] = dmxInputReceivePin; if_live_dmx[F("inputEnablePin")] = dmxInputEnablePin; if_live_dmx[F("dmxInputPort")] = dmxInputPort; #endif if_live[F("timeout")] = realtimeTimeoutMs / 100; if_live[F("maxbri")] = arlsForceMaxBri; if_live[F("no-gc")] = arlsDisableGammaCorrection; if_live[F("offset")] = arlsOffset; #ifndef WLED_DISABLE_ALEXA JsonObject if_va = interfaces.createNestedObject("va"); if_va[F("alexa")] = alexaEnabled; JsonArray if_va_macros = if_va.createNestedArray("macros"); if_va_macros.add(macroAlexaOn); if_va_macros.add(macroAlexaOff); if_va["p"] = alexaNumPresets; #endif #ifndef WLED_DISABLE_MQTT JsonObject if_mqtt = interfaces.createNestedObject("mqtt"); if_mqtt["en"] = mqttEnabled; if_mqtt[F("broker")] = mqttServer; if_mqtt["port"] = mqttPort; if_mqtt[F("user")] = mqttUser; if_mqtt[F("pskl")] = strlen(mqttPass); if_mqtt[F("cid")] = mqttClientID; if_mqtt[F("rtn")] = retainMqttMsg; JsonObject if_mqtt_topics = if_mqtt.createNestedObject(F("topics")); if_mqtt_topics[F("device")] = mqttDeviceTopic; if_mqtt_topics[F("group")] = mqttGroupTopic; #endif #ifndef WLED_DISABLE_HUESYNC JsonObject if_hue = interfaces.createNestedObject("hue"); if_hue["en"] = huePollingEnabled; if_hue["id"] = huePollLightId; if_hue[F("iv")] = huePollIntervalMs / 100; JsonObject if_hue_recv = if_hue.createNestedObject(F("recv")); if_hue_recv["on"] = hueApplyOnOff; if_hue_recv["bri"] = hueApplyBri; if_hue_recv["col"] = hueApplyColor; JsonArray if_hue_ip = if_hue.createNestedArray("ip"); for (unsigned i = 0; i < 4; i++) { if_hue_ip.add(hueIP[i]); } #endif JsonObject if_ntp = interfaces.createNestedObject("ntp"); if_ntp["en"] = ntpEnabled; if_ntp[F("host")] = ntpServerName; if_ntp[F("tz")] = currentTimezone; if_ntp[F("offset")] = utcOffsetSecs; if_ntp[F("ampm")] = useAMPM; if_ntp[F("ln")] = longitude; if_ntp[F("lt")] = latitude; JsonObject ol = root.createNestedObject("ol"); ol[F("clock")] = overlayCurrent; ol[F("cntdwn")] = countdownMode; ol["min"] = overlayMin; ol[F("max")] = overlayMax; ol[F("o12pix")] = analogClock12pixel; ol[F("o5m")] = analogClock5MinuteMarks; ol[F("osec")] = analogClockSecondsTrail; ol[F("osb")] = analogClockSolidBlack; JsonObject timers = root.createNestedObject(F("timers")); JsonObject cntdwn = timers.createNestedObject(F("cntdwn")); JsonArray goal = cntdwn.createNestedArray(F("goal")); goal.add(countdownYear); goal.add(countdownMonth); goal.add(countdownDay); goal.add(countdownHour); goal.add(countdownMin); goal.add(countdownSec); cntdwn["macro"] = macroCountdown; JsonArray timers_ins = timers.createNestedArray("ins"); for (unsigned i = 0; i < 10; i++) { if (timerMacro[i] == 0 && timerHours[i] == 0 && timerMinutes[i] == 0) continue; // sunrise/sunset get saved always (timerHours=255) JsonObject timers_ins0 = timers_ins.createNestedObject(); timers_ins0["en"] = (timerWeekday[i] & 0x01); timers_ins0[F("hour")] = timerHours[i]; timers_ins0["min"] = timerMinutes[i]; timers_ins0["macro"] = timerMacro[i]; timers_ins0[F("dow")] = timerWeekday[i] >> 1; if (i<8) { JsonObject start = timers_ins0.createNestedObject("start"); start["mon"] = (timerMonth[i] >> 4) & 0xF; start["day"] = timerDay[i]; JsonObject end = timers_ins0.createNestedObject("end"); end["mon"] = timerMonth[i] & 0xF; end["day"] = timerDayEnd[i]; } } JsonObject ota = root.createNestedObject("ota"); ota[F("lock")] = otaLock; ota[F("lock-wifi")] = wifiLock; ota[F("pskl")] = strlen(otaPass); #ifndef WLED_DISABLE_OTA ota[F("aota")] = aOtaEnabled; #endif ota[F("same-subnet")] = otaSameSubnet; #ifdef WLED_ENABLE_DMX JsonObject dmx = root.createNestedObject("dmx"); dmx[F("chan")] = DMXChannels; dmx[F("gap")] = DMXGap; dmx["start"] = DMXStart; dmx[F("start-led")] = DMXStartLED; JsonArray dmx_fixmap = dmx.createNestedArray(F("fixmap")); for (unsigned i = 0; i < 15; i++) { dmx_fixmap.add(DMXFixtureMap[i]); } dmx[F("e131proxy")] = e131ProxyUniverse; #endif JsonObject usermods_settings = root.createNestedObject("um"); UsermodManager::addToConfig(usermods_settings); } static const char s_wsec_json[] PROGMEM = "/wsec.json"; //settings in /wsec.json, not accessible via webserver, for passwords and tokens bool deserializeConfigSec() { DEBUG_PRINTLN(F("Reading settings from /wsec.json...")); if (!requestJSONBufferLock(JSON_LOCK_CFG_SEC_DES)) return false; bool success = readObjectFromFile(s_wsec_json, nullptr, pDoc); if (!success) { releaseJSONBufferLock(); return false; } JsonObject root = pDoc->as(); size_t n = 0; JsonArray nw_ins = root["nw"]["ins"]; if (!nw_ins.isNull()) { if (nw_ins.size() > 1 && nw_ins.size() > multiWiFi.size()) multiWiFi.resize(nw_ins.size()); // resize constructs objects while resizing for (JsonObject wifi : nw_ins) { char pw[65] = ""; getStringFromJson(pw, wifi["psk"], 65); strlcpy(multiWiFi[n].clientPass, pw, 65); if (++n >= WLED_MAX_WIFI_COUNT) break; } } JsonObject ap = root["ap"]; getStringFromJson(apPass, ap["psk"] , 65); [[maybe_unused]] JsonObject interfaces = root["if"]; #ifndef WLED_DISABLE_MQTT JsonObject if_mqtt = interfaces["mqtt"]; getStringFromJson(mqttPass, if_mqtt["psk"], 65); #endif #ifndef WLED_DISABLE_HUESYNC getStringFromJson(hueApiKey, interfaces["hue"][F("key")], 47); #endif getStringFromJson(settingsPIN, root["pin"], 5); correctPIN = !strlen(settingsPIN); JsonObject ota = root["ota"]; getStringFromJson(otaPass, ota[F("pwd")], 33); CJSON(otaLock, ota[F("lock")]); CJSON(wifiLock, ota[F("lock-wifi")]); #ifndef WLED_DISABLE_OTA CJSON(aOtaEnabled, ota[F("aota")]); #endif releaseJSONBufferLock(); return true; } void serializeConfigSec() { DEBUG_PRINTLN(F("Writing settings to /wsec.json...")); if (!requestJSONBufferLock(JSON_LOCK_CFG_SEC_SER)) return; JsonObject root = pDoc->to(); JsonObject nw = root.createNestedObject("nw"); JsonArray nw_ins = nw.createNestedArray("ins"); for (size_t i = 0; i < multiWiFi.size(); i++) { JsonObject wifi = nw_ins.createNestedObject(); wifi[F("psk")] = multiWiFi[i].clientPass; } JsonObject ap = root.createNestedObject("ap"); ap["psk"] = apPass; [[maybe_unused]] JsonObject interfaces = root.createNestedObject("if"); #ifndef WLED_DISABLE_MQTT JsonObject if_mqtt = interfaces.createNestedObject("mqtt"); if_mqtt["psk"] = mqttPass; #endif #ifndef WLED_DISABLE_HUESYNC JsonObject if_hue = interfaces.createNestedObject("hue"); if_hue[F("key")] = hueApiKey; #endif root["pin"] = settingsPIN; JsonObject ota = root.createNestedObject("ota"); ota[F("pwd")] = otaPass; ota[F("lock")] = otaLock; ota[F("lock-wifi")] = wifiLock; #ifndef WLED_DISABLE_OTA ota[F("aota")] = aOtaEnabled; #endif File f = WLED_FS.open(FPSTR(s_wsec_json), "w"); if (f) serializeJson(root, f); f.close(); releaseJSONBufferLock(); } ================================================ FILE: wled00/colors.cpp ================================================ #include "wled.h" /* * Color conversion & utility methods */ /* * color blend function, based on FastLED blend function * the calculation for each color is: result = (A*(amountOfA) + A + B*(amountOfB) + B) / 256 with amountOfA = 255 - amountOfB */ uint32_t WLED_O2_ATTR IRAM_ATTR color_blend(uint32_t color1, uint32_t color2, uint8_t blend) { // min / max blend checking is omitted: calls with 0 or 255 are rare, checking lowers overall performance const uint32_t TWO_CHANNEL_MASK = 0x00FF00FF; // mask for R and B channels or W and G if negated (poorman's SIMD; https://github.com/wled/WLED/pull/4568#discussion_r1986587221) uint32_t rb1 = color1 & TWO_CHANNEL_MASK; // extract R & B channels from color1 uint32_t wg1 = (color1 >> 8) & TWO_CHANNEL_MASK; // extract W & G channels from color1 (shifted for multiplication later) uint32_t rb2 = color2 & TWO_CHANNEL_MASK; // extract R & B channels from color2 uint32_t wg2 = (color2 >> 8) & TWO_CHANNEL_MASK; // extract W & G channels from color2 (shifted for multiplication later) uint32_t rb3 = ((((rb1 << 8) | rb2) + (rb2 * blend) - (rb1 * blend)) >> 8) & TWO_CHANNEL_MASK; // blend red and blue uint32_t wg3 = ((((wg1 << 8) | wg2) + (wg2 * blend) - (wg1 * blend))) & ~TWO_CHANNEL_MASK; // negated mask for white and green return rb3 | wg3; } /* * color add function that preserves ratio * original idea: https://github.com/wled-dev/WLED/pull/2465 by https://github.com/Proto-molecule * speed optimisations by @dedehai */ uint32_t WLED_O2_ATTR color_add(uint32_t c1, uint32_t c2, bool preserveCR) //1212558 | 1212598 | 1212576 | 1212530 { if (c1 == BLACK) return c2; if (c2 == BLACK) return c1; const uint32_t TWO_CHANNEL_MASK = 0x00FF00FF; // mask for R and B channels or W and G if negated uint32_t rb = ( c1 & TWO_CHANNEL_MASK) + ( c2 & TWO_CHANNEL_MASK); // mask and add two colors at once uint32_t wg = ((c1>>8) & TWO_CHANNEL_MASK) + ((c2>>8) & TWO_CHANNEL_MASK); if (preserveCR) { // preserve color ratios uint32_t overflow = (rb | wg) & 0x01000100; // detect overflow by checking 9th bit if (overflow) { uint32_t r = rb >> 16; // extract single color values uint32_t b = rb & 0xFFFF; uint32_t w = wg >> 16; uint32_t g = wg & 0xFFFF; uint32_t max = std::max(r,g); max = std::max(max,b); max = std::max(max,w); const uint32_t scale = (uint32_t(255)<<8) / max; // division of two 8bit (shifted) values does not work -> use bit shifts and multiplaction instead rb = ((rb * scale) >> 8) & TWO_CHANNEL_MASK; wg = (wg * scale) & ~TWO_CHANNEL_MASK; } else wg <<= 8; //shift white and green back to correct position } else { // branchless per-channel saturation to 255 (extract 9th bit, subtract 1 if it is set, mask with 0xFF, input is 0xFF+0xFF=0x1EF max) // example with overflow: input: 0x01EF01EF -> (0x0100100 - 0x00010001) = 0x00FF00FF -> input|0x00FF00FF = 0x00FF00FF (saturate) // example without overflow: input: 0x007F007F -> (0x00000000 - 0x00000000) = 0x00000000 -> input|0x00000000 = input (no change) rb |= ((rb & 0x01000100) - ((rb >> 8) & 0x00010001)) & 0x00FF00FF; wg |= ((wg & 0x01000100) - ((wg >> 8) & 0x00010001)) & 0x00FF00FF; wg <<= 8; // restore WG position } return rb | wg; } /* * fades color toward black * if using "video" method the resulting color will never become black unless it is already black */ uint32_t IRAM_ATTR color_fade(uint32_t c1, uint8_t amount, bool video) { if (c1 == BLACK || amount == 0) return 0; // black or full fade if (amount == 255) return c1; // no change uint32_t addRemains = 0; if (!video) amount++; // add one for correct scaling using bitshifts else { // video scaling: make sure colors do not dim to zero if they started non-zero unless they distort the hue uint8_t r = byte(c1>>16), g = byte(c1>>8), b = byte(c1), w = byte(c1>>24); // extract r, g, b, w channels uint8_t maxc = (r > g) ? ((r > b) ? r : b) : ((g > b) ? g : b); // determine dominant channel for hue preservation addRemains = r && (r<<5) > maxc ? 0x00010000 : 0; // note: setting color preservation threshold too high results in flickering and addRemains |= g && (g<<5) > maxc ? 0x00000100 : 0; // jumping colors in low brightness gradients. Multiplying the color preserves addRemains |= b && (b<<5) > maxc ? 0x00000001 : 0; // better accuracy than dividing the maxc. Shifting by 5 is a good compromise addRemains |= w ? 0x01000000 : 0; // i.e. remove color channel if <13% of max } const uint32_t TWO_CHANNEL_MASK = 0x00FF00FF; uint32_t rb = (((c1 & TWO_CHANNEL_MASK) * amount) >> 8) & TWO_CHANNEL_MASK; // scale red and blue uint32_t wg = (((c1 >> 8) & TWO_CHANNEL_MASK) * amount) & ~TWO_CHANNEL_MASK; // scale white and green return (rb | wg) + addRemains; } /* * color adjustment in HSV color space (converts RGB to HSV and back), color conversions are not 100% accurate! shifts hue, increase brightness, decreases saturation (if not black) note: inputs are 32bit to speed up the function, useful input value ranges are 0-255 */ uint32_t adjust_color(uint32_t rgb, uint32_t hueShift, uint32_t lighten, uint32_t brighten) { if (rgb == 0 || hueShift + lighten + brighten == 0) return rgb; // black or no change CHSV32 hsv; rgb2hsv(rgb, hsv); //convert to HSV hsv.h += (hueShift << 8); // shift hue (hue is 16 bits) hsv.s = max((int32_t)0, (int32_t)hsv.s - (int32_t)lighten); // desaturate hsv.v = min((uint32_t)255, (uint32_t)hsv.v + brighten); // increase brightness uint32_t rgb_adjusted; hsv2rgb(hsv, rgb_adjusted); // convert back to RGB TODO: make this into 16 bit conversion return rgb_adjusted; } // 1:1 replacement of fastled function optimized for ESP, slightly faster, more accurate and uses less flash (~ -200bytes) uint32_t ColorFromPaletteWLED(const CRGBPalette16& pal, unsigned index, uint8_t brightness, TBlendType blendType) { if (blendType == LINEARBLEND_NOWRAP) { index = (index * 0xF0) >> 8; // Blend range is affected by lo4 blend of values, remap to avoid wrapping } unsigned hi4 = byte(index) >> 4; unsigned lo4 = (index & 0x0F); const CRGB* entry = (CRGB*)&(pal[0]) + hi4; unsigned red1 = entry->r; unsigned green1 = entry->g; unsigned blue1 = entry->b; if (lo4 && blendType != NOBLEND) { if (hi4 == 15) entry = &(pal[0]); else ++entry; unsigned f2 = (lo4 << 4); unsigned f1 = 256 - f2; red1 = (red1 * f1 + (unsigned)entry->r * f2) >> 8; // note: using color_blend() is slower green1 = (green1 * f1 + (unsigned)entry->g * f2) >> 8; blue1 = (blue1 * f1 + (unsigned)entry->b * f2) >> 8; } if (brightness < 255) { // note: zero checking could be done to return black but that is hardly ever used so it is omitted // actually same as color_fade(), using color_fade() is slower uint32_t scale = brightness + 1; // adjust for rounding (bitshift) red1 = (red1 * scale) >> 8; green1 = (green1 * scale) >> 8; blue1 = (blue1 * scale) >> 8; } return RGBW32(red1,green1,blue1,0); } void setRandomColor(byte* rgb) { lastRandomIndex = get_random_wheel_index(lastRandomIndex); colorHStoRGB(lastRandomIndex*256,255,rgb); } /* * generates a random palette based on harmonic color theory * takes a base palette as the input, it will choose one color of the base palette and keep it */ CRGBPalette16 generateHarmonicRandomPalette(const CRGBPalette16 &basepalette) { CHSV palettecolors[4]; // array of colors for the new palette uint8_t keepcolorposition = hw_random8(4); // color position of current random palette to keep palettecolors[keepcolorposition] = rgb2hsv(basepalette.entries[keepcolorposition*5]); // read one of the base colors of the current palette palettecolors[keepcolorposition].hue += hw_random8(10)-5; // +/- 5 randomness of base color // generate 4 saturation and brightness value numbers // only one saturation is allowed to be below 200 creating mostly vibrant colors // only one brightness value number is allowed below 200, creating mostly bright palettes for (int i = 0; i < 3; i++) { // generate three high values palettecolors[i].saturation = hw_random8(200,255); palettecolors[i].value = hw_random8(220,255); } // allow one to be lower palettecolors[3].saturation = hw_random8(20,255); palettecolors[3].value = hw_random8(80,255); // shuffle the arrays for (int i = 3; i > 0; i--) { std::swap(palettecolors[i].saturation, palettecolors[hw_random8(i + 1)].saturation); std::swap(palettecolors[i].value, palettecolors[hw_random8(i + 1)].value); } // now generate three new hues based off of the hue of the chosen current color uint8_t basehue = palettecolors[keepcolorposition].hue; uint8_t harmonics[3]; // hues that are harmonic but still a little random uint8_t type = hw_random8(5); // choose a harmony type switch (type) { case 0: // analogous harmonics[0] = basehue + hw_random8(30, 50); harmonics[1] = basehue + hw_random8(10, 30); harmonics[2] = basehue - hw_random8(10, 30); break; case 1: // triadic harmonics[0] = basehue + 113 + hw_random8(15); harmonics[1] = basehue + 233 + hw_random8(15); harmonics[2] = basehue - 7 + hw_random8(15); break; case 2: // split-complementary harmonics[0] = basehue + 145 + hw_random8(10); harmonics[1] = basehue + 205 + hw_random8(10); harmonics[2] = basehue - 5 + hw_random8(10); break; case 3: // square harmonics[0] = basehue + 85 + hw_random8(10); harmonics[1] = basehue + 175 + hw_random8(10); harmonics[2] = basehue + 265 + hw_random8(10); break; case 4: // tetradic harmonics[0] = basehue + 80 + hw_random8(20); harmonics[1] = basehue + 170 + hw_random8(20); harmonics[2] = basehue - 15 + hw_random8(30); break; } if (hw_random8() < 128) { // 50:50 chance of shuffling hues or keep the color order for (int i = 2; i > 0; i--) { std::swap(harmonics[i], harmonics[hw_random8(i + 1)]); } } // now set the hues int j = 0; for (int i = 0; i < 4; i++) { if (i==keepcolorposition) continue; // skip the base color palettecolors[i].hue = harmonics[j]; j++; } bool makepastelpalette = false; if (hw_random8() < 25) { // ~10% chance of desaturated 'pastel' colors makepastelpalette = true; } // apply saturation CRGB RGBpalettecolors[4]; for (int i = 0; i < 4; i++) { if (makepastelpalette && palettecolors[i].saturation > 180) { palettecolors[i].saturation -= 160; //desaturate all four colors } RGBpalettecolors[i] = (CRGB)palettecolors[i]; //convert to RGB RGBpalettecolors[i] = ((uint32_t)RGBpalettecolors[i]) & 0x00FFFFFFU; //strip alpha from CRGB } return CRGBPalette16(RGBpalettecolors[0], RGBpalettecolors[1], RGBpalettecolors[2], RGBpalettecolors[3]); } CRGBPalette16 generateRandomPalette() // generate fully random palette { return CRGBPalette16(CHSV(hw_random8(), hw_random8(160, 255), hw_random8(128, 255)), CHSV(hw_random8(), hw_random8(160, 255), hw_random8(128, 255)), CHSV(hw_random8(), hw_random8(160, 255), hw_random8(128, 255)), CHSV(hw_random8(), hw_random8(160, 255), hw_random8(128, 255))); } void loadCustomPalettes() { byte tcp[72]; //support gradient palettes with up to 18 entries CRGBPalette16 targetPalette; customPalettes.clear(); // start fresh StaticJsonDocument<1536> pDoc; // barely enough to fit 72 numbers -> TODO: current format uses 214 bytes max per palette, why is this buffer so large? unsigned emptyPaletteGap = 0; // count gaps in palette files to stop looking for more (each exists() call takes ~5ms) for (int index = 0; index < WLED_MAX_CUSTOM_PALETTES; index++) { char fileName[32]; sprintf_P(fileName, PSTR("/palette%d.json"), index); if (WLED_FS.exists(fileName)) { emptyPaletteGap = 0; // reset gap counter if file exists DEBUGFX_PRINTF_P(PSTR("Reading palette from %s\n"), fileName); if (readObjectFromFile(fileName, nullptr, &pDoc)) { JsonArray pal = pDoc[F("palette")]; if (!pal.isNull() && pal.size()>3) { // not an empty palette (at least 2 entries) memset(tcp, 255, sizeof(tcp)); if (pal[0].is() && pal[1].is()) { // we have an array of index & hex strings size_t palSize = MIN(pal.size(), 36); palSize -= palSize % 2; // make sure size is multiple of 2 for (size_t i=0, j=0; i()<256; i+=2) { uint8_t rgbw[] = {0,0,0,0}; if (colorFromHexString(rgbw, pal[i+1].as())) { // will catch non-string entires tcp[ j ] = (uint8_t) pal[ i ].as(); // index for (size_t c=0; c<3; c++) tcp[j+1+c] = rgbw[c]; // only use RGB component DEBUGFX_PRINTF_P(PSTR("%2u -> %3d [%3d,%3d,%3d]\n"), i, int(tcp[j]), int(tcp[j+1]), int(tcp[j+2]), int(tcp[j+3])); j += 4; } } } else { size_t palSize = MIN(pal.size(), 72); palSize -= palSize % 4; // make sure size is multiple of 4 for (size_t i=0; i()<256; i+=4) { tcp[ i ] = (uint8_t) pal[ i ].as(); // index for (size_t c=0; c<3; c++) tcp[i+1+c] = (uint8_t) pal[i+1+c].as(); DEBUGFX_PRINTF_P(PSTR("%2u -> %3d [%3d,%3d,%3d]\n"), i, int(tcp[i]), int(tcp[i+1]), int(tcp[i+2]), int(tcp[i+3])); } } customPalettes.push_back(targetPalette.loadDynamicGradientPalette(tcp)); } else { DEBUGFX_PRINTLN(F("Wrong palette format.")); } } } else { emptyPaletteGap++; if (emptyPaletteGap > WLED_MAX_CUSTOM_PALETTE_GAP) break; // stop looking for more palettes } } } void hsv2rgb(const CHSV32& hsv, uint32_t& rgb) // convert HSV (16bit hue) to RGB (32bit with white = 0) { unsigned int remainder, region, p, q, t; unsigned int h = hsv.h; unsigned int s = hsv.s; unsigned int v = hsv.v; if (s == 0) { rgb = v << 16 | v << 8 | v; return; } region = h / 10923; // 65536 / 6 = 10923 remainder = (h - (region * 10923)) * 6; p = (v * (255 - s)) >> 8; q = (v * (255 - ((s * remainder) >> 16))) >> 8; t = (v * (255 - ((s * (65535 - remainder)) >> 16))) >> 8; switch (region) { case 0: rgb = v << 16 | t << 8 | p; break; case 1: rgb = q << 16 | v << 8 | p; break; case 2: rgb = p << 16 | v << 8 | t; break; case 3: rgb = p << 16 | q << 8 | v; break; case 4: rgb = t << 16 | p << 8 | v; break; default: rgb = v << 16 | p << 8 | q; break; } } void rgb2hsv(const uint32_t rgb, CHSV32& hsv) // convert RGB to HSV (16bit hue), much more accurate and faster than fastled version { hsv.raw = 0; int32_t r = (rgb>>16)&0xFF; int32_t g = (rgb>>8)&0xFF; int32_t b = rgb&0xFF; int32_t minval, maxval, delta; minval = min(r, g); minval = min(minval, b); maxval = max(r, g); maxval = max(maxval, b); if (maxval == 0) return; // black hsv.v = maxval; delta = maxval - minval; hsv.s = (255 * delta) / maxval; if (hsv.s == 0) return; // gray value if (maxval == r) hsv.h = (10923 * (g - b)) / delta; else if (maxval == g) hsv.h = 21845 + (10923 * (b - r)) / delta; else hsv.h = 43690 + (10923 * (r - g)) / delta; } void colorHStoRGB(uint16_t hue, byte sat, byte* rgb) { //hue, sat to rgb uint32_t crgb; hsv2rgb(CHSV32(hue, sat, 255), crgb); rgb[0] = byte((crgb) >> 16); rgb[1] = byte((crgb) >> 8); rgb[2] = byte(crgb); } //get RGB values from color temperature in K (https://tannerhelland.com/2012/09/18/convert-temperature-rgb-algorithm-code.html) void colorKtoRGB(uint16_t kelvin, byte* rgb) //white spectrum to rgb, calc { int r = 0, g = 0, b = 0; float temp = kelvin / 100.0f; if (temp <= 66.0f) { r = 255; g = roundf(99.4708025861f * logf(temp) - 161.1195681661f); if (temp <= 19.0f) { b = 0; } else { b = roundf(138.5177312231f * logf((temp - 10.0f)) - 305.0447927307f); } } else { r = roundf(329.698727446f * powf((temp - 60.0f), -0.1332047592f)); g = roundf(288.1221695283f * powf((temp - 60.0f), -0.0755148492f)); b = 255; } //g += 12; //mod by Aircoookie, a bit less accurate but visibly less pinkish rgb[0] = (uint8_t) constrain(r, 0, 255); rgb[1] = (uint8_t) constrain(g, 0, 255); rgb[2] = (uint8_t) constrain(b, 0, 255); rgb[3] = 0; } void colorCTtoRGB(uint16_t mired, byte* rgb) //white spectrum to rgb, bins { //this is only an approximation using WS2812B with gamma correction enabled if (mired > 475) { rgb[0]=255;rgb[1]=199;rgb[2]=92;//500 } else if (mired > 425) { rgb[0]=255;rgb[1]=213;rgb[2]=118;//450 } else if (mired > 375) { rgb[0]=255;rgb[1]=216;rgb[2]=118;//400 } else if (mired > 325) { rgb[0]=255;rgb[1]=234;rgb[2]=140;//350 } else if (mired > 275) { rgb[0]=255;rgb[1]=243;rgb[2]=160;//300 } else if (mired > 225) { rgb[0]=250;rgb[1]=255;rgb[2]=188;//250 } else if (mired > 175) { rgb[0]=247;rgb[1]=255;rgb[2]=215;//200 } else { rgb[0]=237;rgb[1]=255;rgb[2]=239;//150 } } #ifndef WLED_DISABLE_HUESYNC void colorXYtoRGB(float x, float y, byte* rgb) //coordinates to rgb (https://www.developers.meethue.com/documentation/color-conversions-rgb-xy) { float z = 1.0f - x - y; float X = (1.0f / y) * x; float Z = (1.0f / y) * z; float r = (int)255*(X * 1.656492f - 0.354851f - Z * 0.255038f); float g = (int)255*(-X * 0.707196f + 1.655397f + Z * 0.036152f); float b = (int)255*(X * 0.051713f - 0.121364f + Z * 1.011530f); if (r > b && r > g && r > 1.0f) { // red is too big g = g / r; b = b / r; r = 1.0f; } else if (g > b && g > r && g > 1.0f) { // green is too big r = r / g; b = b / g; g = 1.0f; } else if (b > r && b > g && b > 1.0f) { // blue is too big r = r / b; g = g / b; b = 1.0f; } // Apply gamma correction r = r <= 0.0031308f ? 12.92f * r : (1.0f + 0.055f) * powf(r, (1.0f / 2.4f)) - 0.055f; g = g <= 0.0031308f ? 12.92f * g : (1.0f + 0.055f) * powf(g, (1.0f / 2.4f)) - 0.055f; b = b <= 0.0031308f ? 12.92f * b : (1.0f + 0.055f) * powf(b, (1.0f / 2.4f)) - 0.055f; if (r > b && r > g) { // red is biggest if (r > 1.0f) { g = g / r; b = b / r; r = 1.0f; } } else if (g > b && g > r) { // green is biggest if (g > 1.0f) { r = r / g; b = b / g; g = 1.0f; } } else if (b > r && b > g) { // blue is biggest if (b > 1.0f) { r = r / b; g = g / b; b = 1.0f; } } rgb[0] = byte(255.0f*r); rgb[1] = byte(255.0f*g); rgb[2] = byte(255.0f*b); } void colorRGBtoXY(const byte* rgb, float* xy) //rgb to coordinates (https://www.developers.meethue.com/documentation/color-conversions-rgb-xy) { float X = rgb[0] * 0.664511f + rgb[1] * 0.154324f + rgb[2] * 0.162028f; float Y = rgb[0] * 0.283881f + rgb[1] * 0.668433f + rgb[2] * 0.047685f; float Z = rgb[0] * 0.000088f + rgb[1] * 0.072310f + rgb[2] * 0.986039f; xy[0] = X / (X + Y + Z); xy[1] = Y / (X + Y + Z); } #endif // WLED_DISABLE_HUESYNC //RRGGBB / WWRRGGBB order for hex void colorFromDecOrHexString(byte* rgb, const char* in) { if (in[0] == 0) return; char first = in[0]; uint32_t c = 0; if (first == '#' || first == 'h' || first == 'H') //is HEX encoded { c = strtoul(in +1, NULL, 16); } else { c = strtoul(in, NULL, 10); } rgb[0] = R(c); rgb[1] = G(c); rgb[2] = B(c); rgb[3] = W(c); } //contrary to the colorFromDecOrHexString() function, this uses the more standard RRGGBB / RRGGBBWW order bool colorFromHexString(byte* rgb, const char* in) { if (in == nullptr) return false; size_t inputSize = strnlen(in, 9); if (inputSize != 6 && inputSize != 8) return false; uint32_t c = strtoul(in, NULL, 16); if (inputSize == 6) { rgb[0] = (c >> 16); rgb[1] = (c >> 8); rgb[2] = c ; } else { rgb[0] = (c >> 24); rgb[1] = (c >> 16); rgb[2] = (c >> 8); rgb[3] = c ; } return true; } static inline float minf(float v, float w) { if (w > v) return v; return w; } static inline float maxf(float v, float w) { if (w > v) return w; return v; } // adjust RGB values based on color temperature in K (range [2800-10200]) (https://en.wikipedia.org/wiki/Color_balance) // called from bus manager when color correction is enabled! uint32_t colorBalanceFromKelvin(uint16_t kelvin, uint32_t rgb) { //remember so that slow colorKtoRGB() doesn't have to run for every setPixelColor() static byte correctionRGB[4] = {0,0,0,0}; static uint16_t lastKelvin = 0; if (lastKelvin != kelvin) colorKtoRGB(kelvin, correctionRGB); // convert Kelvin to RGB lastKelvin = kelvin; byte rgbw[4]; rgbw[0] = ((uint16_t) correctionRGB[0] * R(rgb)) /255; // correct R rgbw[1] = ((uint16_t) correctionRGB[1] * G(rgb)) /255; // correct G rgbw[2] = ((uint16_t) correctionRGB[2] * B(rgb)) /255; // correct B rgbw[3] = W(rgb); return RGBW32(rgbw[0],rgbw[1],rgbw[2],rgbw[3]); } //approximates a Kelvin color temperature from an RGB color. //this does no check for the "whiteness" of the color, //so should be used combined with a saturation check (as done by auto-white) //values from http://www.vendian.org/mncharity/dir3/blackbody/UnstableURLs/bbr_color.html (10deg) //equation spreadsheet at https://bit.ly/30RkHaN //accuracy +-50K from 1900K up to 8000K //minimum returned: 1900K, maximum returned: 10091K (range of 8192) uint16_t approximateKelvinFromRGB(uint32_t rgb) { //if not either red or blue is 255, color is dimmed. Scale up uint8_t r = R(rgb), b = B(rgb); if (r == b) return 6550; //red == blue at about 6600K (also can't go further if both R and B are 0) if (r > b) { //scale blue up as if red was at 255 uint16_t scale = 0xFFFF / r; //get scale factor (range 257-65535) b = ((uint16_t)b * scale) >> 8; //For all temps K<6600 R is bigger than B (for full bri colors R=255) //-> Use 9 linear approximations for blackbody radiation blue values from 2000-6600K (blue is always 0 below 2000K) if (b < 33) return 1900 + b *6; if (b < 72) return 2100 + (b-33) *10; if (b < 101) return 2492 + (b-72) *14; if (b < 132) return 2900 + (b-101) *16; if (b < 159) return 3398 + (b-132) *19; if (b < 186) return 3906 + (b-159) *22; if (b < 210) return 4500 + (b-186) *25; if (b < 230) return 5100 + (b-210) *30; return 5700 + (b-230) *34; } else { //scale red up as if blue was at 255 uint16_t scale = 0xFFFF / b; //get scale factor (range 257-65535) r = ((uint16_t)r * scale) >> 8; //For all temps K>6600 B is bigger than R (for full bri colors B=255) //-> Use 2 linear approximations for blackbody radiation red values from 6600-10091K (blue is always 0 below 2000K) if (r > 225) return 6600 + (254-r) *50; uint16_t k = 8080 + (225-r) *86; return (k > 10091) ? 10091 : k; } } // gamma lookup tables used for color correction (filled on 1st use (cfg.cpp & set.cpp)) uint8_t NeoGammaWLEDMethod::gammaT[256]; uint8_t NeoGammaWLEDMethod::gammaT_inv[256]; // re-calculates & fills gamma tables void NeoGammaWLEDMethod::calcGammaTable(float gamma) { float gamma_inv = 1.0f / gamma; // inverse gamma for (size_t i = 1; i < 256; i++) { gammaT[i] = (int)(powf((float)i / 255.0f, gamma) * 255.0f + 0.5f); gammaT_inv[i] = (int)(powf(((float)i - 0.5f) / 255.0f, gamma_inv) * 255.0f + 0.5f); //DEBUG_PRINTF_P(PSTR("gammaT[%d] = %d gammaT_inv[%d] = %d\n"), i, gammaT[i], i, gammaT_inv[i]); } gammaT[0] = 0; gammaT_inv[0] = 0; } uint8_t NeoGammaWLEDMethod::Correct(uint8_t value) { if (!gammaCorrectCol) return value; return gammaT[value]; } uint32_t NeoGammaWLEDMethod::inverseGamma32(uint32_t color) { if (!gammaCorrectCol) return color; uint8_t w = W(color); uint8_t r = R(color); uint8_t g = G(color); uint8_t b = B(color); w = gammaT_inv[w]; r = gammaT_inv[r]; g = gammaT_inv[g]; b = gammaT_inv[b]; return RGBW32(r, g, b, w); } ================================================ FILE: wled00/colors.h ================================================ #pragma once #ifndef WLED_COLORS_H #define WLED_COLORS_H /* * Color structs and color utility functions */ #include #include "FastLED.h" #define ColorFromPalette ColorFromPaletteWLED // override fastled version // CRGBW can be used to manipulate 32bit colors faster. However: if it is passed to functions, it adds overhead compared to a uint32_t color // use with caution and pay attention to flash size. Usually converting a uint32_t to CRGBW to extract r, g, b, w values is slower than using bitshifts // it can be useful to avoid back and forth conversions between uint32_t and fastled CRGB struct CRGBW { union { uint32_t color32; // Access as a 32-bit value (0xWWRRGGBB) struct { uint8_t b; uint8_t g; uint8_t r; uint8_t w; }; uint8_t raw[4]; // Access as an array in the order B, G, R, W }; // Default constructor inline CRGBW() __attribute__((always_inline)) = default; // Constructor from a 32-bit color (0xWWRRGGBB) constexpr CRGBW(uint32_t color) __attribute__((always_inline)) : color32(color) {} // Constructor with r, g, b, w values constexpr CRGBW(uint8_t red, uint8_t green, uint8_t blue, uint8_t white = 0) __attribute__((always_inline)) : b(blue), g(green), r(red), w(white) {} // Constructor from CRGB constexpr CRGBW(CRGB rgb) __attribute__((always_inline)) : b(rgb.b), g(rgb.g), r(rgb.r), w(0) {} // Access as an array inline const uint8_t& operator[] (uint8_t x) const __attribute__((always_inline)) { return raw[x]; } // Assignment from 32-bit color inline CRGBW& operator=(uint32_t color) __attribute__((always_inline)) { color32 = color; return *this; } // Assignment from r, g, b, w inline CRGBW& operator=(const CRGB& rgb) __attribute__((always_inline)) { b = rgb.b; g = rgb.g; r = rgb.r; w = 0; return *this; } // Conversion operator to uint32_t inline operator uint32_t() const __attribute__((always_inline)) { return color32; } /* // Conversion operator to CRGB inline operator CRGB() const __attribute__((always_inline)) { return CRGB(r, g, b); } CRGBW& scale32 (uint8_t scaledown) // 32bit math { if (color32 == 0) return *this; // 2 extra instructions, worth it if called a lot on black (which probably is true) adding check if scaledown is zero adds much more overhead as its 8bit uint32_t scale = scaledown + 1; uint32_t rb = (((color32 & 0x00FF00FF) * scale) >> 8) & 0x00FF00FF; // scale red and blue uint32_t wg = (((color32 & 0xFF00FF00) >> 8) * scale) & 0xFF00FF00; // scale white and green color32 = rb | wg; return *this; }*/ }; struct CHSV32 { // 32bit HSV color with 16bit hue for more accurate conversions union { struct { uint16_t h; // hue uint8_t s; // saturation uint8_t v; // value }; uint32_t raw; // 32bit access }; inline CHSV32() __attribute__((always_inline)) = default; // default constructor /// Allow construction from hue, saturation, and value /// @param ih input hue /// @param is input saturation /// @param iv input value inline CHSV32(uint16_t ih, uint8_t is, uint8_t iv) __attribute__((always_inline)) // constructor from 16bit h, s, v : h(ih), s(is), v(iv) {} inline CHSV32(uint8_t ih, uint8_t is, uint8_t iv) __attribute__((always_inline)) // constructor from 8bit h, s, v : h((uint16_t)ih << 8), s(is), v(iv) {} inline CHSV32(const CHSV& chsv) __attribute__((always_inline)) // constructor from CHSV : h((uint16_t)chsv.h << 8), s(chsv.s), v(chsv.v) {} inline operator CHSV() const { return CHSV((uint8_t)(h >> 8), s, v); } // typecast to CHSV }; extern bool gammaCorrectCol; // similar to NeoPixelBus NeoGammaTableMethod but allows dynamic changes (superseded by NPB::NeoGammaDynamicTableMethod) class NeoGammaWLEDMethod { public: [[gnu::hot]] static uint8_t Correct(uint8_t value); // apply Gamma to single channel [[gnu::hot]] static uint32_t inverseGamma32(uint32_t color); // apply inverse Gamma to RGBW32 color static void calcGammaTable(float gamma); // re-calculates & fills gamma tables static inline uint8_t rawGamma8(uint8_t val) { return gammaT[val]; } // get value from Gamma table (WLED specific, not used by NPB) static inline uint8_t rawInverseGamma8(uint8_t val) { return gammaT_inv[val]; } // get value from inverse Gamma table (WLED specific, not used by NPB) static inline uint32_t Correct32(uint32_t color) { // apply Gamma to RGBW32 color (WLED specific, not used by NPB) if (!gammaCorrectCol) return color; // no gamma correction uint8_t w = byte(color>>24), r = byte(color>>16), g = byte(color>>8), b = byte(color); // extract r, g, b, w channels w = gammaT[w]; r = gammaT[r]; g = gammaT[g]; b = gammaT[b]; return (uint32_t(w) << 24) | (uint32_t(r) << 16) | (uint32_t(g) << 8) | uint32_t(b); } private: static uint8_t gammaT[]; static uint8_t gammaT_inv[]; }; #define gamma32(c) NeoGammaWLEDMethod::Correct32(c) #define gamma8(c) NeoGammaWLEDMethod::rawGamma8(c) #define gamma32inv(c) NeoGammaWLEDMethod::inverseGamma32(c) #define gamma8inv(c) NeoGammaWLEDMethod::rawInverseGamma8(c) [[gnu::hot, gnu::pure]] uint32_t color_blend(uint32_t c1, uint32_t c2 , uint8_t blend); inline uint32_t color_blend16(uint32_t c1, uint32_t c2, uint16_t b) { return color_blend(c1, c2, b >> 8); }; [[gnu::hot, gnu::pure]] uint32_t color_add(uint32_t, uint32_t, bool preserveCR = false); [[gnu::hot, gnu::pure]] uint32_t color_fade(uint32_t c1, uint8_t amount, bool video = false); [[gnu::hot, gnu::pure]] uint32_t adjust_color(uint32_t rgb, uint32_t hueShift, uint32_t lighten, uint32_t brighten); [[gnu::hot, gnu::pure]] uint32_t ColorFromPaletteWLED(const CRGBPalette16 &pal, unsigned index, uint8_t brightness = (uint8_t)255U, TBlendType blendType = LINEARBLEND); CRGBPalette16 generateHarmonicRandomPalette(const CRGBPalette16 &basepalette); CRGBPalette16 generateRandomPalette(); void loadCustomPalettes(); extern std::vector customPalettes; inline size_t getPaletteCount() { return FIXED_PALETTE_COUNT + customPalettes.size(); } inline uint32_t colorFromRgbw(byte* rgbw) { return uint32_t((byte(rgbw[3]) << 24) | (byte(rgbw[0]) << 16) | (byte(rgbw[1]) << 8) | (byte(rgbw[2]))); } void hsv2rgb(const CHSV32& hsv, uint32_t& rgb); void colorHStoRGB(uint16_t hue, byte sat, byte* rgb); void rgb2hsv(const uint32_t rgb, CHSV32& hsv); inline CHSV rgb2hsv(const CRGB c) { CHSV32 hsv; rgb2hsv((uint32_t((byte(c.r) << 16) | (byte(c.g) << 8) | (byte(c.b)))), hsv); return CHSV(hsv); } // CRGB to hsv void colorKtoRGB(uint16_t kelvin, byte* rgb); void colorCTtoRGB(uint16_t mired, byte* rgb); //white spectrum to rgb void colorXYtoRGB(float x, float y, byte* rgb); // only defined if huesync disabled TODO void colorRGBtoXY(const byte* rgb, float* xy); // only defined if huesync disabled TODO void colorFromDecOrHexString(byte* rgb, const char* in); bool colorFromHexString(byte* rgb, const char* in); uint32_t colorBalanceFromKelvin(uint16_t kelvin, uint32_t rgb); uint16_t approximateKelvinFromRGB(uint32_t rgb); void setRandomColor(byte* rgb); // fast scaling function for colors, performs color*scale/256 for all four channels, speed over accuracy // note: inlining uses less code than actual function calls static inline uint32_t fast_color_scale(const uint32_t c, const uint8_t scale) { uint32_t rb = (((c & 0x00FF00FF) * scale) >> 8) & 0x00FF00FF; uint32_t wg = (((c>>8) & 0x00FF00FF) * scale) & ~0x00FF00FF; return rb | wg; } // palettes extern const TProgmemRGBPalette16* const fastledPalettes[]; extern const uint8_t* const gGradientPalettes[]; #endif ================================================ FILE: wled00/const.h ================================================ #pragma once #ifndef WLED_CONST_H #define WLED_CONST_H /* * Readability defines and their associated numerical values + compile-time constants */ constexpr size_t FASTLED_PALETTE_COUNT = 7; // 6-12 = sizeof(fastledPalettes) / sizeof(fastledPalettes[0]); constexpr size_t GRADIENT_PALETTE_COUNT = 59; // 13-72 = sizeof(gGradientPalettes) / sizeof(gGradientPalettes[0]); constexpr size_t DYNAMIC_PALETTE_COUNT = 6; // 0- 5 = dynamic palettes (0=default(virtual),1=random,2=primary,3=primary+secondary,4=primary+secondary+tertiary,5=primary+secondary(+tertiary if not black) constexpr size_t FIXED_PALETTE_COUNT = DYNAMIC_PALETTE_COUNT + FASTLED_PALETTE_COUNT + GRADIENT_PALETTE_COUNT; // total number of fixed palettes #ifndef ESP8266 #define WLED_MAX_CUSTOM_PALETTES (255 - FIXED_PALETTE_COUNT) // allow up to 255 total palettes, user is warned about stability issues when adding more than 10 #else #define WLED_MAX_CUSTOM_PALETTES 10 // ESP8266: limit custom palettes to 10 #endif #define WLED_MAX_CUSTOM_PALETTE_GAP 20 // max number of empty palette files in a row before stopping to look for more (20 takes 100ms) // You can define custom product info from build flags. // This is useful to allow API consumer to identify what type of WLED version // they are interacting with. Be aware that changing this might cause some third // party API consumers to consider this as a non-WLED device since the values // returned by the API and by MQTT will no longer be default. However, most // third party only uses mDNS to validate, so this is generally fine to change. // For example, Home Assistant will still work fine even with this value changed. // Use like this: // -D WLED_BRAND="\"Custom Brand\"" // -D WLED_PRODUCT_NAME="\"Custom Product\"" #ifndef WLED_BRAND #define WLED_BRAND "WLED" #endif #ifndef WLED_PRODUCT_NAME #define WLED_PRODUCT_NAME "FOSS" #endif //Defaults #define DEFAULT_CLIENT_SSID "Your_Network" #define DEFAULT_AP_SSID WLED_BRAND "-AP" #define DEFAULT_AP_PASS "wled1234" #define DEFAULT_OTA_PASS "wledota" #define DEFAULT_MDNS_NAME "x" //increase if you need more #ifndef WLED_MAX_WIFI_COUNT #define WLED_MAX_WIFI_COUNT 3 #endif #ifndef WLED_MAX_USERMODS #if defined(ESP8266) || defined(CONFIG_IDF_TARGET_ESP32S2) #define WLED_MAX_USERMODS 4 #else #define WLED_MAX_USERMODS 6 #endif #endif #ifdef ESP8266 #define WLED_MAX_DIGITAL_CHANNELS 3 #define WLED_MAX_RMT_CHANNELS 0 // ESP8266 does not have RMT nor I2S #define WLED_MAX_I2S_CHANNELS 0 #define WLED_MAX_ANALOG_CHANNELS 5 #define WLED_PLATFORM_ID 0 // used in UI to distinguish ESP types, needs a proper fix! #else #if !defined(LEDC_CHANNEL_MAX) || !defined(LEDC_SPEED_MODE_MAX) #include "driver/ledc.h" // needed for analog/LEDC channel counts #endif #define WLED_MAX_ANALOG_CHANNELS (LEDC_CHANNEL_MAX*LEDC_SPEED_MODE_MAX) #if defined(CONFIG_IDF_TARGET_ESP32C3) // 2 RMT, 6 LEDC, only has 1 I2S but NPB does not support it ATM #define WLED_MAX_RMT_CHANNELS 2 // ESP32-C3 has 2 RMT output channels #define WLED_MAX_I2S_CHANNELS 0 // I2S not supported by NPB //#define WLED_MAX_ANALOG_CHANNELS 6 #define WLED_PLATFORM_ID 1 // used in UI to distinguish ESP types, needs a proper fix! #elif defined(CONFIG_IDF_TARGET_ESP32S2) // 4 RMT, 8 LEDC, only has 1 I2S bus, supported in NPB #define WLED_MAX_RMT_CHANNELS 4 // ESP32-S2 has 4 RMT output channels #define WLED_MAX_I2S_CHANNELS 8 // I2S parallel output supported by NPB //#define WLED_MAX_ANALOG_CHANNELS 8 #define WLED_PLATFORM_ID 2 // used in UI to distinguish ESP type in UI #elif defined(CONFIG_IDF_TARGET_ESP32S3) // 4 RMT, 8 LEDC, has 2 I2S but NPB supports parallel x8 LCD on I2S1 #define WLED_MAX_RMT_CHANNELS 4 // ESP32-S3 has 4 RMT output channels #define WLED_MAX_I2S_CHANNELS 8 // uses LCD parallel output not I2S //#define WLED_MAX_ANALOG_CHANNELS 8 #define WLED_PLATFORM_ID 3 // used in UI to distinguish ESP type in UI, needs a proper fix! #else #define WLED_MAX_RMT_CHANNELS 8 // ESP32 has 8 RMT output channels #define WLED_MAX_I2S_CHANNELS 8 // I2S parallel output supported by NPB //#define WLED_MAX_ANALOG_CHANNELS 16 #define WLED_PLATFORM_ID 4 // used in UI to distinguish ESP type in UI, needs a proper fix! #endif #define WLED_MAX_DIGITAL_CHANNELS (WLED_MAX_RMT_CHANNELS + WLED_MAX_I2S_CHANNELS) #endif // WLED_MAX_BUSSES was used to define the size of busses[] array which is no longer needed // instead it will help determine max number of buses that can be defined at compile time #ifdef WLED_MAX_BUSSES #undef WLED_MAX_BUSSES #endif #define WLED_MAX_BUSSES (WLED_MAX_DIGITAL_CHANNELS+WLED_MAX_ANALOG_CHANNELS) static_assert(WLED_MAX_BUSSES <= 32, "WLED_MAX_BUSSES exceeds hard limit"); // Maximum number of pins per output. 5 for RGBCCT analog LEDs. #define OUTPUT_MAX_PINS 5 // for pin manager #ifdef ESP8266 #define WLED_NUM_PINS (GPIO_PIN_COUNT+1) // somehow they forgot GPIO 16 (0-16==17) #else #define WLED_NUM_PINS (GPIO_PIN_COUNT) #endif #ifndef WLED_MAX_BUTTONS #ifdef ESP8266 #define WLED_MAX_BUTTONS 10 #else #define WLED_MAX_BUTTONS 32 #endif #else #if WLED_MAX_BUTTONS < 2 #undef WLED_MAX_BUTTONS #define WLED_MAX_BUTTONS 2 #endif #endif #define RELAY_DELAY 50 // delay in ms between switching on relay and sending data to LEDs #if defined(ESP8266) || defined(CONFIG_IDF_TARGET_ESP32S2) #define WLED_MAX_COLOR_ORDER_MAPPINGS 5 #else #define WLED_MAX_COLOR_ORDER_MAPPINGS 10 #endif #if defined(WLED_MAX_LEDMAPS) && (WLED_MAX_LEDMAPS > 32 || WLED_MAX_LEDMAPS < 10) #undef WLED_MAX_LEDMAPS #endif #ifndef WLED_MAX_LEDMAPS #if defined(ESP8266) || defined(CONFIG_IDF_TARGET_ESP32S2) #define WLED_MAX_LEDMAPS 10 #else #define WLED_MAX_LEDMAPS 16 #endif #endif #ifndef WLED_MAX_SEGNAME_LEN #ifdef ESP8266 #define WLED_MAX_SEGNAME_LEN 32 #else #define WLED_MAX_SEGNAME_LEN 64 #endif #else #if WLED_MAX_SEGNAME_LEN<32 #undef WLED_MAX_SEGNAME_LEN #define WLED_MAX_SEGNAME_LEN 32 #else #warning WLED UI does not support modified maximum segment name length! #endif #endif #define WLED_MAX_PANELS 18 // must not be more than 32 //Usermod IDs #define USERMOD_ID_RESERVED 0 //Unused. Might indicate no usermod present #define USERMOD_ID_UNSPECIFIED 1 //Default value for a general user mod that does not specify a custom ID #define USERMOD_ID_EXAMPLE 2 //Usermod "usermod_v2_example.h" #define USERMOD_ID_TEMPERATURE 3 //Usermod "usermod_temperature.h" #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" #define USERMOD_ID_DHT 10 //Usermod "usermod_dht.h" #define USERMOD_ID_MODE_SORT 11 //Usermod "usermod_v2_mode_sort.h" #define USERMOD_ID_VL53L0X 12 //Usermod "usermod_vl53l0x_gestures.h" #define USERMOD_ID_MULTI_RELAY 13 //Usermod "usermod_multi_relay.h" #define USERMOD_ID_ANIMATED_STAIRCASE 14 //Usermod "Animated_Staircase.h" #define USERMOD_ID_RTC 15 //Usermod "usermod_rtc.h" #define USERMOD_ID_ELEKSTUBE_IPS 16 //Usermod "usermod_elekstube_ips.h" #define USERMOD_ID_SN_PHOTORESISTOR 17 //Usermod "usermod_sn_photoresistor.h" #define USERMOD_ID_BATTERY 18 //Usermod "usermod_v2_battery.h" #define USERMOD_ID_PWM_FAN 19 //Usermod "usermod_PWM_fan.h" #define USERMOD_ID_BH1750 20 //Usermod "usermod_bh1750.h" #define USERMOD_ID_SEVEN_SEGMENT_DISPLAY 21 //Usermod "usermod_v2_seven_segment_display.h" #define USERMOD_RGB_ROTARY_ENCODER 22 //Usermod "rgb-rotary-encoder.h" #define USERMOD_ID_QUINLED_AN_PENTA 23 //Usermod "quinled-an-penta.h" #define USERMOD_ID_SSDR 24 //Usermod "usermod_v2_seven_segment_display_reloaded.h" #define USERMOD_ID_CRONIXIE 25 //Usermod "usermod_cronixie.h" #define USERMOD_ID_WIZLIGHTS 26 //Usermod "wizlights.h" #define USERMOD_ID_WORDCLOCK 27 //Usermod "usermod_v2_word_clock.h" #define USERMOD_ID_MY9291 28 //Usermod "usermod_MY9291.h" #define USERMOD_ID_SI7021_MQTT_HA 29 //Usermod "usermod_si7021_mqtt_ha.h" #define USERMOD_ID_BME280 30 //Usermod "usermod_bme280.h #define USERMOD_ID_SMARTNEST 31 //Usermod "usermod_smartnest.h" #define USERMOD_ID_AUDIOREACTIVE 32 //Usermod "audioreactive.h" #define USERMOD_ID_ANALOG_CLOCK 33 //Usermod "Analog_Clock.h" #define USERMOD_ID_PING_PONG_CLOCK 34 //Usermod "usermod_v2_ping_pong_clock.h" #define USERMOD_ID_ADS1115 35 //Usermod "usermod_ads1115.h" #define USERMOD_ID_BOBLIGHT 36 //Usermod "boblight.h" #define USERMOD_ID_SD_CARD 37 //Usermod "usermod_sd_card.h" #define USERMOD_ID_PWM_OUTPUTS 38 //Usermod "usermod_pwm_outputs.h #define USERMOD_ID_SHT 39 //Usermod "usermod_sht.h #define USERMOD_ID_KLIPPER 40 //Usermod Klipper percentage #define USERMOD_ID_WIREGUARD 41 //Usermod "wireguard.h" #define USERMOD_ID_INTERNAL_TEMPERATURE 42 //Usermod "usermod_internal_temperature.h" #define USERMOD_ID_LDR_DUSK_DAWN 43 //Usermod "usermod_LDR_Dusk_Dawn_v2.h" #define USERMOD_ID_STAIRWAY_WIPE 44 //Usermod "stairway-wipe-usermod-v2.h" #define USERMOD_ID_ANIMARTRIX 45 //Usermod "usermod_v2_animartrix.h" #define USERMOD_ID_HTTP_PULL_LIGHT_CONTROL 46 //usermod "usermod_v2_HttpPullLightControl.h" #define USERMOD_ID_TETRISAI 47 //Usermod "usermod_v2_tetris.h" #define USERMOD_ID_MAX17048 48 //Usermod "usermod_max17048.h" #define USERMOD_ID_BME68X 49 //Usermod "usermod_bme68x.h #define USERMOD_ID_INA226 50 //Usermod "usermod_ina226.h" #define USERMOD_ID_AHT10 51 //Usermod "usermod_aht10.h" #define USERMOD_ID_LD2410 52 //Usermod "usermod_ld2410.h" #define USERMOD_ID_POV_DISPLAY 53 //Usermod "usermod_pov_display.h" #define USERMOD_ID_PIXELS_DICE_TRAY 54 //Usermod "pixels_dice_tray.h" #define USERMOD_ID_DEEP_SLEEP 55 //Usermod "usermod_deep_sleep.h" #define USERMOD_ID_RF433 56 //Usermod "usermod_v2_RF433.h" #define USERMOD_ID_BRIGHTNESS_FOLLOW_SUN 57 //Usermod "usermod_v2_brightness_follow_sun.h" #define USERMOD_ID_USER_FX 58 //Usermod "user_fx" //Wifi encryption type #ifdef WLED_ENABLE_WPA_ENTERPRISE #define WIFI_ENCRYPTION_TYPE_PSK 0 //None/WPA/WPA2 #define WIFI_ENCRYPTION_TYPE_ENTERPRISE 1 //WPA/WPA2-Enterprise #endif //Access point behavior #define AP_BEHAVIOR_BOOT_NO_CONN 0 //Open AP when no connection after boot #define AP_BEHAVIOR_NO_CONN 1 //Open when no connection (either after boot or if connection is lost) #define AP_BEHAVIOR_ALWAYS 2 //Always open #define AP_BEHAVIOR_BUTTON_ONLY 3 //Only when button pressed for 6 sec #define AP_BEHAVIOR_TEMPORARY 4 //Open AP when no connection after boot but only temporary #ifndef WLED_AP_TIMEOUT #define WLED_AP_TIMEOUT 300000 //Temporary AP timeout #endif //Notifier callMode #define CALL_MODE_INIT 0 //no updates on init, can be used to disable updates #define CALL_MODE_DIRECT_CHANGE 1 #define CALL_MODE_BUTTON 2 //default button actions applied to selected segments #define CALL_MODE_NOTIFICATION 3 //caused by incoming notification (UDP or DMX preset) #define CALL_MODE_NIGHTLIGHT 4 //nightlight progress #define CALL_MODE_NO_NOTIFY 5 //change state but do not send notifications (UDP) #define CALL_MODE_FX_CHANGED 6 //no longer used #define CALL_MODE_HUE 7 #define CALL_MODE_PRESET_CYCLE 8 //no longer used #define CALL_MODE_BLYNK 9 //no longer used #define CALL_MODE_ALEXA 10 #define CALL_MODE_WS_SEND 11 //special call mode, not for notifier, updates websocket only #define CALL_MODE_BUTTON_PRESET 12 //button/IR JSON preset/macro //RGB to RGBW conversion mode #define RGBW_MODE_MANUAL_ONLY 0 // No automatic white channel calculation. Manual white channel slider #define RGBW_MODE_AUTO_BRIGHTER 1 // New algorithm. Adds as much white as the darkest RGBW channel #define RGBW_MODE_AUTO_ACCURATE 2 // New algorithm. Adds as much white as the darkest RGBW channel and subtracts this amount from each RGB channel #define RGBW_MODE_DUAL 3 // Manual slider + auto calculation. Automatically calculates only if manual slider is set to off (0) #define RGBW_MODE_MAX 4 // Sets white to the value of the brightest RGB channel (good for white-only LEDs without any RGB) //#define RGBW_MODE_LEGACY 4 // Old floating algorithm. Too slow for realtime and palette support (unused) #define AW_GLOBAL_DISABLED 255 // Global auto white mode override disabled. Per-bus setting is used //realtime modes #define REALTIME_MODE_INACTIVE 0 #define REALTIME_MODE_GENERIC 1 #define REALTIME_MODE_UDP 2 #define REALTIME_MODE_HYPERION 3 #define REALTIME_MODE_E131 4 #define REALTIME_MODE_ADALIGHT 5 #define REALTIME_MODE_ARTNET 6 #define REALTIME_MODE_TPM2NET 7 #define REALTIME_MODE_DDP 8 #define REALTIME_MODE_DMX 9 //realtime override modes #define REALTIME_OVERRIDE_NONE 0 #define REALTIME_OVERRIDE_ONCE 1 #define REALTIME_OVERRIDE_ALWAYS 2 //E1.31 DMX modes #define DMX_MODE_DISABLED 0 //not used #define DMX_MODE_SINGLE_RGB 1 //all LEDs same RGB color (3 channels) #define DMX_MODE_SINGLE_DRGB 2 //all LEDs same RGB color and master dimmer (4 channels) #define DMX_MODE_EFFECT 3 //trigger standalone effects of WLED (15 channels) #define DMX_MODE_EFFECT_W 7 //trigger standalone effects of WLED (18 channels) #define DMX_MODE_MULTIPLE_RGB 4 //every LED is addressed with its own RGB (ledCount * 3 channels) #define DMX_MODE_MULTIPLE_DRGB 5 //every LED is addressed with its own RGB and share a master dimmer (ledCount * 3 + 1 channels) #define DMX_MODE_MULTIPLE_RGBW 6 //every LED is addressed with its own RGBW (ledCount * 4 channels) #define DMX_MODE_EFFECT_SEGMENT 8 //trigger standalone effects of WLED (15 channels per segment) #define DMX_MODE_EFFECT_SEGMENT_W 9 //trigger standalone effects of WLED (18 channels per segment) #define DMX_MODE_PRESET 10 //apply presets (1 channel) //Light capability byte (unused) 0bRCCCTTTT //bits 0/1/2/3: specifies a type of LED driver. A single "driver" may have different chip models but must have the same protocol/behavior //bits 4/5/6: specifies the class of LED driver - 0b000 (dec. 0-15) unconfigured/reserved // - 0b001 (dec. 16-31) digital (data pin only) // - 0b010 (dec. 32-47) analog (PWM) // - 0b011 (dec. 48-63) digital (data + clock / SPI) // - 0b100 (dec. 64-79) unused/reserved // - 0b101 (dec. 80-95) virtual network busses // - 0b110 (dec. 96-111) unused/reserved // - 0b111 (dec. 112-127) unused/reserved //bit 7 is reserved and set to 0 #define TYPE_NONE 0 //light is not configured #define TYPE_RESERVED 1 //unused. Might indicate a "virtual" light //Digital types (data pin only) (16-39) #define TYPE_DIGITAL_MIN 16 // first usable digital type #define TYPE_WS2812_1CH 18 //white-only chips (1 channel per IC) (unused) #define TYPE_WS2812_1CH_X3 19 //white-only chips (3 channels per IC) #define TYPE_WS2812_2CH_X3 20 //CCT chips (1st IC controls WW + CW of 1st zone and CW of 2nd zone, 2nd IC controls WW of 2nd zone and WW + CW of 3rd zone) #define TYPE_WS2812_WWA 21 //amber + warm + cold white #define TYPE_WS2812_RGB 22 #define TYPE_GS8608 23 //same driver as WS2812, but will require signal 2x per second (else displays test pattern) #define TYPE_WS2811_400KHZ 24 //half-speed WS2812 protocol, used by very old WS2811 units #define TYPE_TM1829 25 #define TYPE_UCS8903 26 #define TYPE_APA106 27 #define TYPE_FW1906 28 //RGB + CW + WW + unused channel (6 channels per IC) #define TYPE_UCS8904 29 //first RGBW digital type (hardcoded in busmanager.cpp) #define TYPE_SK6812_RGBW 30 #define TYPE_TM1814 31 #define TYPE_WS2805 32 //RGB + WW + CW #define TYPE_TM1914 33 //RGB #define TYPE_SM16825 34 //RGB + WW + CW #define TYPE_DIGITAL_MAX 39 // last usable digital type //"Analog" types (40-47) #define TYPE_ONOFF 40 //binary output (relays etc.; NOT PWM) #define TYPE_ANALOG_MIN 41 // first usable analog type #define TYPE_ANALOG_1CH 41 //single channel PWM. Uses value of brightest RGBW channel #define TYPE_ANALOG_2CH 42 //analog WW + CW #define TYPE_ANALOG_3CH 43 //analog RGB #define TYPE_ANALOG_4CH 44 //analog RGBW #define TYPE_ANALOG_5CH 45 //analog RGB + WW + CW #define TYPE_ANALOG_6CH 46 //analog RGB + A + WW + CW #define TYPE_ANALOG_MAX 47 // last usable analog type //Digital types (data + clock / SPI) (48-63) #define TYPE_2PIN_MIN 48 #define TYPE_WS2801 50 #define TYPE_APA102 51 #define TYPE_LPD8806 52 #define TYPE_P9813 53 #define TYPE_LPD6803 54 #define TYPE_2PIN_MAX 63 #define TYPE_HUB75MATRIX_MIN 64 #define TYPE_HUB75MATRIX_HS 65 #define TYPE_HUB75MATRIX_QS 66 #define TYPE_HUB75MATRIX_MAX 71 //Network types (master broadcast) (80-95) #define TYPE_VIRTUAL_MIN 80 #define TYPE_NET_DDP_RGB 80 //network DDP RGB bus (master broadcast bus) #define TYPE_NET_E131_RGB 81 //network E131 RGB bus (master broadcast bus, unused) #define TYPE_NET_ARTNET_RGB 82 //network ArtNet RGB bus (master broadcast bus, unused) #define TYPE_NET_DDP_RGBW 88 //network DDP RGBW bus (master broadcast bus) #define TYPE_NET_ARTNET_RGBW 89 //network ArtNet RGB bus (master broadcast bus, unused) #define TYPE_VIRTUAL_MAX 95 //Color orders #define COL_ORDER_GRB 0 //GRB(w),defaut #define COL_ORDER_RGB 1 //common for WS2811 #define COL_ORDER_BRG 2 #define COL_ORDER_RBG 3 #define COL_ORDER_BGR 4 #define COL_ORDER_GBR 5 #define COL_ORDER_MAX 5 //ESP-NOW #define ESP_NOW_STATE_UNINIT 0 #define ESP_NOW_STATE_ON 1 #define ESP_NOW_STATE_ERROR 2 //Button type #define BTN_TYPE_NONE 0 #define BTN_TYPE_RESERVED 1 #define BTN_TYPE_PUSH 2 #define BTN_TYPE_PUSH_ACT_HIGH 3 #define BTN_TYPE_SWITCH 4 #define BTN_TYPE_PIR_SENSOR 5 #define BTN_TYPE_TOUCH 6 #define BTN_TYPE_ANALOG 7 #define BTN_TYPE_ANALOG_INVERTED 8 #define BTN_TYPE_TOUCH_SWITCH 9 //Ethernet board types #define WLED_NUM_ETH_TYPES 14 #define WLED_ETH_NONE 0 #define WLED_ETH_WT32_ETH01 1 #define WLED_ETH_ESP32_POE 2 #define WLED_ETH_WESP32 3 #define WLED_ETH_QUINLED 4 #define WLED_ETH_TWILIGHTLORD 5 #define WLED_ETH_ESP32DEUX 6 #define WLED_ETH_ESP32ETHKITVE 7 #define WLED_ETH_QUINLED_OCTA 8 #define WLED_ETH_ABCWLEDV43ETH 9 #define WLED_ETH_SERG74 10 #define WLED_ETH_ESP32_POE_WROVER 11 #define WLED_ETH_LILYGO_T_POE_PRO 12 #define WLED_ETH_GLEDOPTO 13 //Hue error codes #define HUE_ERROR_INACTIVE 0 #define HUE_ERROR_UNAUTHORIZED 1 #define HUE_ERROR_LIGHTID 3 #define HUE_ERROR_PUSHLINK 101 #define HUE_ERROR_JSON_PARSING 250 #define HUE_ERROR_TIMEOUT 251 #define HUE_ERROR_ACTIVE 255 //Segment option byte bits #define SEG_OPTION_SELECTED 0 #define SEG_OPTION_REVERSED 1 #define SEG_OPTION_ON 2 #define SEG_OPTION_MIRROR 3 //Indicates that the effect will be mirrored within the segment #define SEG_OPTION_FREEZE 4 //Segment contents will not be refreshed #define SEG_OPTION_RESET 5 //Segment runtime requires reset #define SEG_OPTION_REVERSED_Y 6 #define SEG_OPTION_MIRROR_Y 7 #define SEG_OPTION_TRANSPOSED 8 //Segment differs return byte #define SEG_DIFFERS_BRI 0x01 // opacity #define SEG_DIFFERS_OPT 0x02 // all segment options except: selected, reset & transitional #define SEG_DIFFERS_COL 0x04 // colors #define SEG_DIFFERS_FX 0x08 // effect/mode parameters #define SEG_DIFFERS_BOUNDS 0x10 // segment start/stop bounds #define SEG_DIFFERS_GSO 0x20 // grouping, spacing & offset #define SEG_DIFFERS_SEL 0x80 // selected //Playlist option byte #define PL_OPTION_SHUFFLE 0x01 #define PL_OPTION_RESTORE 0x02 // Segment capability byte #define SEG_CAPABILITY_RGB 0x01 #define SEG_CAPABILITY_W 0x02 #define SEG_CAPABILITY_CCT 0x04 // WLED Error modes #define ERR_NONE 0 // All good :) #define ERR_DENIED 1 // Permission denied #define ERR_CONCURRENCY 2 // Conurrency (client active) #define ERR_NOBUF 3 // JSON buffer was not released in time, request cannot be handled at this time #define ERR_NOT_IMPL 4 // Not implemented #define ERR_NORAM_PX 7 // not enough RAM for pixels #define ERR_NORAM 8 // effect RAM depleted #define ERR_JSON 9 // JSON parsing failed (input too large?) #define ERR_FS_BEGIN 10 // Could not init filesystem (no partition?) #define ERR_FS_QUOTA 11 // The FS is full or the maximum file size is reached #define ERR_FS_PLOAD 12 // It was attempted to load a preset that does not exist #define ERR_FS_IRLOAD 13 // It was attempted to load an IR JSON cmd, but the "ir.json" file does not exist #define ERR_FS_RMLOAD 14 // It was attempted to load an remote JSON cmd, but the "remote.json" file does not exist #define ERR_FS_GENERAL 19 // A general unspecified filesystem error occurred #define ERR_OVERTEMP 30 // An attached temperature sensor has measured above threshold temperature (not implemented) #define ERR_OVERCURRENT 31 // An attached current sensor has measured a current above the threshold (not implemented) #define ERR_UNDERVOLT 32 // An attached voltmeter has measured a voltage below the threshold (not implemented) // JSON buffer lock owners #define JSON_LOCK_UNKNOWN 255 #define JSON_LOCK_CFG_DES 1 #define JSON_LOCK_CFG_SER 2 #define JSON_LOCK_CFG_SEC_DES 3 #define JSON_LOCK_CFG_SEC_SER 4 #define JSON_LOCK_SETTINGS 5 #define JSON_LOCK_XML 6 #define JSON_LOCK_LEDMAP 7 // unused 8 #define JSON_LOCK_PRESET_LOAD 9 #define JSON_LOCK_PRESET_SAVE 10 #define JSON_LOCK_WS_RECEIVE 11 #define JSON_LOCK_WS_SEND 12 #define JSON_LOCK_IR 13 #define JSON_LOCK_SERVER 14 #define JSON_LOCK_MQTT 15 #define JSON_LOCK_SERIAL 16 #define JSON_LOCK_SERVEJSON 17 #define JSON_LOCK_NOTIFY 18 #define JSON_LOCK_PRESET_NAME 19 #define JSON_LOCK_LEDGAP 20 #define JSON_LOCK_LEDMAP_ENUM 21 #define JSON_LOCK_REMOTE 22 // Timer mode types #define NL_MODE_SET 0 //After nightlight time elapsed, set to target brightness #define NL_MODE_FADE 1 //Fade to target brightness gradually #define NL_MODE_COLORFADE 2 //Fade to target brightness and secondary color gradually #define NL_MODE_SUN 3 //Sunrise/sunset. Target brightness is set immediately, then Sunrise effect is started. Max 60 min. // Settings sub page IDs #define SUBPAGE_MENU 0 #define SUBPAGE_WIFI 1 #define SUBPAGE_LEDS 2 #define SUBPAGE_UI 3 #define SUBPAGE_SYNC 4 #define SUBPAGE_TIME 5 #define SUBPAGE_SEC 6 #define SUBPAGE_DMX 7 #define SUBPAGE_UM 8 #define SUBPAGE_UPDATE 9 #define SUBPAGE_2D 10 #define SUBPAGE_PINS 11 #define SUBPAGE_LAST SUBPAGE_PINS #define SUBPAGE_LOCK 251 #define SUBPAGE_PINREQ 252 #define SUBPAGE_CSS 253 #define SUBPAGE_JS 254 #define SUBPAGE_WELCOME 255 #define NTP_PACKET_SIZE 48 // size of NTP receive buffer #define NTP_MIN_PACKET_SIZE 48 // min expected size - NTP v4 allows for "extended information" appended to the standard fields //maximum number of rendered LEDs - this does not have to match max. physical LEDs, e.g. if there are virtual busses #ifndef MAX_LEDS #ifdef ESP8266 #define MAX_LEDS 1536 //can't rely on memory limit to limit this to 1536 LEDs #elif defined(CONFIG_IDF_TARGET_ESP32S2) #define MAX_LEDS 2048 //due to memory constraints S2 #else #define MAX_LEDS 16384 #endif #endif // maximum total memory that can be used for bus-buffers and pixel buffers #ifndef MAX_LED_MEMORY #ifdef ESP8266 #define MAX_LED_MEMORY (8*1024) #else #if defined(CONFIG_IDF_TARGET_ESP32S2) #ifndef BOARD_HAS_PSRAM #define MAX_LED_MEMORY (28*1024) // S2 has ~170k of free heap after boot, using 28k is the absolute limit to keep WLED functional #else #define MAX_LED_MEMORY (48*1024) // with PSRAM there is more wiggle room as buffers get moved to PSRAM when needed (prioritize functionality over speed) #endif #elif defined(CONFIG_IDF_TARGET_ESP32S3) #define MAX_LED_MEMORY (192*1024) // S3 has ~330k of free heap after boot #elif defined(CONFIG_IDF_TARGET_ESP32C3) #define MAX_LED_MEMORY (100*1024) // C3 has ~240k of free heap after boot, even with 8000 LEDs configured (2D) there is 30k of contiguous heap left #else #define MAX_LED_MEMORY (85*1024) // ESP32 has ~160k of free heap after boot and an additional 64k of 32bit access memory that is used for pixel buffers #endif #endif #endif #ifndef MAX_LEDS_PER_BUS #define MAX_LEDS_PER_BUS 2048 // may not be enough for fast LEDs (i.e. APA102) #endif // string temp buffer (now stored in stack locally) #ifdef ESP8266 #define SETTINGS_STACK_BUF_SIZE 2560 #else #define SETTINGS_STACK_BUF_SIZE 3840 // warning: quite a large value for stack (640 * WLED_MAX_USERMODS) #endif #ifdef WLED_USE_ETHERNET #define E131_MAX_UNIVERSE_COUNT 20 #else #ifdef ESP8266 #define E131_MAX_UNIVERSE_COUNT 9 #else #define E131_MAX_UNIVERSE_COUNT 12 #endif #endif #ifndef ABL_MILLIAMPS_DEFAULT #define ABL_MILLIAMPS_DEFAULT 850 // auto lower brightness to stay close to milliampere limit #else #if ABL_MILLIAMPS_DEFAULT == 0 // disable ABL #elif ABL_MILLIAMPS_DEFAULT < 250 // make sure value is at least 250 #warning "make sure value is at least 250" #define ABL_MILLIAMPS_DEFAULT 250 #endif #endif #ifndef LED_MILLIAMPS_DEFAULT #define LED_MILLIAMPS_DEFAULT 55 // common WS2812B #else #if LED_MILLIAMPS_DEFAULT < 1 || LED_MILLIAMPS_DEFAULT > 100 #warning "Unusual LED mA current, overriding with default value." #undef LED_MILLIAMPS_DEFAULT #define LED_MILLIAMPS_DEFAULT 55 #endif #endif // PWM settings #ifndef WLED_PWM_FREQ #ifdef ESP8266 #define WLED_PWM_FREQ 880 //PWM frequency proven as good for LEDs #else #ifdef SOC_LEDC_SUPPORT_XTAL_CLOCK #define WLED_PWM_FREQ 9765 // XTAL clock is 40MHz (this will allow 12 bit resolution) #else #define WLED_PWM_FREQ 19531 // APB clock is 80MHz #endif #endif #endif #define TOUCH_THRESHOLD 32 // limit to recognize a touch, higher value means more sensitive // Size of buffer for API JSON object (increase for more segments) #ifdef ESP8266 #define JSON_BUFFER_SIZE 10240 #else #if defined(CONFIG_IDF_TARGET_ESP32S2) #define JSON_BUFFER_SIZE 24576 #else #define JSON_BUFFER_SIZE 32767 #endif #endif // minimum heap size required to process web requests: try to keep free heap above this value #ifdef ESP8266 #define MIN_HEAP_SIZE (9*1024) #else #define MIN_HEAP_SIZE (15*1024) // WLED allocation functions (util.cpp) try to keep this much contiguous heap free for other tasks #endif // threshold for PSRAM use: if heap is running low, requests to allocate_buffer(prefer DRAM) above PSRAM_THRESHOLD may be put in PSRAM // if heap is depleted, PSRAM will be used regardless of threshold #if defined(CONFIG_IDF_TARGET_ESP32S3) #define PSRAM_THRESHOLD (12*1024) // S3 has plenty of DRAM #elif defined(CONFIG_IDF_TARGET_ESP32) #define PSRAM_THRESHOLD (5*1024) #else #define PSRAM_THRESHOLD (2*1024) // S2 does not have a lot of RAM. C3 and ESP8266 do not support PSRAM: the value is not used #endif // Web server limits #ifdef ESP8266 // Minimum heap to consider handling a request #define WLED_REQUEST_MIN_HEAP (8*1024) // Estimated maximum heap required by any one request #define WLED_REQUEST_HEAP_USAGE (6*1024) #else // ESP32 TCP stack needs much more RAM than ESP8266 // Minimum heap remaining before queuing a request #define WLED_REQUEST_MIN_HEAP (12*1024) // Estimated maximum heap required by any one request #define WLED_REQUEST_HEAP_USAGE (12*1024) #endif // Maximum number of requests in queue; absolute cap on web server resource usage. // Websockets do not count against this limit. #define WLED_REQUEST_MAX_QUEUE 6 // Maximum size of node map (list of other WLED instances) #ifdef ESP8266 #define WLED_MAX_NODES 24 #else #define WLED_MAX_NODES 150 #endif // Defaults pins, type and counts to configure LED output #if defined(ESP8266) || defined(CONFIG_IDF_TARGET_ESP32C3) #ifdef WLED_ENABLE_DMX #define DEFAULT_LED_PIN 1 #warning "Compiling with DMX. The default LED pin has been changed to pin 1." #else #define DEFAULT_LED_PIN 2 // GPIO2 (D4) on Wemos D1 mini compatible boards, safe to use on any board #endif #else #if defined(WLED_USE_ETHERNET) #define DEFAULT_LED_PIN 4 // GPIO4 seems to be a "safe bet" for all known ethernet boards (issue #5155) //#warning "Compiling with Ethernet support. The default LED pin has been changed to pin 4." #else #define DEFAULT_LED_PIN 16 // aligns with GPIO2 (D4) on Wemos D1 mini32 compatible boards (if it is unusable it will be reassigned in WS2812FX::finalizeInit()) #endif #endif #define DEFAULT_LED_TYPE TYPE_WS2812_RGB #define DEFAULT_LED_COUNT 30 #define INTERFACE_UPDATE_COOLDOWN 1000 // time in ms to wait between websockets, alexa, and MQTT updates #define PIN_RETRY_COOLDOWN 3000 // time in ms after an incorrect attempt PIN and OTA pass will be rejected even if correct #define PIN_TIMEOUT 900000 // time in ms after which the PIN will be required again, 15 minutes // HW_PIN_SCL & HW_PIN_SDA are used for information in usermods settings page and usermods themselves // which GPIO pins are actually used in a hardware layout (controller board) #if defined(I2CSCLPIN) && !defined(HW_PIN_SCL) #define HW_PIN_SCL I2CSCLPIN #endif #if defined(I2CSDAPIN) && !defined(HW_PIN_SDA) #define HW_PIN_SDA I2CSDAPIN #endif // you cannot change HW I2C pins on 8266 #if defined(ESP8266) && defined(HW_PIN_SCL) #undef HW_PIN_SCL #endif #if defined(ESP8266) && defined(HW_PIN_SDA) #undef HW_PIN_SDA #endif // defaults for 1st I2C on ESP32 (Wire global) #ifndef HW_PIN_SCL #define HW_PIN_SCL SCL #endif #ifndef HW_PIN_SDA #define HW_PIN_SDA SDA #endif // HW_PIN_SCLKSPI & HW_PIN_MOSISPI & HW_PIN_MISOSPI are used for information in usermods settings page and usermods themselves // which GPIO pins are actually used in a hardware layout (controller board) #if defined(SPISCLKPIN) && !defined(HW_PIN_CLOCKSPI) #define HW_PIN_CLOCKSPI SPISCLKPIN #endif #if defined(SPIMOSIPIN) && !defined(HW_PIN_MOSISPI) #define HW_PIN_MOSISPI SPIMOSIPIN #endif #if defined(SPIMISOPIN) && !defined(HW_PIN_MISOSPI) #define HW_PIN_MISOSPI SPIMISOPIN #endif // you cannot change HW SPI pins on 8266 #if defined(ESP8266) && defined(HW_PIN_CLOCKSPI) #undef HW_PIN_CLOCKSPI #endif #if defined(ESP8266) && defined(HW_PIN_DATASPI) #undef HW_PIN_DATASPI #endif #if defined(ESP8266) && defined(HW_PIN_MISOSPI) #undef HW_PIN_MISOSPI #endif // defaults for VSPI on ESP32 (SPI global, SPI.cpp) as HSPI is used by WLED (bus_wrapper.h) #ifndef HW_PIN_CLOCKSPI #define HW_PIN_CLOCKSPI SCK #endif #ifndef HW_PIN_DATASPI #define HW_PIN_DATASPI MOSI #endif #ifndef HW_PIN_MISOSPI #define HW_PIN_MISOSPI MISO #endif // IRAM_ATTR for 8266 with 32Kb IRAM causes error: section `.text1' will not fit in region `iram1_0_seg' // this hack removes the IRAM flag for some 1D/2D functions - somewhat slower, but it solves problems with some older 8266 chips #ifdef WLED_SAVE_IRAM #define IRAM_ATTR_YN #else #define IRAM_ATTR_YN IRAM_ATTR #endif #define WLED_O2_ATTR __attribute__((optimize("O2"))) #endif ================================================ FILE: wled00/data/404.htm ================================================ Not found

404 Not Found

Akemi does not know where you are headed...

================================================ FILE: wled00/data/common.js ================================================ var d=document; var loc = false, locip, locproto = "http:"; function H(pg="") { window.open("https://kno.wled.ge/"+pg); } function GH() { window.open("https://github.com/wled-dev/WLED"); } function gId(c) { return d.getElementById(c); } // getElementById function cE(e) { return d.createElement(e); } // createElement function gEBCN(c) { return d.getElementsByClassName(c); } // getElementsByClassName function gN(s) { return d.getElementsByName(s)[0]; } // getElementsByName function isE(o) { return Object.keys(o).length === 0; } // isEmpty function isO(i) { return (i && typeof i === 'object' && !Array.isArray(i)); } // isObject function isN(n) { return !isNaN(parseFloat(n)) && isFinite(n); } // isNumber // https://stackoverflow.com/questions/3885817/how-do-i-check-that-a-number-is-float-or-integer function isF(n) { return n === +n && n !== (n|0); } // isFloat function isI(n) { return n === +n && n === (n|0); } // isInteger function toggle(el) { gId(el).classList.toggle("hide"); let n = gId('No'+el); if (n) n.classList.toggle("hide"); } function tooltip(cont=null) { d.querySelectorAll((cont?cont+" ":"")+"[title]").forEach((element)=>{ element.addEventListener("pointerover", ()=>{ // save title element.setAttribute("data-title", element.getAttribute("title")); const tooltip = d.createElement("span"); tooltip.className = "tooltip"; tooltip.textContent = element.getAttribute("title"); // prevent default title popup element.removeAttribute("title"); let { top, left, width } = element.getBoundingClientRect(); d.body.appendChild(tooltip); const { offsetHeight, offsetWidth } = tooltip; const offset = element.classList.contains("sliderwrap") ? 4 : 10; top -= offsetHeight + offset; left += (width - offsetWidth) / 2; tooltip.style.top = top + "px"; tooltip.style.left = left + "px"; tooltip.classList.add("visible"); }); element.addEventListener("pointerout", ()=>{ d.querySelectorAll('.tooltip').forEach((tooltip)=>{ tooltip.classList.remove("visible"); d.body.removeChild(tooltip); }); // restore title element.setAttribute("title", element.getAttribute("data-title")); }); }); }; // sequential loading of external resources (JS or CSS) with retry, calls init() when done function loadResources(files, init) { let i = 0; const loadNext = () => { if (i >= files.length) { if (init) { d.documentElement.style.visibility = 'visible'; // make page visible after all files are loaded if it was hidden (prevent ugly display) d.readyState === 'complete' ? init() : window.addEventListener('load', init); } return; } const file = files[i++]; const isCSS = file.endsWith('.css'); const el = d.createElement(isCSS ? 'link' : 'script'); if (isCSS) { el.rel = 'stylesheet'; el.href = file; const st = d.head.querySelector('style'); if (st) d.head.insertBefore(el, st); // insert before any ================================================ FILE: wled00/data/dmxmap.htm ================================================ DMX Map
...
================================================ FILE: wled00/data/edit.htm ================================================ WLED File Editor
================================================ FILE: wled00/data/icons-ui/HowTo_AddNewIcons.txt ================================================ To edit the current font, this is the workflow: go to https://icomoon.io/ In the menu, go to manage projects and import the json file from this folder and load it Add new icons or exchange existing ones: if changing existing one, make sure the unicode stays the same (can be edited before exporting) Go to "Generate SVG & More" and check the size of new icons (clicking on icons brings up the editor) -> scale new icons to match the size of existing ones Go to "Generate font" tab, check unicodes are correct (can use any unicode, range > e900 is "custom range" and now preferred) Download the font package and replace the files in this folder with new files Using an online converter, convert the *.woff font into woff2 format (about half the file size) Using another online converter, convert the woff2 font to base64 encoding for CSS in index.css, replace the font string at the top, keep the "data:font/woff2;charset=utf-8;" and dont use octet-stream (browser compatibility). enjoy your new icons in the UI :) ================================================ FILE: wled00/data/icons-ui/Read Me.txt ================================================ Open *demo.html* to see a list of all the glyphs in your font along with their codes/ligatures. To use the generated font in desktop programs, you can install the TTF font. In order to copy the character associated with each icon, refer to the text box at the bottom right corner of each glyph in demo.html. The character inside this text box may be invisible; but it can still be copied. See this guide for more info: https://icomoon.io/docs#install You won't need any of the files located under the *demo-files* directory when including the generated font in your own projects. You can import *selection.json* back to the IcoMoon app using the *Import Icons* button (or via Main Menu → Manage Projects) to retrieve your icon selection. ================================================ FILE: wled00/data/icons-ui/demo-files/demo.css ================================================ body { padding: 0; margin: 0; font-family: sans-serif; font-size: 1em; line-height: 1.5; color: #555; background: #fff; } h1 { font-size: 1.5em; font-weight: normal; } small { font-size: .66666667em; } a { color: #e74c3c; text-decoration: none; } a:hover, a:focus { box-shadow: 0 1px #e74c3c; } .bshadow0, input { box-shadow: inset 0 -2px #e7e7e7; } input:hover { box-shadow: inset 0 -2px #ccc; } input, fieldset { font-family: sans-serif; font-size: 1em; margin: 0; padding: 0; border: 0; } input { color: inherit; line-height: 1.5; height: 1.5em; padding: .25em 0; } input:focus { outline: none; box-shadow: inset 0 -2px #449fdb; } .glyph { font-size: 16px; width: 15em; padding-bottom: 1em; margin-right: 4em; margin-bottom: 1em; float: left; overflow: hidden; } .liga { width: 80%; width: calc(100% - 2.5em); } .talign-right { text-align: right; } .talign-center { text-align: center; } .bgc1 { background: #f1f1f1; } .fgc1 { color: #999; } .fgc0 { color: #000; } p { margin-top: 1em; margin-bottom: 1em; } .mvm { margin-top: .75em; margin-bottom: .75em; } .mtn { margin-top: 0; } .mtl, .mal { margin-top: 1.5em; } .mbl, .mal { margin-bottom: 1.5em; } .mal, .mhl { margin-left: 1.5em; margin-right: 1.5em; } .mhmm { margin-left: 1em; margin-right: 1em; } .mls { margin-left: .25em; } .ptl { padding-top: 1.5em; } .pbs, .pvs { padding-bottom: .25em; } .pvs, .pts { padding-top: .25em; } .unit { float: left; } .unitRight { float: right; } .size1of2 { width: 50%; } .size1of1 { width: 100%; } .clearfix:before, .clearfix:after { content: " "; display: table; } .clearfix:after { clear: both; } .hidden-true { display: none; } .textbox0 { width: 3em; background: #f1f1f1; padding: .25em .5em; line-height: 1.5; height: 1.5em; } #testDrive { display: block; padding-top: 24px; line-height: 1.5; } .fs0 { font-size: 16px; } .fs1 { font-size: 48px; } .fs2 { font-size: 28px; } .fs3 { font-size: 32px; } ================================================ FILE: wled00/data/icons-ui/demo-files/demo.js ================================================ if (!('boxShadow' in document.body.style)) { document.body.setAttribute('class', 'noBoxShadow'); } document.body.addEventListener("click", function(e) { var target = e.target; if (target.tagName === "INPUT" && target.getAttribute('class').indexOf('liga') === -1) { target.select(); } }); (function() { var fontSize = document.getElementById('fontSize'), testDrive = document.getElementById('testDrive'), testText = document.getElementById('testText'); function updateTest() { testDrive.innerHTML = testText.value || String.fromCharCode(160); if (window.icomoonLiga) { window.icomoonLiga(testDrive); } } function updateSize() { testDrive.style.fontSize = fontSize.value + 'px'; } fontSize.addEventListener('change', updateSize, false); testText.addEventListener('input', updateTest, false); testText.addEventListener('change', updateTest, false); updateSize(); }()); ================================================ FILE: wled00/data/icons-ui/demo.html ================================================ IcoMoon Demo

Font Name: wled122 (Glyphs: 25)

Grid Size: 16

i-pixelforge
liga:

Grid Size: 14

i-editor
liga:

Grid Size: Unknown

i-pattern
liga:
i-segments
liga:
i-sun
liga:
i-palette
liga:
i-eye
liga:
i-speed
liga:
i-expand
liga:
i-power
liga:
i-settings
liga:
i-playlist
liga:
i-night
liga:
i-cancel
liga:
i-sync
liga:
i-confirm
liga:
i-brightness
liga:
i-nodes
liga:
i-add
liga:
i-edit
liga:
i-intensity
liga:
i-star
liga:
i-info
liga:
i-del
liga:
i-presets
liga:

Font Test Drive

 

Generated by IcoMoon

================================================ FILE: wled00/data/icons-ui/selection.json ================================================ {"IcoMoonType":"selection","icons":[{"icon":{"paths":["M910.398 765.581l-241.236-241.236c-14.934-14.934-39.371-14.934-54.306 0l-18.102 18.102-147.2-147.2 241.646-241.648h-256.001l-113.645 113.645-11.249-11.247h-54.306v54.306l11.247 11.247-164.848 164.849 127.999 127.999 164.848-164.848 147.2 147.2-18.102 18.102c-14.934 14.934-14.934 39.371 0 54.306l241.236 241.236c14.934 14.934 39.371 14.934 54.306 0l90.509-90.509c14.935-14.934 14.935-39.371 0.002-54.306z"],"attrs":[{}],"isMulticolor":false,"isMulticolor2":false,"tags":["hammer","tool","fix","make","generate","work","build"],"grid":16},"attrs":[{}],"properties":{"order":1,"id":0,"name":"pixelforge","prevSize":48,"code":59648},"setIdx":0,"setId":3,"iconIdx":0},{"icon":{"paths":["M976.272 538.191c0 11.223-7.016 22.448-14.5 30.867l-157.14 185.202c-27.126 31.802-82.311 57.055-123.469 57.055h-508.837c-16.837 0-40.688-5.146-40.688-26.191 0-11.223 7.016-22.448 14.5-30.867l157.14-185.202c27.126-31.802 82.311-57.055 123.469-57.055h508.837c16.837 0 40.688 5.146 40.688 26.191zM815.856 377.307v74.828h-389.112c-58.461 0-130.952 33.208-168.835 78.104l-159.949 188.009c0-3.74-0.467-7.951-0.467-11.691v-448.977c0-57.523 47.233-104.761 104.761-104.761h149.66c57.523 0 104.761 47.233 104.761 104.761v14.968h254.418c57.523 0 104.761 47.233 104.761 104.761z"],"attrs":[{}],"width":1074,"isMulticolor":false,"isMulticolor2":false,"tags":["folder-open"],"grid":14},"attrs":[{}],"properties":{"order":1,"id":1,"prevSize":28,"name":"editor","code":59649},"setIdx":1,"setId":2,"iconIdx":0},{"icon":{"paths":["M511.573 85.333c235.947 0 427.094 191.147 427.094 426.667s-191.147 426.667-427.094 426.667c-235.52 0-426.24-191.147-426.24-426.667s190.72-426.667 426.24-426.667zM512 853.333c188.587 0 341.333-152.746 341.333-341.333s-152.746-341.333-341.333-341.333-341.333 152.746-341.333 341.333 152.746 341.333 341.333 341.333zM661.333 469.333c-35.413 0-64-28.586-64-64s28.587-64 64-64c35.414 0 64 28.587 64 64s-28.586 64-64 64zM362.667 469.333c-35.414 0-64-28.586-64-64s28.586-64 64-64c35.413 0 64 28.587 64 64s-28.587 64-64 64zM512 746.667c-99.413 0-183.893-62.294-218.027-149.334h436.054c-34.134 87.040-118.614 149.334-218.027 149.334z"],"isMulticolor":false,"isMulticolor2":false,"tags":["uniE23D"],"defaultCode":57917,"grid":0,"attrs":[]},"attrs":[],"properties":{"id":11,"order":26,"prevSize":32,"code":57917,"name":"pattern"},"setIdx":2,"setId":1,"iconIdx":0},{"icon":{"paths":["M511.573 791.040l314.88-244.907 69.547 54.187-384 298.667-384-298.667 69.12-53.76zM512 682.667l-384-298.667 384-298.667 384 298.667-69.973 54.187z"],"isMulticolor":false,"isMulticolor2":false,"tags":["uniE34B"],"defaultCode":58187,"grid":0,"attrs":[]},"attrs":[],"properties":{"id":14,"order":35,"ligatures":"","prevSize":32,"code":58187,"name":"segments"},"setIdx":2,"setId":1,"iconIdx":1},{"icon":{"paths":["M288.427 206.507l-60.587 60.16-76.373-76.374 60.16-60.16zM170.667 448v85.333h-128v-85.333h128zM554.667 23.467v125.866h-85.334v-125.866h85.334zM872.533 190.293l-76.373 76.374-60.16-60.16 76.373-76.374zM735.573 774.827l59.734-59.734 76.8 76.374-60.16 60.16zM853.333 448h128v85.333h-128v-85.333zM512 234.667c141.227 0 256 114.773 256 256 0 141.226-114.773 256-256 256s-256-114.774-256-256c0-141.227 114.773-256 256-256zM469.333 957.867v-125.867h85.334v125.867h-85.334zM151.467 791.040l76.373-76.8 60.16 60.16-76.373 76.8z"],"isMulticolor":false,"isMulticolor2":false,"tags":["uniE333"],"defaultCode":58163,"grid":0,"attrs":[]},"attrs":[],"properties":{"id":40,"order":73,"ligatures":"","prevSize":32,"code":58163,"name":"sun"},"setIdx":2,"setId":1,"iconIdx":2},{"icon":{"paths":["M512 128c212.053 0 384 152.747 384 341.333 0 117.76-95.573 213.334-213.333 213.334h-75.52c-35.414 0-64 28.586-64 64 0 16.213 6.4 31.146 16.213 42.24 10.24 11.52 16.64 26.453 16.64 43.093 0 35.413-28.587 64-64 64-212.053 0-384-171.947-384-384s171.947-384 384-384zM277.333 512c35.414 0 64-28.587 64-64s-28.586-64-64-64c-35.413 0-64 28.587-64 64s28.587 64 64 64zM405.333 341.333c35.414 0 64-28.586 64-64 0-35.413-28.586-64-64-64-35.413 0-64 28.587-64 64 0 35.414 28.587 64 64 64zM618.667 341.333c35.413 0 64-28.586 64-64 0-35.413-28.587-64-64-64-35.414 0-64 28.587-64 64 0 35.414 28.586 64 64 64zM746.667 512c35.413 0 64-28.587 64-64s-28.587-64-64-64c-35.414 0-64 28.587-64 64s28.586 64 64 64z"],"isMulticolor":false,"isMulticolor2":false,"tags":["uniE2B3"],"defaultCode":58035,"grid":0,"attrs":[]},"attrs":[],"properties":{"id":75,"order":75,"prevSize":32,"code":58035,"name":"palette"},"setIdx":2,"setId":1,"iconIdx":3},{"icon":{"paths":["M512 192c213.333 0 395.52 132.693 469.333 320-73.813 187.307-256 320-469.333 320s-395.52-132.693-469.333-320c73.813-187.307 256-320 469.333-320zM512 725.333c117.76 0 213.333-95.573 213.333-213.333s-95.573-213.333-213.333-213.333-213.333 95.573-213.333 213.333 95.573 213.333 213.333 213.333zM512 384c70.827 0 128 57.173 128 128s-57.173 128-128 128-128-57.173-128-128 57.173-128 128-128z"],"isMulticolor":false,"isMulticolor2":false,"tags":["uniE0E8"],"defaultCode":57576,"grid":0,"attrs":[]},"attrs":[],"properties":{"id":172,"order":74,"prevSize":32,"code":57576,"name":"eye"},"setIdx":2,"setId":1,"iconIdx":4},{"icon":{"paths":["M640 42.667v85.333h-256v-85.333h256zM469.333 597.333v-256h85.334v256h-85.334zM811.947 315.307c52.48 65.706 84.053 148.906 84.053 239.36 0 212.053-171.52 384-384 384s-384-171.947-384-384c0-212.054 171.947-384 384-384 90.453 0 173.653 31.573 239.787 84.48l60.586-60.587c21.76 17.92 41.814 38.4 60.16 60.16zM512 853.333c165.12 0 298.667-133.546 298.667-298.666s-133.547-298.667-298.667-298.667-298.667 133.547-298.667 298.667 133.547 298.666 298.667 298.666z"],"isMulticolor":false,"isMulticolor2":false,"tags":["uniE325"],"defaultCode":58149,"grid":0,"attrs":[]},"attrs":[],"properties":{"id":370,"order":21,"ligatures":"","prevSize":32,"code":58149,"name":"speed"},"setIdx":2,"setId":1,"iconIdx":5},{"icon":{"paths":["M707.84 366.507l60.16 60.16-256 256-256-256 60.16-60.16 195.84 195.413z"],"isMulticolor":false,"isMulticolor2":false,"tags":["uniE395"],"defaultCode":58261,"grid":0,"attrs":[]},"attrs":[],"properties":{"id":549,"order":69,"ligatures":"","prevSize":32,"code":58261,"name":"expand"},"setIdx":2,"setId":1,"iconIdx":6},{"icon":{"paths":["M554.667 128v426.667h-85.334v-426.667h85.334zM760.747 220.587c82.773 70.4 135.253 174.506 135.253 291.413 0 212.053-171.947 384-384 384s-384-171.947-384-384c0-116.907 52.48-221.013 135.253-291.413l60.16 60.16c-66.986 54.613-110.080 137.813-110.080 231.253 0 165.12 133.547 298.667 298.667 298.667s298.667-133.547 298.667-298.667c0-93.44-43.094-176.64-110.507-230.827z"],"isMulticolor":false,"isMulticolor2":false,"tags":["uniE08F"],"defaultCode":57487,"grid":0,"attrs":[]},"attrs":[],"properties":{"id":557,"order":19,"ligatures":"","prevSize":32,"code":57487,"name":"power"},"setIdx":2,"setId":1,"iconIdx":7},{"icon":{"paths":["M816.64 551.936l85.504 67.584c8.192 6.144 10.24 16.896 5.12 26.112l-81.92 141.824c-5.12 9.216-15.872 12.8-25.088 9.216l-101.888-40.96c-20.992 15.872-44.032 29.696-69.12 39.936l-15.36 108.544c-1.024 10.24-9.728 17.408-19.968 17.408h-163.84c-10.24 0-18.432-7.168-20.48-17.408l-15.36-108.544c-25.088-10.24-47.616-23.552-69.12-39.936l-101.888 40.96c-9.216 3.072-19.968 0-25.088-9.216l-81.92-141.824c-4.608-8.704-2.56-19.968 5.12-26.112l86.528-67.584c-2.048-12.8-3.072-26.624-3.072-39.936s1.536-27.136 3.584-39.936l-86.528-67.584c-8.192-6.144-10.24-16.896-5.12-26.112l81.92-141.824c5.12-9.216 15.872-12.8 25.088-9.216l101.888 40.96c20.992-15.872 44.032-29.696 69.12-39.936l15.36-108.544c1.536-10.24 9.728-17.408 19.968-17.408h163.84c10.24 0 18.944 7.168 20.48 17.408l15.36 108.544c25.088 10.24 47.616 23.552 69.12 39.936l101.888-40.96c9.216-3.072 19.968 0 25.088 9.216l81.92 141.824c4.608 8.704 2.56 19.968-5.12 26.112l-86.528 67.584c2.048 12.8 3.072 26.112 3.072 39.936s-1.024 27.136-2.56 39.936zM512 665.6c84.48 0 153.6-69.12 153.6-153.6s-69.12-153.6-153.6-153.6-153.6 69.12-153.6 153.6 69.12 153.6 153.6 153.6z"],"isMulticolor":false,"isMulticolor2":false,"tags":["uniE0A2"],"defaultCode":57506,"grid":0,"attrs":[]},"attrs":[],"properties":{"id":562,"order":29,"ligatures":"","prevSize":32,"code":57506,"name":"settings"},"setIdx":2,"setId":1,"iconIdx":8},{"icon":{"paths":["M556.8 417.707l125.867 94.293-256 192v-384zM556.8 417.707l125.867 94.293-256 192v-384zM556.8 417.707l-130.133-97.707v384l256-192zM469.333 173.653c-62.293 7.68-119.040 32.427-166.4 69.12l-60.586-61.013c63.146-51.627 141.226-85.76 226.986-94.293v86.186zM242.773 302.933c-36.693 47.36-61.44 104.107-69.12 166.4h-86.186c8.533-85.76 42.666-163.84 94.293-226.986zM173.653 554.667c7.68 62.293 32.427 119.040 69.12 165.973l-61.013 61.013c-51.627-63.146-85.76-141.226-94.293-226.986h86.186zM242.347 842.24l60.586-61.013c47.36 36.693 104.107 61.44 166.4 69.12v86.186c-85.333-8.533-163.84-42.666-226.986-94.293zM938.667 512c0 220.16-167.254 401.92-381.867 424.533v-86.186c167.253-22.187 296.533-165.547 296.533-338.347s-129.28-316.16-296.533-338.347v-86.186c214.613 22.613 381.867 204.373 381.867 424.533z"],"isMulticolor":false,"isMulticolor2":false,"tags":["uniE139"],"defaultCode":57657,"grid":0,"attrs":[]},"attrs":[],"properties":{"id":595,"order":46,"ligatures":"","prevSize":32,"code":57657,"name":"playlist"},"setIdx":2,"setId":1,"iconIdx":9},{"icon":{"paths":["M386.4 93.333c231.104 0 418.667 187.563 418.667 418.667s-187.563 418.667-418.667 418.667c-43.96 0-85.827-6.699-125.6-19.259 169.979-53.171 293.067-211.845 293.067-399.408s-123.088-346.237-293.067-399.408c39.773-12.56 81.64-19.259 125.6-19.259z"],"isMulticolor":false,"isMulticolor2":false,"tags":["uniE2A2"],"defaultCode":58018,"grid":0,"attrs":[]},"attrs":[],"properties":{"id":607,"order":34,"ligatures":"","prevSize":32,"code":58018,"name":"night"},"setIdx":2,"setId":1,"iconIdx":10},{"icon":{"paths":["M512 85.333c235.947 0 426.667 190.72 426.667 426.667s-190.72 426.667-426.667 426.667-426.667-190.72-426.667-426.667 190.72-426.667 426.667-426.667zM725.333 665.173l-153.173-153.173 153.173-153.173-60.16-60.16-153.173 153.173-153.173-153.173-60.16 60.16 153.173 153.173-153.173 153.173 60.16 60.16 153.173-153.173 153.173 153.173z"],"isMulticolor":false,"isMulticolor2":false,"tags":["uniE38F"],"defaultCode":58255,"grid":0,"attrs":[]},"attrs":[],"properties":{"id":662,"order":50,"ligatures":"","prevSize":32,"code":58255,"name":"cancel"},"setIdx":2,"setId":1,"iconIdx":11},{"icon":{"paths":["M512 170.667c188.587 0 341.333 152.746 341.333 341.333 0 66.987-19.626 129.28-52.906 181.76l-62.294-62.293c19.2-35.414 29.867-76.374 29.867-119.467 0-141.227-114.773-256-256-256v128l-170.667-170.667 170.667-170.666v128zM512 768v-128l170.667 170.667-170.667 170.666v-128c-188.587 0-341.333-152.746-341.333-341.333 0-66.987 19.626-129.28 52.906-181.76l62.294 62.293c-19.2 35.414-29.867 76.374-29.867 119.467 0 141.227 114.773 256 256 256z"],"isMulticolor":false,"isMulticolor2":false,"tags":["uniE116"],"defaultCode":57622,"grid":0,"attrs":[]},"attrs":[],"properties":{"id":709,"order":17,"ligatures":"","prevSize":32,"code":57622,"name":"sync"},"setIdx":2,"setId":1,"iconIdx":12},{"icon":{"paths":["M384 689.92l451.84-451.413 60.16 60.16-512 512-238.507-238.507 60.587-60.16z"],"isMulticolor":false,"isMulticolor2":false,"tags":["uniE390"],"defaultCode":58256,"grid":0,"attrs":[]},"attrs":[],"properties":{"id":733,"order":56,"ligatures":"","prevSize":32,"code":58256,"name":"confirm"},"setIdx":2,"setId":1,"iconIdx":13},{"icon":{"paths":["M853.333 370.773l141.227 141.227-141.227 141.227v200.106h-200.106l-141.227 141.227-141.227-141.227h-200.106v-200.106l-141.227-141.227 141.227-141.227v-200.106h200.106l141.227-141.227 141.227 141.227h200.106v200.106zM512 768c141.227 0 256-114.773 256-256s-114.773-256-256-256-256 114.773-256 256 114.773 256 256 256zM512 341.333c94.293 0 170.667 76.374 170.667 170.667s-76.374 170.667-170.667 170.667-170.667-76.374-170.667-170.667 76.374-170.667 170.667-170.667z"],"isMulticolor":false,"isMulticolor2":false,"tags":["uniE2A6"],"defaultCode":58022,"grid":0,"attrs":[]},"attrs":[],"properties":{"id":785,"order":15,"ligatures":"","prevSize":32,"code":58022,"name":"brightness"},"setIdx":2,"setId":1,"iconIdx":14},{"icon":{"paths":["M85.333 725.333v-42.666h128v170.666h-128v-42.666h85.334v-21.334h-42.667v-42.666h42.667v-21.334h-85.334zM128 341.333v-128h-42.667v-42.666h85.334v170.666h-42.667zM85.333 469.333v-42.666h128v38.4l-76.8 89.6h76.8v42.666h-128v-38.4l76.8-89.6h-76.8zM298.667 213.333h597.333v85.334h-597.333v-85.334zM298.667 810.667v-85.334h597.333v85.334h-597.333zM298.667 554.667v-85.334h597.333v85.334h-597.333z"],"isMulticolor":false,"isMulticolor2":false,"tags":["uniE22D"],"defaultCode":57901,"grid":0,"attrs":[]},"attrs":[],"properties":{"id":797,"order":58,"ligatures":"","prevSize":32,"code":57901,"name":"nodes"},"setIdx":2,"setId":1,"iconIdx":15},{"icon":{"paths":["M810.667 554.667h-256v256h-85.334v-256h-256v-85.334h256v-256h85.334v256h256v85.334z"],"isMulticolor":false,"isMulticolor2":false,"tags":["uniE18A"],"defaultCode":57738,"grid":0,"attrs":[]},"attrs":[],"properties":{"id":803,"order":59,"ligatures":"","prevSize":32,"code":57738,"name":"add"},"setIdx":2,"setId":1,"iconIdx":16},{"icon":{"paths":["M128 736l471.893-471.893 160 160-471.893 471.893h-160v-160zM883.627 300.373l-78.080 78.080-160-160 78.080-78.080c16.64-16.64 43.52-16.64 60.16 0l99.84 99.84c16.64 16.64 16.64 43.52 0 60.16z"],"isMulticolor":false,"isMulticolor2":false,"tags":["uniE2C6"],"defaultCode":58054,"grid":0,"attrs":[]},"attrs":[],"properties":{"id":834,"order":72,"ligatures":"","prevSize":32,"code":58054,"name":"edit"},"setIdx":2,"setId":1,"iconIdx":17},{"icon":{"paths":["M576 28.587c166.827 133.546 277.333 338.773 277.333 568.746 0 188.587-152.746 341.334-341.333 341.334s-341.333-152.747-341.333-341.334c0-144.213 51.626-276.906 137.813-379.306l-1.28 15.36c0 87.893 66.56 159.146 154.88 159.146 87.893 0 145.493-71.253 145.493-159.146 0-91.734-31.573-204.8-31.573-204.8zM499.627 810.667c113.066 0 204.8-91.734 204.8-204.8 0-59.307-8.534-117.334-25.174-172.374-43.52 58.454-121.6 94.72-197.12 110.080-75.093 15.36-119.893 64-119.893 133.12 0 74.24 61.44 133.974 137.387 133.974z"],"isMulticolor":false,"isMulticolor2":false,"tags":["uniE409"],"defaultCode":58377,"grid":0,"attrs":[]},"attrs":[],"properties":{"id":871,"order":10,"ligatures":"","prevSize":32,"code":58377,"name":"intensity"},"setIdx":2,"setId":1,"iconIdx":18},{"icon":{"paths":["M938.667 394.24l-232.534 201.813 69.547 299.947-263.68-159.147-263.68 159.147 69.973-299.947-232.96-201.813 306.774-26.027 119.893-282.88 119.893 282.454zM512 657.067l160.853 97.28-42.666-182.614 141.653-122.88-186.88-16.213-72.96-172.373-72.533 171.946-186.88 16.214 141.653 122.88-42.667 182.613z"],"isMulticolor":false,"isMulticolor2":false,"tags":["uniE410"],"defaultCode":58384,"grid":0,"attrs":[]},"attrs":[],"properties":{"id":927,"order":9,"ligatures":"","prevSize":32,"code":58384,"name":"star"},"setIdx":2,"setId":1,"iconIdx":19},{"icon":{"paths":["M512 85.333c235.52 0 426.667 191.147 426.667 426.667s-191.147 426.667-426.667 426.667-426.667-191.147-426.667-426.667 191.147-426.667 426.667-426.667zM554.667 725.333v-256h-85.334v256h85.334zM554.667 384v-85.333h-85.334v85.333h85.334z"],"isMulticolor":false,"isMulticolor2":false,"tags":["uniE066"],"defaultCode":57446,"grid":0,"attrs":[]},"attrs":[],"properties":{"id":952,"order":62,"ligatures":"","prevSize":32,"code":57446,"name":"info"},"setIdx":2,"setId":1,"iconIdx":20},{"icon":{"paths":["M256 810.667v-512h512v512c0 46.933-38.4 85.333-85.333 85.333h-341.334c-46.933 0-85.333-38.4-85.333-85.333zM810.667 170.667v85.333h-597.334v-85.333h149.334l42.666-42.667h213.334l42.666 42.667h149.334z"],"isMulticolor":false,"isMulticolor2":false,"tags":["uniE037"],"defaultCode":57399,"grid":0,"attrs":[]},"attrs":[],"properties":{"id":969,"order":55,"ligatures":"","prevSize":32,"code":57399,"name":"del"},"setIdx":2,"setId":1,"iconIdx":21},{"icon":{"paths":["M704 128c131.413 0 234.667 103.253 234.667 234.667 0 161.28-145.067 292.693-364.8 491.946l-61.867 56.32-61.867-55.893c-219.733-199.68-364.8-331.093-364.8-492.373 0-131.414 103.254-234.667 234.667-234.667 74.24 0 145.493 34.56 192 89.173 46.507-54.613 117.76-89.173 192-89.173zM516.267 791.467c203.093-183.894 337.066-305.494 337.066-428.8 0-85.334-64-149.334-149.333-149.334-65.707 0-129.707 42.24-151.893 100.694h-79.787c-22.613-58.454-86.613-100.694-152.32-100.694-85.333 0-149.333 64-149.333 149.334 0 123.306 133.973 244.906 337.066 428.8l4.267 4.266z"],"isMulticolor":false,"isMulticolor2":false,"tags":["uniE04C"],"defaultCode":57420,"grid":0,"attrs":[]},"attrs":[],"properties":{"id":1034,"order":61,"ligatures":"","prevSize":32,"code":57420,"name":"presets"},"setIdx":2,"setId":1,"iconIdx":22}],"height":1024,"metadata":{"name":"wled122"},"preferences":{"showGlyphs":true,"showCodes":true,"showQuickUse":true,"showQuickUse2":true,"showSVGs":true,"fontPref":{"prefix":"i-","metadata":{"fontFamily":"wled122","majorVersion":1,"minorVersion":7},"metrics":{"emSize":1024,"baseline":20,"whitespace":0},"embed":false,"autoHost":true,"noie8":true,"ie7":false,"showSelector":false,"showMetrics":false,"showMetadata":false,"showVersion":true},"imagePref":{"prefix":"icon-","png":true,"useClassSelector":true,"color":0,"bgColor":16777215,"name":"icomoon","classSelector":".icon"},"historySize":50,"quickUsageToken":{"wled122":"MDA1MGUxOTY0MyMxNzczNTY5NTMxI0ljcTJCSm9WQnFUUUFOdUZHQzlpaFZ0QkZaOGRFTXFRWFJoU1VjN2VqbmFn"},"showLiga":false,"gridSize":16}} ================================================ FILE: wled00/data/icons-ui/style.css ================================================ @font-face { font-family: 'wled122'; src: url('fonts/wled122.ttf?yzxblb') format('truetype'), url('fonts/wled122.woff?yzxblb') format('woff'), url('fonts/wled122.svg?yzxblb#wled122') format('svg'); font-weight: normal; font-style: normal; font-display: block; } [class^="i-"], [class*=" i-"] { /* use !important to prevent issues with browser extensions that change fonts */ font-family: 'wled122' !important; speak: never; font-style: normal; font-weight: normal; font-variant: normal; text-transform: none; line-height: 1; /* Better Font Rendering =========== */ -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } .i-pixelforge:before { content: "\e900"; } .i-editor:before { content: "\e901"; } .i-pattern:before { content: "\e23d"; } .i-segments:before { content: "\e34b"; } .i-sun:before { content: "\e333"; } .i-palette:before { content: "\e2b3"; } .i-eye:before { content: "\e0e8"; } .i-speed:before { content: "\e325"; } .i-expand:before { content: "\e395"; } .i-power:before { content: "\e08f"; } .i-settings:before { content: "\e0a2"; } .i-playlist:before { content: "\e139"; } .i-night:before { content: "\e2a2"; } .i-cancel:before { content: "\e38f"; } .i-sync:before { content: "\e116"; } .i-confirm:before { content: "\e390"; } .i-brightness:before { content: "\e2a6"; } .i-nodes:before { content: "\e22d"; } .i-add:before { content: "\e18a"; } .i-edit:before { content: "\e2c6"; } .i-intensity:before { content: "\e409"; } .i-star:before { content: "\e410"; } .i-info:before { content: "\e066"; } .i-del:before { content: "\e037"; } .i-presets:before { content: "\e04c"; } ================================================ FILE: wled00/data/index.css ================================================ @font-face { font-family: "WIcons"; src: url(data:font/woff2;charset=utf-8;base64,d09GMgABAAAAAAsAAA0AAAAAFlgAAAqqAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP0ZGVE0cGhgGYACEGhEICp08lnkLPgABNgIkA3gEIAWDGweCNhvxEVGUcFI3wBeFsYOmlCFXadeSCl4PGhMTwyMh0q9d2MXuDaeszCMkmT3Abd0Eu2ijAIMUa1IDbaQmRj/wndtnJB+d8BHN/+ZKv+zJJpUCCAsMA5IcArBbtlteAg6ToYi3nPp6KxH97fd9OQgssMYTSymghAPMMODmLNpvv/P8BPzeodosVKppyCRNZE0QEqlTCp0SqP9T4O4gAMzzFuTJg2RPa6/23s/f4IYKREKfr6tTc/cLu7dh2JTwmhJdUiSLQqZVQFvmy6mScazQAwlZ7apjDAOl7l8dYEyN5azo7xRYCTCz7gCAzIa7hoI38uBn9/NfQMIrA5RCyCOfOtya0oEneAKP2+M8AEzujgX5QIQYkXEhC5nk4BVC6f6L4cmN4YazURxLPmVQjD4XkFWhNcfmv38+EMNisJkOyOKfgx6n/2z9efLjZY9fPol6EvJEdaY7I5y1zu3Ok64kl58r7bcprplPfZ+GvELuPwEAiGmvZJPj8ErdT9kXF+1jV7AvsG3seNY31uuFw5m/LLgKwNzGLGd8mO+cfw6A8S5jCsM/9wfEH8iWrJEYBLUxMHfsLJpcHQqzOuDEFhQjM1otoVvVg94O/zMIoCJtI1ACwThSfr8yQL1KvQ5rAApCJOJJKBSl4cdB3IwhcY7A5i3/hNDuIJq7NmfVLJNq2Z1hACMTkEpSDwPzGMtL6Qj7EFl8BemVw4zAppSRHW5ZhSxVZIZwKIDXduoxP57T0cgYeukZbC1afoHHq6/OwUSERJEC0lcLGXjp0QKyd7tOLYzdaXLTFHYixavtddgQ0YyI6xbZbLleW+DKSDGxqrvjTWIRtNNgOF6yGYc0ZhihH0R1vR9WuWn2Q4pkWdcmW0QsbEIYzglYJKxhzbvPBSWhn9uiMsuraZ3jiQ75dBgpD4mW9tgSdSHFzLzEcnLiNDvb59n9lVxzrObWObWDviOG3Dwt5RZCKdLLyl04L0+xvKG6aEG0nJFTM6AcuXROdpzmFJCcH9+uWfmohYxDH0Nxk+nRN4ZT3uJW3O32b9GChl57lSFlYeur0F6s+ve/cC8GeUHLy5CeTZoB7XGeFaxDWspDQ9CBaXdnUZU9hGerGTqIgUtgQxhFauojOOdYXo78csyahwycYlRk/FVxQdrYrQc7r1tJQJv1+Xi5FbW+xPCwj5pLicU1YATAPRM9hVc9RfdxWc2300x9lIgM3K/9xgtYHI8miESYICECeMSQt3EtAdq7jhUlLE2CiYgNqUeZNrzc9nLTTg+EeckP9Kz28vnwTeoolOtCGyF5WOonuVZNPkHX/RKff2/l48rnCUbIfJZad59cYhSwkWPEJUQkRvZrYUMVbAKCS6jB/bp7M2ItABfEMpgBinhBFLgze5jkAlW62xjORdV67XqlRsPsObLU7cI+/4ss97HdGJ2iXMrTFMuRTzAe2SISYd9NlE6rZmS4ahqS+8GKTA7ZuWs9YGQfYGQHdUqbXcy+iQC2aiEDhkdLTkhvpoYOmp6tTc6yvgVbEIGdkoPu2sV275V27N23h7awKFxyUm5n1CGxXfscu7nrlINyF7v00vEyotuwG5If4LpYtazK+s3xmi4bpC2UoPNVnRa9JubCZj3+jg4Zl+iGnds38V2bNqxnXOKcUkYv8Vw1vppL4+lMDDMok9jqbFmxHE1LeYp/Sc6O03odj1droeRpqckiE7Qa4jB+nO5OlVyIymtCtJdACJKcTKe3Kct4DL+2knGWW/gpzKXr191XULH0Ay1NmD9ndUMvJaoqrCq5dStqFaosxPyr8/N902gfWD5BcFtmaqreo8wxq1T1+g6+d8iQDLnRJBeYZP4jf/MEBpHR0Lj1zmvSecXw/+vqjLhyTs+enLSoujoiRy3LDbIhvmtxCTAzTZPZBNzr683+Pi7U/TOZjE+Z8yHfzlQzMbsdS4t1ulIwTTJN6/hj5nBM5GevHDFhfTVob+tnthVHUVyu6o1q8GeQCn5TYowqQ4a0asLK33fsSX2zLCVo473WZ4XPWu/gTUr4n1nSfH38mHmqzKpYCucxNo9yXJO1toU1NYX8GuAm7EXRRVH9ja9f0zCPBxUQoNvXeb64MoLftWmu23d39+9eBU1d+UObPKOkpETCw4F7hvbO3brNG0u0Qnrt6B9fveVI0AIMu+aSkOtc3VrSJG5IwMsAv2Rwvfs6ObS5xyXIGfFGlW5cxjv/b4+s7/gTclsCLce7ZvXo6i3rJxi2P9ln4irW+XW89OtSmD3FBmYRo9jaDUvEEip98Bf1mytr7BaFwmJXXVf/AsfRQx8c8MBdywDCjkgAM7s2GDeXXEdyeRSPy4viEmSqzesYgTclp1nKvv50S/kNN+Me01EF0wbWprFZyoBXWACDKu3Cljz17p1WbIZ7xFwjnWai0bGQqsZQK2xf3jggsrSXIVaxQ5EaS2GoE0/jlHG6deccNaU4PqGWZWrG4+588wUzl9saGzWaiLzKjH1B/XEQ4LgwcYIvPn16iYkW1K9gpBLXayyhAJWUWWu2o4jXaVtbtfzXgfuQTk3DaPbaBw817l7OvamJX0Yz0gPOtn4jx9N79MYQbCTF84i+sxz6kXTj3MYcbvkx56XzGsMoWng/EOvWrcWLo2/Jki/by8srSCjHsse7du1fBqtFNQfTTAMOYnfw+6srmZgvttlWFUunU+SoXWtJpU4qtduaqVndnxftCHhw2c5Qs43pa9cbRfu0y1Nt5oTN6hPvfS+w6LgjvexcaoGJZO76IeYh02unz5FWVjqiKer9q78ieyU0Da60eLSoAM296/BJHbMKCIXs4Xs17vLgTs35ikkIrh9BLc4dTXAxNvU5UvV1Vb7bhkO4BhD/9lGHO+/fn4NjlwtHhQO8BSSK5a9HRtGUqfZwnbmeTb2rTbpb764lHTY8Ydt87VtQbHW9UlkZn5WaPRqobxB3qLN+/cb18J+f+dNROn5AISbO1lVAbseul2ewdd4vjwdVkzC2L02fKWdJE3fnxAH7JhVtSF4/EDxhQNoukP0c++bTOk5j6JfTPn6EbndfYOD6FcsJIgKUob1Inz6u5zRZLPsWD0IB4t4DWzCg1XLY/wIGg30NHTTauPsJKVtSOtJ9O2/rYgfF03zzHqybNYqD/yx4tforP6Ld9vAr2ybl/3yIRTcdrwzuetFFSSMAH0LMxI2+fkDdCcDYJyA3ipitETmBOLi8EZmJSOpOPFq/DTxMGhrE3JLs83kymayp5Uh8Ms2xDiHtOJqBLNjEdz8eyLwgrYDkpX6syTp5sNVEYdFEZesHeyLOS68ey57lZy682pmLOqOJ4wcS2GwSmTfZWPLgMWbCdumm7N32YP5QDH110k4bAfiCL0Df065NIHyl/q626c2Y16wHeIviHYE4G+iT5oGtK/bUXlddcGyeJwQBPKxxgIKM7PhKE0/2uuQ+juqSmmzG3PDQFXfqjwMpWpmyPLpjTQbA8zda3OddU9za9W/xDBTYht7SfiikklBMEosFGw5ceGBX1J+TRABBhBBGBFHEEIeCD40EkkghjYx77NI+y02QY4JeWJYom4tVXCrlMg1XCDMwWSeBQMpFORkyRSehUM1EmQphXMqVogyVtJNIKOEiERcruUyTmZOVJOkkzsrJEGRl5WR5AgA=); } :root { --c-1: #111; --c-f: #fff; --c-2: #222; --c-3: #333; --c-4: #444; --c-5: #555; --c-6: #666; --c-8: #888; --c-b: #bbb; --c-c: #ccc; --c-e: #eee; --c-d: #ddd; --c-r: #c32; --c-g: #2c1; --c-l: #48a; --c-y: #a90; --t-b: .5; --c-o: rgba(34, 34, 34, .9); --c-tb : rgba(34, 34, 34, var(--t-b)); --c-tba: rgba(102, 102, 102, var(--t-b)); --c-tbh: rgba(51, 51, 51, var(--t-b)); /*following are internal*/ --th: 70px; --tp: 70px; --bh: 63px; --tbp: 14px 14px 10px 14px; --bbp: 9px 0 7px 0; --bhd: none; --sgp: "block"; --bmt: 0; --sti: 42px; --stp: 42px; } html { touch-action: manipulation; } body { margin: 0; background-color: var(--c-1); font-family: Helvetica, Verdana, sans-serif; font-size: 17px; color: var(--c-f); text-align: center; -webkit-touch-callout: none; -webkit-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; -webkit-tap-highlight-color: transparent; scrollbar-width: 6px; scrollbar-color: var(--c-sb) transparent; } html, body { height: 100%; width: 100%; position: fixed; overscroll-behavior: none; } #bg { height: 100vh; width: 100vw; position: fixed; z-index: -10; background-position: center; background-repeat: no-repeat; background-size: cover; opacity: 0; transition: opacity 2s; } p { margin: 10px 0 2px 0; } a, p, a:visited { color: var(--c-d); } a, a:visited { text-decoration: none; } button { outline: 0; cursor: pointer; } .iconlabel{ font-size:12px; margin-top:2px; } .labels { margin: 0; padding: 8px 0 2px 0; font-size: 19px; } #namelabel { position: fixed; bottom: calc(var(--bh) + 6px); right: 6px; color: var(--c-8); /* set bright (--c-d) with dark text shadow (see below) to be legible on gray background (in image) */ cursor: pointer; writing-mode: vertical-rl; /* transform: rotate(180deg); */ } .bri { padding: 4px; } .wrapper { position: fixed; top: 0; left: 0; right: 0; background: var(--c-tb); z-index: 1; } .icons { font-family: 'WIcons'; font-style: normal; font-size: 24px !important; line-height: 1 !important; display: inline-block; } .on { color: var(--c-g) !important; } .off { color: var(--c-6) !important; /* cursor: default !important; */ } .top .icons, .bot .icons { margin: -2px 0 4px 0; } .huge { font-size: 60px !important; } .segt, .plentry TABLE { table-layout: fixed; width: 100%; } .segt TD { padding: 2px 0 !important; text-align: center; /*text-transform: uppercase;*/ } .segt TD, .plentry TD { font-size: 13px; padding: 0; vertical-align: middle; } .keytd { text-align: left; } .valtd { text-align: right; } .valtd i { font-size: small; } .slider-icon { position: absolute; left: 8px; bottom: 5px; } .sel-icon { transform: translateX(3px); } .e-icon, .g-icon, .sel-icon, .slider-icon { cursor: pointer; color: var(--c-d); } .g-icon { font-style: normal; position: absolute; top: 8px; right: 8px; } /* pop-up container */ .pop { position: absolute; display: inline-block; top: 0; right: 0; } /* pop-up content (segment sets) */ .pop-c { position: absolute; background-color: var(--c-2); border: 1px solid var(--c-8); border-radius: 20px; z-index: 1; top: 3px; right: 35px; padding: 3px 8px 1px; font-size: 24px; line-height: 24px; } .pop-c span { padding: 2px 6px; } .search-icon { position: absolute; top: 8px; left: 12px; width: 24px; height: 24px; } .clear-icon { position: absolute; top: 8px; right: 9px; cursor: pointer; } .flr { color: var(--c-f); transform: rotate(0deg); transition: transform .3s; position: absolute; top: 0; right: 0; padding: 8px; } .expanded .flr, .exp { transform: rotate(180deg); } .il { display: inline-block; vertical-align: middle; } #liveview { height: 4px; width: 100%; border: 0; } #liveview2D { height: 90%; width: 90%; border: 0; position: absolute; top: 50%; left: 50%; transform: translate(-50%,-50%); } .tab { background-color: transparent; color: var(--c-d); } .bot { position: fixed; bottom: 0; left: 0; width: 100%; background-color: var(--c-tb); } .tab button { background-color: transparent; float: left; border: 0; transition: color .3s, background-color .3s; font-size: 17px; color: var(--c-c); min-width: 44px; } .top button { padding: var(--tbp); margin: 0; } .bot button { padding: var(--bbp); width: 25%; margin: 0; } .tab button:hover { background-color: var(--c-tbh); color: var(--c-e); } .tab button.active { background-color: var(--c-tba) !important; color: var(--c-f); } .active { background-color: var(--c-6) !important; color: var(--c-f); } .container { --n: 1; width: 100%; width: calc(var(--n)*100%); height: calc(100% - var(--tp) - var(--bh)); margin-top: var(--tp); transform: translate(calc(var(--i, 0)/var(--n)*-100%)); overscroll-behavior: none; } .tabcontent { float: left; position: relative; width: 100%; width: calc(100%/var(--n)); box-sizing: border-box; border: 0; overflow-y: auto; overflow-x: hidden; height: 100%; overscroll-behavior: none; padding: 0 4px; -webkit-overflow-scrolling: touch; } #Segments, #Presets, #Effects, #Colors { font-size: 19px; padding: 4px 0 0; } #segutil, #segutil2, #segcont, #putil, #pcont, #pql, #fx, #palw, #bsp, .fnd { max-width: 280px; } #putil, #segutil, #segutil2, #bsp { min-height: 42px; margin: 0 auto; } #segutil .segin { padding-top: 12px; } #fx, #pql, #segcont, #pcont, #sliders, #qcs-w, #hexw, #pall, #ledmap, .slider, .filter, .option, .segname, .pname, .fnd { margin: 0 auto; } #putil { padding: 5px 0 0; } /* Quick load magin for simplified UI */ .simplified #pql, .simplified #palw, .simplified #fx { margin-bottom: 8px; } .smooth { transition: transform calc(var(--f, 1)*.5s) ease-out } .tab-label { margin: 0 0 -5px 0; padding-bottom: 4px; display: var(--bhd); } .overlay { position: fixed; height: 100%; width: 100%; top: 0; left: 0; background-color: var(--c-3); font-size: 24px; display: flex; align-items: center; justify-content: center; z-index: 11; opacity: .95; transition: .7s; pointer-events: none; } .staytop, .staybot { display: block; position: -webkit-sticky; position: sticky !important; top: 0; z-index: 2; margin: 0 auto auto; } .staybot { bottom: 5px; } #sliders { position: -webkit-sticky; position: sticky; bottom: 0; max-width: 300px; z-index: 2; } #sliders .labels { padding-top: 3px; font-size: small; } .slider { /*max-width: 300px;*/ /* margin: 5px auto; add 5px; if you want some vertical space but looks ugly */ border-radius: 24px; position: relative; padding-bottom: 2px; } /* Slider wrapper div */ .sliderwrap { height: 30px; width: 230px; max-width: 230px; position: relative; z-index: 0; } #sliders .slider { padding-right: 64px; /* offset for bubble */ } #sliders .slider, #info .slider { background-color: var(--c-2); } #sliders .sliderwrap, .sbs .sliderwrap { left: 32px; /* offset for icon */ } .filter, .option { background-color: var(--c-4); border-radius: 26px; height: 26px; max-width: 300px; /* margin: 0 auto 4px; add 4-8px if you want space at the bottom */ padding: 4px 2px; position: relative; opacity: 1; transition: opacity .25s linear, height .2s, transform .2s; } .filter { z-index: 1; /*overflow: visible;*/ border-radius: 0 0 16px 16px; max-width: 220px; height: 54px; line-height: 1.5; padding-bottom: 8px; pointer-events: none; } /* New tooltip */ .tooltip { position: absolute; opacity: 0; visibility: hidden; transition: opacity .25s ease, visibility .25s ease; background-color: var(--c-5); box-shadow: 4px 4px 10px 4px var(--c-1); color: var(--c-f); text-align: center; padding: 8px 16px; border-radius: 6px; z-index: 1; pointer-events: none; } .tooltip::after { content: ""; position: absolute; border: 8px solid; border-color: var(--c-5) transparent transparent transparent; top: 100%; left: calc(50% - 8px); z-index: 0; } .tooltip.visible { opacity: 1; visibility: visible; } .fade { visibility: hidden; /* hide it */ opacity: 0; /* make it transparent */ transform: scaleY(0); /* shrink content */ height: 0; /* force other elements to move */ padding: 0; /* remove empty space */ } .first { margin-top: 10px; } #toast { opacity: 0; background-color: var(--c-5); border: 1px solid var(--c-2); max-width: 90%; color: var(--c-f); text-align: center; border-radius: 5px; padding: 22px; position: fixed; z-index: 5; left: 50%; transform: translateX(-50%); bottom: calc(var(--bh) + 22px); font-size: 17px; pointer-events: none; } #toast.show { opacity: 1; animation: fadein .5s, fadein .5s 2.5s reverse; } #toast.error { opacity: 1; background-color: #b21; animation: fadein .5s; } .modal { position: fixed; left: 0; bottom: 0; right: 0; top: calc(var(--th) - 1px); background-color: var(--c-o); transform: translateY(100%); transition: transform .4s; padding: 8px; font-size: 20px; overflow: auto; } .close { position: -webkit-sticky; position: sticky; top: 0; float: right; } #info, #nodes { z-index: 4; } #rover { z-index: 3; } #rover .ibtn { margin: 5px; } #ndlt { margin: 12px 0; } #roverstar { position: fixed; top: calc(var(--th) + 5px); left: 1px; cursor: pointer; } #connind { position: fixed; bottom: calc(var(--bh) + 5px); left: 4px; padding: 5px; border-radius: 5px; background-color: #a90; z-index: -2; } #info .slider { max-width: 200px; min-width: 145px; float: right; margin: 0; } #info .sliderwrap { width: 200px; } #info table, #nodes table { table-layout: fixed; width: 100%; } #info td, #nodes td { padding-bottom: 8px; } #info .ibtn { margin: 5px; } #info div, #nodes div { max-width: 490px; margin: 0 auto; } #info #imgw { margin: 8px auto; } #lv { max-width: 600px; display: inline-block; } #heart { transition: color .9s; font-size: 16px; color: #f00; } img { max-width: 100%; max-height: 100%; } .wi { image-rendering: pixelated; image-rendering: crisp-edges; width: 210px; } @keyframes fadein { from {bottom: 0; opacity: 0;} to {bottom: calc(var(--bh) + 22px); opacity: 1;} } .sliderdisplay { content:''; position: absolute; top: 12px; left: 8px; right: 8px; height: 5px; background: var(--c-4); border-radius: 16px; pointer-events: none; z-index: -1; --bg: var(--c-f); } #rwrap .sliderdisplay { --bg: none; background: linear-gradient(90deg, #000 -15%, #f00); } /* -15% since #000 is too dark */ #gwrap .sliderdisplay { --bg: none; background: linear-gradient(90deg, #000 -15%, #0f0); } /* -15% since #000 is too dark */ #bwrap .sliderdisplay { --bg: none; background: linear-gradient(90deg, #000 -15%, #00f); } /* -15% since #000 is too dark */ #wwrap .sliderdisplay { --bg: none; background: linear-gradient(90deg, #000 -15%, #fff); } /* -15% since #000 is too dark */ #kwrap .sliderdisplay, #wbal .sliderdisplay { background: linear-gradient(90deg, #ff8f1f 0%, #fff 50%, #cbdbff); } /* wrapper divs hidden by default */ #liveview, #liveview2D, #roverstar, #pql #rgbwrap, #swrap, #hwrap, #kwrap, #wwrap, #wbal, #qcs-w, #hexw, .clear-icon, .edit-icon, .ptxt { display: none; } .sliderbubble { width: 24px; position: absolute; display: inline-block; border-radius: 16px; background: var(--c-3); color: var(--c-f); padding: 4px; font-size: 14px; right: 6px; transition: visibility .25s ease,opacity .25s ease; opacity: 0; visibility: hidden; /* left: 8px; */ top: 4px; } output.sliderbubbleshow { visibility: visible; opacity: 1; } input[type=range] { -webkit-appearance: none; width: 100%; padding: 0; margin: 0; background-color: transparent; cursor: pointer; } input[type=range]:focus { outline: 0; } input[type=range]::-webkit-slider-runnable-track { width: 100%; height: 30px; cursor: pointer; background: transparent; } input[type=range]::-webkit-slider-thumb { height: 16px; width: 16px; border-radius: 50%; background: var(--c-f); cursor: pointer; -webkit-appearance: none; margin-top: 7px; } input[type=range]::-moz-range-track { width: 100%; height: 30px; background-color: rgba(0, 0, 0, 0); } input[type=range]::-moz-range-thumb { border: 0 solid rgba(0, 0, 0, 0); height: 16px; width: 16px; border-radius: 50%; background: var(--c-f); transform: translateY(5px); } #Colors input[type=range]::-webkit-slider-thumb { height: 18px; width: 18px; border: 2px solid var(--c-1); margin-top: 5px; } #Colors input[type=range]::-moz-range-thumb { border: 2px solid var(--c-1); } #Colors .sliderwrap { margin: 2px 0 0; } /* Dynamically hide labels */ .hd { display: var(--bhd); } /* Do not hide quick load label in simplified mode on small screen widths */ .simplified #pql .hd { display: var(--bhd) !important; } #briwrap { min-width: 300px; float: right; margin-top: var(--bmt); } #picker { margin: 4px auto 0 !important; max-width: max-content; } /* buttons */ .btn { padding: 8px; margin: 10px 4px; width: 230px; font-size: 19px; color: var(--c-d); cursor: pointer; border-radius: 25px; transition-duration: .3s; -webkit-backface-visibility: hidden; -webkit-transform: translate3d(0,0,0); backface-visibility: hidden; transform: translate3d(0,0,0); overflow: hidden; text-overflow: ellipsis; border: 1px solid var(--c-3); background-color: var(--c-3); } #segutil .btn-s:hover, #segutil2 .btn-s:hover, #putil .btn-s:hover, .btn:hover { border: 1px solid var(--c-5) /*!important*/; background-color: var(--c-5) /*!important*/; } .btn-s { width: 100%; margin: 0; } .btn-icon { margin: -4px 4px -1px 0; vertical-align: middle; display: inline-block; } .btn-n { width: 230px; margin: 0 8px 0 0; } .btn-p { width: 120px; margin: 5px 0; } .btn-xs, .btn-pl-del, .btn-pl-add { width: 42px !important; height: 42px !important; text-overflow: clip; } .btn-xs { margin: 0; } #info .btn-xs { border: 1px solid var(--c-4); } #btns .btn-xs { margin: 0 4px; } #putil .btn-s { width: 135px; } #nodes .ibtn { margin: 0; } #segutil .btn-s, #segutil2 .btn-s, #putil .btn-s { background-color: var(--c-3); border: 1px solid var(--c-3); } .btn-pl-del, .btn-pl-add { margin: 0; white-space: nowrap; } a.btn { display: block; white-space: nowrap; text-align: center; padding: 9px 32px 7px 24px; position: relative; box-sizing: border-box; line-height: 24px; } /* Quick color select wrapper div */ #qcs-w { margin-top: 10px; } /* Quick color select buttons */ .qcs { margin: 2px; border-radius: 14px; display: inline-block; width: 28px; height: 28px; line-height: 28px; } /* Quick color select Black and White button (has white/black border, depending on the theme) */ .qcsb, .qcsw { width: 26px; height: 26px; line-height: 26px; border: 1px solid var(--c-f); } /* Hex color input wrapper div */ #hexw { margin-top: 5px; } select { padding: 4px 8px; margin: 0; font-size: 19px; background-color: var(--c-3); color: var(--c-d); cursor: pointer; border: 0 solid var(--c-2); border-radius: 20px; transition-duration: .5s; -webkit-backface-visibility: hidden; -webkit-transform: translate3d(0,0,0); -webkit-appearance: none; -moz-appearance: none; backface-visibility: hidden; transform: translate3d(0,0,0); text-overflow: ellipsis; } #tt { text-align: center; } select.sel-p, select.sel-pl, select.sel-ple { margin: 5px 0; width: 100%; height: 40px; padding: 0 20px 0 8px; } div.sel-p { position: relative; } div.sel-p:after { content: ""; position: absolute; right: 10px; top: 22px; width: 0; height: 0; border-left: 8px solid transparent; border-right: 8px solid transparent; border-top: 8px solid var(--c-f); } select.sel-ple { text-align: center; } select.sel-sg { margin: 5px 0; height: 40px; } option { background-color: var(--c-3); color: var(--c-f); } input[type=number], input[type=text] { background: var(--c-3); color: var(--c-f); border: 0 solid var(--c-2); border-radius: 10px; padding: 8px; /*margin: 6px 6px 6px 0;*/ font-size: 19px; transition: background-color .2s; outline: 0; -webkit-appearance: textfield; -moz-appearance: textfield; appearance: textfield; } input[type=number] { text-align: right; width: 50px; } input[type=text] { text-align: center; } input[type=number]:focus, input[type=text]:focus { background: var(--c-6); } input[type=number]::-webkit-inner-spin-button, input[type=number]::-webkit-outer-spin-button { -webkit-appearance: none; } #hexw input[type=text] { width: 6em; } input[type=text].ptxt { width: calc(100% - 24px); } textarea { background: var(--c-2); color: var(--c-f); width: calc(100% - 14px); /* +padding=260px */ height: 90px; border-radius: 5px; border: 2px solid var(--c-5); outline: 0; resize: none; font-size: 19px; padding: 5px; } .apitxt { height: 7em; } ::selection { background: var(--c-b); } .ptxt { margin: -1px 4px 8px !important; } .stxt { width: 50px !important; } .segname, .pname { white-space: nowrap; text-align: center; overflow: hidden; text-overflow: ellipsis; line-height: 24px; padding: 8px 24px; max-width: 170px; position: relative; } .segname .flr, .pname .flr { transform: rotate(0deg); /*right: -6px;*/ } /* segment power wrapper */ .sbs { /*padding: 1px 0 1px 20px;*/ display: var(--sgp); width: 100%; position: relative; } .pname { top: 1px; } .plname { top: 0; } /* preset id number */ .pid { position: absolute; top: 8px; left: 12px; font-size: 16px; text-align: center; color: var(--c-b); } .newseg { cursor: default; } /* .ic { padding: 6px 0 0 0; } */ /* color selector */ #csl button { width: 44px; height: 44px; margin: 5px; border: 2px solid var(--c-d) !important; background-color: #000; } /* selected color selector */ #csl .sl { margin: 2px; width: 50px; height: 50px; border-width: 5px !important; } .qcs, #namelabel { /* text shadow for name to be legible on grey backround */ text-shadow: -1px -1px 0 var(--c-1), 1px -1px 0 var(--c-1), -1px 1px 0 var(--c-1), 1px 1px 0 var(--c-1); } .psts { color: var(--c-f); margin: 4px; } .pwr { color: var(--c-6); cursor: pointer; } .act { color: var(--c-f); } .del { position: absolute; bottom: 8px; right: 8px; } .frz { left: 10px; position: absolute; top: 8px; cursor: pointer; z-index: 1; } /* radiobuttons and checkmarks */ .check, .radio { display: block; position: relative; cursor: pointer; } .revchkl { padding: 4px 0 0 35px; margin-bottom: 0; margin-top: 8px; } TD .revchkl { padding: 0 0 0 32px; margin-top: 0; } .check input, .radio input { position: absolute; opacity: 0; cursor: pointer; height: 0; width: 0; } .checkmark, .radiomark { position: absolute; height: 24px; width: 24px; top: 0; bottom: 0; left: 0; background-color: var(--c-3); border: 1px solid var(--c-2); } .radiomark { top: 8px; left: 8px; height: 22px; width: 22px; border-radius: 50%; background-color: transparent; } .checkmark { border-radius: 10px; } .radio:hover input ~ .radiomark, .check:hover input ~ .checkmark { background-color: var(--c-5); } .checkmark:after, .radiomark:after { content: ""; position: absolute; display: none; } .check .checkmark:after { left: 9px; top: 4px; width: 5px; height: 10px; border: solid var(--c-f); border-width: 0 3px 3px 0; } .rot45, .check .checkmark:after { -webkit-transform: rotate(45deg); -ms-transform: rotate(45deg); transform: rotate(45deg); } .radio .radiomark:after { width: 14px; height: 14px; top: 50%; left: 50%; margin: -7px; border-radius: 50%; background: var(--c-f); } TD .checkmark, TD .radiomark { top: -6px; } .h { font-size: 13px; text-align: center; color: var(--c-b); } .bp { margin-bottom: 8px; } /* segment & preset wrapper */ .seg, .pres { background-color: var(--c-2); /*color: var(--c-f);*/ /* seems to affect only the Add segment button, which should be same color as reset segments */ border: 0 solid var(--c-f); text-align: left; transition: background-color .5s; border-radius: 21px; } .seg { top: auto !important; /* prevent sticky */ bottom: auto !important; } /* checkmark labels */ .seg .schkl { position: absolute; top: 7px; left: 9px; } /* checkmark labels */ .filter .fchkl, .option .ochkl { display: inline-block; min-width: .7em; padding: 1px 4px 1px 32px; text-align: left; line-height: 24px; vertical-align: middle; -webkit-filter: grayscale(100%); /* Safari 6.0 - 9.0 */ filter: grayscale(100%); } .filter .fchkl { margin: 0 4px; min-width: 20px; pointer-events: auto; } .lbl-l { font-size: 13px; text-align: center; padding: 4px 0; } .lbl-s { display: inline-block; margin-top: 6px; font-size: 13px; width: 48%; text-align: center; } /* list wrapper */ .list { position: relative; transition: background-color .5s; margin: auto auto 10px; line-height: 24px; } /* list item */ .lstI { align-items: center; cursor: pointer; background-color: var(--c-2); overflow: hidden; position: -webkit-sticky; position: sticky; border-radius: 21px; margin: 0 auto 12px; min-height: 40px; border: 1px solid var(--c-2); width: 100%; } #segutil { margin-bottom: 12px; } #segcont > div:first-child, #fxFind { margin-top: 4px; } /* Simplify segments */ .simplified #segcont .lstI { margin-top: 4px; min-height: unset; } /* selected item/element */ .selected { /* has to be after .lstI since !important is not ok */ background: var(--c-4); } #segcont .seg:hover:not([class*="expanded"]), .lstI:hover:not([class*="expanded"]) { background: var(--c-5); } .selected .checkmark, .selected .radiomark, .selected input[type=number], .selected input[type=text] { background-color: var(--c-3); } /* selected list item */ .lstI.selected { top: 0; bottom: 0; border: 1px solid var(--c-4); } .lstI.sticky, .lstI.selected { z-index: 1; box-shadow: 0 0 10px 4px var(--c-1); } .lstI .flr:hover { background: var(--c-6); border-radius: 100%; } #pcont .selected:not([class*="expanded"]) { bottom: 52px; top: 42px; } #fxlist .lstI.selected { top: calc(var(--sti) + 42px); } #pallist .lstI.selected { top: calc(var(--stp) + 42px); } dialog::backdrop { backdrop-filter: blur(10px); -webkit-backdrop-filter: blur(10px); } dialog { max-height: 70%; border: 0; border-radius: 10px; background: linear-gradient(rgba(0, 0, 0, 0.15), rgba(0, 0, 0, 0.1)), var(--c-3); box-shadow: 4px 4px 10px 4px var(--c-1); color: var(--c-f); } #fxlist .lstI.sticky { top: var(--sti); } #pallist .lstI.sticky { top: var(--stp); } /* list item content */ .lstIcontent { padding: 9px 0 7px; position: relative; } /* list item name (for sorting) */ .lstIname { white-space: nowrap; text-overflow: ellipsis; -webkit-filter: grayscale(100%); /* Safari 6.0 - 9.0 */ filter: grayscale(100%); } /* list item palette preview */ .lstIprev { width: 100%; height: 6px; position: absolute; bottom: 0; left: 0; z-index: -1; } /* find/search element */ .fnd { position: relative; } .fnd input[type="text"] { display: block; width: 100%; box-sizing: border-box; padding: 8px 40px 8px 44px; margin: 4px auto 12px; text-align: left; border-radius: 21px; background: var(--c-2); border: 1px solid var(--c-3); -webkit-filter: grayscale(100%); /* Safari 6.0 - 9.0 */ filter: grayscale(100%); } .fnd input[type="text"]:focus { background-color: var(--c-4); } .fnd input[type="text"]:not(:placeholder-shown), .fnd input[type="text"]:hover { background-color: var(--c-3); } #fxFind.fnd input[type="text"] { margin-bottom: 0; } #fxFind { margin-bottom: 12px; } /* segment & preset inner/expanded content */ .segin, .presin { padding: 8px; position: relative; } .presin { width: 100%; box-sizing: border-box; } .btn-s, .btn-n { border: 1px solid var(--c-2); background-color: var(--c-2); } .modal .btn:hover, .segin .btn:hover { border: 1px solid var(--c-5) /*!important*/; background-color: var(--c-5) /*!important*/; } /* hidden list items, must be after .expanded */ .pres .lstIcontent, .segin { display: none; } .check input:checked ~ .checkmark:after, .radio input:checked ~ .radiomark:after, .show, .expanded .edit-icon, .expanded .segin, .expanded .presin, .expanded .sbs, .expanded { display: inline-block !important; } .hide, .expanded .segin.hide, .expanded .presin.hide, .expanded .sbs.hide, .expanded .g-icon { display: none !important; } .m6 { margin: 6px 0; } .c { text-align: center; } .po2 { display: none; margin-top: 8px; } .pwarn { color: red; } /* horizontal divider (playlist entries) */ .hrz { width: auto; height: 2px; background-color: var(--c-b); margin: 3px 0; } ::-webkit-scrollbar { width: 6px; } ::-webkit-scrollbar-track { background: transparent; } ::-webkit-scrollbar-thumb { background: var(--c-sb); opacity: .2; border-radius: 5px; } ::-webkit-scrollbar-thumb:hover { background: var(--c-sbh); } @media not all and (hover: none) { .sliderwrap:hover + output.sliderbubble { visibility: visible; opacity: 1; } } @media all and (max-width: 1023px) { .top button { width: 8%; padding: 10px 0 8px 0; } #buttonPcm { display: none; } } @media all and (max-width: 335px) { .sliderbubble { display: none; } } @media all and (max-width: 550px) and (min-width: 374px) { #info table .btn, #nodes table .btn { width: 200px; } #info .ibtn, #nodes .ibtn { width: 145px; } #info div, #nodes div, #nodes a.btn { max-width: 320px; } } @media all and (max-width: 420px) { #buttonNodes { display: none; } } @media all and (max-width: 639px) { .top button { width: 16.6%; padding: 8px 0 4px 0; } #briwrap { margin: 0 auto !important; float: none; display: inline-block; } .hd { display: none !important; } } @media all and (min-width: 420px) and (max-width: 639px) { .top button { width: 14.28%; padding: 8px 0 4px 0; } } @media all and (min-width: 640px) and (max-width: 767px) { #buttonNodes { display: none; } } /* small screen & tablet "PC mode" support */ @media all and (min-width: 1024px) and (max-width: 1249px) { #segutil, #segutil2, #segcont, #putil, #pcont, #pql, #fx, #palw, #psFind, #sliders { width: 100%; max-width: 280px; font-size: 18px; } #putil .btn-s { width: 114px; } #sliders .sliderbubble { display: none; } #sliders .sliderwrap, .sbs .sliderwrap { width: calc(100% - 42px); } #sliders .slider { padding-right: 0; } #sliders .sliderwrap { left: 12px; } .segname { max-width: calc(100% - 110px); } .segt TD { padding: 0 !important; } input[type="number"], input[type=text], select, textarea { font-size: 18px; } input[type="number"] { width: 32px; } .lstIcontent { padding-left: 8px; } .revchkl { max-width: 183px; text-overflow: ellipsis; overflow-x: clip; } } ================================================ FILE: wled00/data/index.htm ================================================ WLED
Loading WLED UI...

Brightness


R

Files
PixelForge
Palettes

Color palette

Effect mode

Segments

Loading...

Transition:  s

Presets

Loading...

================================================ FILE: wled00/data/index.js ================================================ //page js var loc = false, locip, locproto = "http:"; var isOn = false, nlA = false, isLv = false, isInfo = false, isNodes = false, syncSend = false/*, syncTglRecv = true*/; var hasWhite = false, hasRGB = false, hasCCT = false, has2D = false; var nlDur = 60, nlTar = 0; var nlMode = false; var segLmax = 0; // size (in pixels) of largest selected segment var selectedFx = 0; var selectedPal = 0; var csel = 0; // selected color slot (0-2) var cpick; // iro color picker var currentPreset = -1; var lastUpdate = 0; var segCount = 0, ledCount = 0, lowestUnused = 0, maxSeg = 0, lSeg = 0; var pcMode = false, pcModeA = false, lastw = 0, wW; var simplifiedUI = false; var tr = 7; var d = document; const ranges = RangeTouch.setup('input[type="range"]', {}); var palettesData; var fxdata = []; var pJson = {}, eJson = {}, lJson = {}; var plJson = {}; // array of playlists var pN = "", pI = 0, pNum = 0; var pmt = 1, pmtLS = 0; var lastinfo = {}; var isM = false, mw = 0, mh=0; var ws, wsRpt=0; var cfg = { theme:{base:"dark", bg:{url:"", rnd: false, rndGrayscale: false, rndBlur: false}, alpha:{bg:0.6,tab:0.8}, color:{bg:""}}, comp :{colors:{picker: true, rgb: false, quick: true, hex: false}, labels:true, pcmbot:false, pid:true, seglen:false, segpwr:false, segexp:false, css:true, hdays:false, fxdef:true, on:0, off:0, idsort: false} }; // [year, month (0 -> January, 11 -> December), day, duration in days, image url] var hol = [ [0, 11, 24, 4, "https://aircoookie.github.io/xmas.png"], // christmas [0, 2, 17, 1, "https://images.alphacoders.com/491/491123.jpg"], // st. Patrick's day [2026, 3, 5, 2, "https://aircoookie.github.io/easter.png"], // easter 2026 [2027, 2, 28, 2, "https://aircoookie.github.io/easter.png"], // easter 2027 //[2028, 3, 16, 2, "https://aircoookie.github.io/easter.png"], // easter 2028 [0, 6, 4, 1, "https://images.alphacoders.com/516/516792.jpg"], // 4th of July [0, 0, 1, 1, "https://images.alphacoders.com/119/1198800.jpg"] // new year ]; // load iro.js sequentially to avoid 503 errors, retries until successful (function loadIro() { const l = d.createElement('script'); l.src = 'iro.js'; l.onload = () => { cpick = new iro.ColorPicker("#picker", { width: 260, wheelLightness: false, wheelAngle: 270, wheelDirection: "clockwise", layout: [{component: iro.ui.Wheel, options: {}}] }); d.readyState === 'complete' ? onLoad() : window.addEventListener('load', onLoad); }; l.onerror = () => setTimeout(loadIro, 100); document.head.appendChild(l); })(); function handleVisibilityChange() {if (!d.hidden && new Date () - lastUpdate > 3000) requestJson();} function sCol(na, col) {d.documentElement.style.setProperty(na, col);} function gId(c) {return d.getElementById(c);} function gEBCN(c) {return d.getElementsByClassName(c);} function isEmpty(o) {for (const i in o) return false; return true;} function isObj(i) {return (i && typeof i === 'object' && !Array.isArray(i));} function isNumeric(n) {return !isNaN(parseFloat(n)) && isFinite(n);} // returns true if dataset R, G & B values are 0 function isRgbBlack(a) {return (parseInt(a.r) == 0 && parseInt(a.g) == 0 && parseInt(a.b) == 0);} // returns RGB color from a given dataset function rgbStr(a) {return "rgb(" + a.r + "," + a.g + "," + a.b + ")";} // brightness approximation for selecting white as text color if background bri < 127, and black if higher function rgbBri(a) {return 0.2126*parseInt(a.r) + 0.7152*parseInt(a.g) + 0.0722*parseInt(a.b);} // sets background of color slot selectors function setCSL(cs) { let w = cs.dataset.w ? parseInt(cs.dataset.w) : 0; let hasShadow = getComputedStyle(cs).textShadow !== "none"; if (hasRGB && !isRgbBlack(cs.dataset)) { if (!hasShadow) cs.style.color = rgbBri(cs.dataset) > 127 ? "#000":"#fff"; // if text has no CSS "shadow" cs.style.background = (hasWhite && w > 0) ? `linear-gradient(180deg, ${rgbStr(cs.dataset)} 30%, rgb(${w},${w},${w}))` : rgbStr(cs.dataset); } else { if (hasRGB && !hasWhite) w = 0; cs.style.background = `rgb(${w},${w},${w})`; if (!hasShadow) cs.style.color = w > 127 ? "#000":"#fff"; } } function applyCfg() { cTheme(cfg.theme.base === "light"); var bg = cfg.theme.color.bg; if (bg) sCol('--c-1', bg); var l = cfg.comp.labels; sCol('--tbp', l ? "14px 14px 10px 14px":"10px 22px 4px 22px"); sCol('--bbp', l ? "9px 0 7px 0":"10px 0 4px 0"); sCol('--bhd', l ? "block":"none"); // show/hide labels sCol('--bmt', l ? "0px":"5px"); sCol('--t-b', cfg.theme.alpha.tab); sCol('--sgp', !cfg.comp.segpwr ? "block":"none"); // show/hide segment power size(); localStorage.setItem('wledUiCfg', JSON.stringify(cfg)); if (lastinfo.leds) updateUI(); // update component visibility } function tglHex() { cfg.comp.colors.hex = !cfg.comp.colors.hex; applyCfg(); } function tglTheme() { cfg.theme.base = (cfg.theme.base === "light") ? "dark":"light"; applyCfg(); } function tglLabels() { cfg.comp.labels = !cfg.comp.labels; applyCfg(); } function tglRgb() { cfg.comp.colors.rgb = !cfg.comp.colors.rgb; cfg.comp.colors.picker = !cfg.comp.colors.picker; applyCfg(); } function cTheme(light) { if (light) { sCol('--c-1','#eee'); sCol('--c-f','#000'); sCol('--c-2','#ddd'); sCol('--c-3','#bbb'); sCol('--c-4','#aaa'); sCol('--c-5','#999'); sCol('--c-6','#999'); sCol('--c-8','#888'); sCol('--c-b','#444'); sCol('--c-c','#333'); sCol('--c-e','#111'); sCol('--c-d','#222'); sCol('--c-r','#a21'); sCol('--c-g','#2a1'); sCol('--c-l','#26c'); sCol('--c-o','rgba(204, 204, 204, 0.9)'); sCol('--c-sb','#0003'); sCol('--c-sbh','#0006'); sCol('--c-tb','rgba(204, 204, 204, var(--t-b))'); sCol('--c-tba','rgba(170, 170, 170, var(--t-b))'); sCol('--c-tbh','rgba(204, 204, 204, var(--t-b))'); gId('imgw').style.filter = "invert(0.8)"; } else { sCol('--c-1','#111'); sCol('--c-f','#fff'); sCol('--c-2','#222'); sCol('--c-3','#333'); sCol('--c-4','#444'); sCol('--c-5','#555'); sCol('--c-6','#666'); sCol('--c-8','#888'); sCol('--c-b','#bbb'); sCol('--c-c','#ccc'); sCol('--c-e','#eee'); sCol('--c-d','#ddd'); sCol('--c-r','#e42'); sCol('--c-g','#4e2'); sCol('--c-l','#48a'); sCol('--c-o','rgba(34, 34, 34, 0.9)'); sCol('--c-sb','#fff3'); sCol('--c-sbh','#fff5'); sCol('--c-tb','rgba(34, 34, 34, var(--t-b))'); sCol('--c-tba','rgba(102, 102, 102, var(--t-b))'); sCol('--c-tbh','rgba(51, 51, 51, var(--t-b))'); gId('imgw').style.filter = "unset"; } } function loadBg() { const { url: iUrl, rnd: iRnd } = cfg.theme.bg; const bg = gId('bg'); const img = d.createElement("img"); img.src = iUrl; if (!iUrl || iRnd) { const today = new Date(); for (const holiday of (hol || [])) { const year = holiday[0] == 0 ? today.getFullYear() : holiday[0]; const holidayStart = new Date(year, holiday[1], holiday[2]); const holidayEnd = new Date(holidayStart); holidayEnd.setDate(holidayEnd.getDate() + holiday[3]); if (today >= holidayStart && today <= holidayEnd) img.src = holiday[4]; } } img.addEventListener('load', (e) => { var a = parseFloat(cfg.theme.alpha.bg); if (isNaN(a)) a = 0.6; bg.style.opacity = a; bg.style.backgroundImage = `url(${img.src})`; gId('namelabel').style.color = "var(--c-c)"; // improve namelabel legibility on background image }); } function loadSkinCSS(cId) { return new Promise((resolve, reject) => { if (gId(cId)) return resolve(); const l = d.createElement('link'); l.id = cId; l.rel = 'stylesheet'; l.href = getURL('/skin.css'); l.onload = resolve; l.onerror = reject; d.head.appendChild(l); }); } function getURL(path) { return (loc ? locproto + "//" + locip : "") + path; } function onLoad() { let l = window.location; if (l.protocol == "file:") { loc = true; locip = localStorage.getItem('locIp'); if (!locip) { locip = prompt("File Mode. Please enter WLED IP!"); localStorage.setItem('locIp', locip); } } else { // detect reverse proxy and/or HTTPS let pathn = l.pathname; let paths = pathn.slice(1,pathn.endsWith('/')?-1:undefined).split("/"); locproto = l.protocol; locip = l.hostname + (l.port ? ":" + l.port : ""); if (paths.length > 0 && paths[0]!=="") { loc = true; locip += "/" + paths.join('/'); } else if (locproto==="https:") { loc = true; } } var sett = localStorage.getItem('wledUiCfg'); if (sett) cfg = mergeDeep(cfg, JSON.parse(sett)); tooltip(); resetPUtil(); initFilters(); if (localStorage.getItem('pcm') == "true" || (!/Mobi/.test(navigator.userAgent) && localStorage.getItem('pcm') == null)) togglePcMode(true); applyCfg(); if (cfg.comp.hdays) { //load custom holiday list fetch(getURL("/holidays.json"), { // may be loaded from external source method: 'get' }) .then((res)=>{ //if (!res.ok) showErrorToast(); return res.json(); }) .then((json)=>{ if (Array.isArray(json)) hol = json; //TODO: do some parsing first }) .catch((e)=>{ console.log("No array of holidays in holidays.json. Defaults loaded."); }) .finally(()=>{ loadBg(); }); } else loadBg(); selectSlot(0); updateTablinks(0); handleLocationHash(); cpick.on("input:end", () => {setColor(1);}); cpick.on("color:change", () => {updatePSliders()}); pmtLS = localStorage.getItem('wledPmt'); // Load initial data sequentially, no parallel requests to avoid "503" errors when heap is low (slower but much more reliable) (async ()=>{ try { await loadPalettes(); // loads base palettes and builds #pallist (safe first) await loadFXData(); // loads fx data await loadFX(); // populates effect list await requestJson(); // updates info variables await loadPalettesData(); // fills palettesData[] for previews populatePalettes(); // repopulate with custom palettes now that cpalcount is known if(pmt == pmtLS) populatePresets(true); // load presets from localStorage if signature matches (i.e. no device reboot) else await loadPresets(); // load and populate presets if (cfg.comp.css) await loadSkinCSS('skinCss'); if (!ws) makeWS(); } catch(e) { showToast("Init failed: " + e, true); } })(); resetUtil(); d.addEventListener("visibilitychange", handleVisibilityChange, false); //size(); gId("cv").style.opacity=0; d.querySelectorAll('input[type="range"]').forEach((sl)=>{ sl.addEventListener('touchstart', toggleBubble); sl.addEventListener('touchend', toggleBubble); }); } function updateTablinks(tabI) { var tablinks = gEBCN("tablinks"); for (var i of tablinks) i.classList.remove('active'); tablinks[tabI].classList.add('active'); } function openTab(tabI, force = false) { if (pcMode && !force) return; iSlide = tabI; _C.classList.toggle('smooth', false); _C.style.setProperty('--i', iSlide); updateTablinks(tabI); switch (tabI) { case 0: window.location.hash = "Colors"; break; case 1: window.location.hash = "Effects"; break; case 2: window.location.hash = "Segments"; break; case 3: window.location.hash = "Presets"; break; } } function handleLocationHash() { switch (window.location.hash) { case "#Colors": openTab(0); break; case "#Effects": openTab(1); break; case "#Segments": openTab(2); break; case "#Presets": openTab(3); break; } } var timeout; function showToast(text, error = false) { var x = gId('toast'); //if (error) text += ''; x.innerHTML = text; x.classList.add(error ? 'error':'show'); clearTimeout(timeout); x.style.animation = 'none'; timeout = setTimeout(()=>{ x.classList.remove('show'); }, 2900); if (error) console.log(text); } function showErrorToast() { gId('connind').style.backgroundColor = "var(--c-r)"; showToast('Connection to light failed!', true); } function clearErrorToast(n=5000) { var x = gId('toast'); if (x.classList.contains('error')) { clearTimeout(timeout); timeout = setTimeout(()=>{ x.classList.remove('show'); x.classList.remove('error'); }, n); } } function getRuntimeStr(rt) { var t = parseInt(rt); var days = Math.floor(t/86400); var hrs = Math.floor((t - days*86400)/3600); var mins = Math.floor((t - days*86400 - hrs*3600)/60); var str = days ? (days + " " + (days == 1 ? "day" : "days") + ", ") : ""; str += (hrs || days) ? (hrs + " " + (hrs == 1 ? "hour" : "hours")) : ""; if (!days && hrs) str += ", "; if (t > 59 && !days) str += mins + " min"; if (t < 3600 && t > 59) str += ", "; if (t < 3600) str += (t - mins*60) + " sec"; return str; } function inforow(key, val, unit = "") { return `${key}${val}${unit}`; } function getLowestUnusedP() { var l = 1; for (var key in pJson) if (key == l) l++; if (l > 250) l = 250; return l; } function checkUsed(i) { var id = gId(`p${i}id`).value; if (pJson[id] && (i == 0 || id != i)) gId(`p${i}warn`).innerHTML = `⚠ Overwriting ${pName(id)}!`; else gId(`p${i}warn`).innerHTML = id>250?"⚠ ID must be 250 or less.":""; } function pName(i) { var n = "Preset " + i; if (pJson && pJson[i] && pJson[i].n) n = pJson[i].n; return n; } function isPlaylist(i) { if (isNumeric(i)) return pJson[i].playlist && pJson[i].playlist.ps; if (isObj(i)) return i.playlist && i.playlist.ps; return false; } function papiVal(i) { if (!pJson || !pJson[i]) return ""; var o = Object.assign({},pJson[i]); if (o.win) return o.win; delete o.n; delete o.p; delete o.ql; return JSON.stringify(o); } function qlName(i) { if (!pJson || !pJson[i] || !pJson[i].ql) return ""; return pJson[i].ql; } function cpBck() { var copyText = gId("bck"); copyText.select(); copyText.setSelectionRange(0, 999999); d.execCommand("copy"); showToast("Copied to clipboard!"); } function presetError(empty) { var hasBackup = false; var bckstr = ""; try { bckstr = localStorage.getItem("wledP"); if (bckstr.length > 10) hasBackup = true; } catch (e) {} var cn = `
`; if (empty) cn += `You have no presets yet!`; else cn += `Sorry, there was an issue loading your presets!`; if (hasBackup) { cn += `

`; if (empty) cn += `However, there is backup preset data of a previous installation available.
(Saving a preset will hide this and overwrite the backup)`; else cn += `Here is a backup of the last known good state:`; cn += `
`; cn += `
`; } cn += `
`; gId('pcont').innerHTML = cn; if (hasBackup) gId('bck').value = bckstr; } function restore(txt) { var req = new XMLHttpRequest(); req.addEventListener('load', function(){showToast(this.responseText,this.status >= 400)}); req.addEventListener('error', function(e){showToast(e.stack,true);}); req.open("POST", getURL("/upload")); var formData = new FormData(); var b = new Blob([txt], {type: "application/json"}); formData.append("data", b, '/presets.json'); req.send(formData); setTimeout(loadPresets, 2000); return false; } async function loadPresets() { return new Promise((resolve) => { fetch(getURL('/presets.json'), {method: 'get'}) .then(res => res.status=="404" ? {"0":{}} : res.json()) .then(json => { pJson = json; populatePresets(); resolve(); }) .catch(() => { presetError(false); resolve(); }) }); } async function loadPalettes(retry=0) { return new Promise((resolve) => { fetch(getURL('/json/palettes'), {method: 'get'}) .then(res => res.ok ? res.json() : Promise.reject()) .then(json => { lJson = Object.entries(json); populatePalettes(); resolve(); }) .catch((e) => { if (retry<5) { setTimeout(() => loadPalettes(retry+1).then(resolve), 100); } else { showToast(e, true); resolve(); } }); }); } async function loadFX(retry=0) { return new Promise((resolve) => { fetch(getURL('/json/effects'), {method: 'get'}) .then(res => res.ok ? res.json() : Promise.reject()) .then(json => { eJson = Object.entries(json); populateEffects(); resolve(); }) .catch((e) => { if (retry<5) { setTimeout(() => loadFX(retry+1).then(resolve), 100); } else { showToast(e, true); resolve(); } }); }); } async function loadFXData(retry=0) { return new Promise((resolve) => { fetch(getURL('/json/fxdata'), {method: 'get'}) .then(res => res.ok ? res.json() : Promise.reject()) .then(json => { fxdata = json||[]; fxdata.shift(); fxdata.unshift(";!;"); resolve(); }) .catch((e) => { fxdata = []; if (retry<5) { setTimeout(() => loadFXData(retry+1).then(resolve), 100); } else { showToast(e, true); resolve(); } }); }); } var pQL = []; function populateQL() { var cn = ""; if (pQL.length > 0) { pQL.sort((a,b) => (a[0]>b[0])); cn += `

Quick load

`; for (var key of (pQL||[])) { cn += ``; } gId('pql').classList.add('expanded'); } else gId('pql').classList.remove('expanded'); gId('pql').innerHTML = cn; } function populatePresets(fromls) { if (fromls) pJson = JSON.parse(localStorage.getItem("wledP")); if (!pJson) {loadPresets(); return;} // note: no await as this is a fallback that should not be needed as init function fetches pJson delete pJson["0"]; var cn = ""; var arr = Object.entries(pJson).sort(cmpP); pQL = []; var is = []; pNum = 0; for (var key of (arr||[])) { if (!isObj(key[1])) continue; let i = parseInt(key[0]); var qll = key[1].ql; if (qll) pQL.push([i, qll, pName(i)]); is.push(i); cn += `
`; if (cfg.comp.pid) cn += `
${i}
`; cn += `
${i==lastinfo.leds.bootps?"":""}${isPlaylist(i)?"":""}${pName(i)}
`; pNum++; } gId('pcont').innerHTML = cn; if (pNum > 0) { if (pmtLS != pmt && pmt != 0) { localStorage.setItem("wledPmt", pmt); pJson["0"] = {}; localStorage.setItem("wledP", JSON.stringify(pJson)); } pmtLS = pmt; } else { presetError(true); } updatePA(); populateQL(); } function parseInfo(i) { lastinfo = i; var name = i.name; gId('namelabel').innerHTML = name; if (!name.match(/[\u3040-\u30ff\u3400-\u4dbf\u4e00-\u9fff\uf900-\ufaff\uff66-\uff9f\u3131-\uD79D]/)) gId('namelabel').style.transform = "rotate(180deg)"; // rotate if no CJK characters if (name === "Dinnerbone") d.documentElement.style.transform = "rotate(180deg)"; // Minecraft easter egg if (i.live) name = "(Live) " + name; if (loc) name = "(L) " + name; d.title = name; simplifiedUI = i.simplifiedui; ledCount = i.leds.count; //syncTglRecv = i.str; maxSeg = i.leds.maxseg; pmt = i.fs.pmt; gId('buttonNodes').style.display = lastinfo.ndc > 0 ? null:"none"; // do we have a matrix set-up mw = i.leds.matrix ? i.leds.matrix.w : 0; mh = i.leds.matrix ? i.leds.matrix.h : 0; isM = mw>0 && mh>0; if (!isM) { gId("filter2D").classList.add('hide'); gId('bs').querySelectorAll('option[data-type="2D"]').forEach((o,i)=>{o.style.display='none';}); } else { gId("filter2D").classList.remove('hide'); gId('bs').querySelectorAll('option[data-type="2D"]').forEach((o,i)=>{o.style.display='';}); } gId("updBt").style.display = (i.opt & 1) ? '':'none'; // if (i.noaudio) { // gId("filterVol").classList.add("hide"); // gId("filterFreq").classList.add("hide"); // } // if (!i.u || !i.u.AudioReactive) { // gId("filterVol").classList.add("hide"); hideModes(" ♪"); // hide volume reactive effects // gId("filterFreq").classList.add("hide"); hideModes(" ♫"); // hide frequency reactive effects // } // Check for version upgrades on page load checkVersionUpgrade(i); } //https://stackoverflow.com/questions/2592092/executing-script-elements-inserted-with-innerhtml //var setInnerHTML = function(elm, html) { // elm.innerHTML = html; // Array.from(elm.querySelectorAll("script")).forEach( oldScript => { // const newScript = d.createElement("script"); // Array.from(oldScript.attributes) // .forEach( attr => newScript.setAttribute(attr.name, attr.value) ); // newScript.appendChild(d.createTextNode(oldScript.innerHTML)); // oldScript.parentNode.replaceChild(newScript, oldScript); // }); //} //setInnerHTML(obj, html); function populateInfo(i) { var cn=""; var pwr = i.leds.pwr; var pwru = "Not calculated"; if (pwr > 1000) {pwr /= 1000; pwr = pwr.toFixed((pwr > 10) ? 0 : 1); pwru = pwr + " A";} else if (pwr > 0) {pwr = 50 * Math.round(pwr/50); pwru = pwr + " mA";} var urows=""; if (i.u) { for (const [k, val] of Object.entries(i.u)) { if (val[1]) urows += inforow(k,val[0],val[1]); else urows += inforow(k,val); } } var vcn = "Kuuhaku"; if (i.cn) vcn = i.cn; cn += `v${i.ver} "${vcn}"${i.release ? '
('+i.release+')' : ''}

${urows} ${urows===""?'':''} ${i.opt&0x100?inforow("Debug",""):''} ${inforow("Build",i.vid)} ${inforow("Signal strength",i.wifi.signal +"% ("+ i.wifi.rssi, " dBm)")} ${inforow("Uptime",getRuntimeStr(i.uptime))} ${inforow("Time",i.time)} ${inforow("Free heap",(i.freeheap/1024).toFixed(1)," kB")} ${i.psram?inforow("Free PSRAM",(i.psram/1024).toFixed(1)," kB"):""} ${i.leds.count?inforow("Total LEDs",i.leds.count):""} ${inforow("Estimated current",pwru)} ${inforow("Average FPS",i.leds.fps)} ${inforow("MAC address",i.mac)} ${inforow("CPU clock",i.clock," MHz")} ${inforow("Flash size",i.flash," MB")} ${inforow("Filesystem",i.fs.u + "/" + i.fs.t + " kB (" +Math.round(i.fs.u*100/i.fs.t) + "%)")} ${inforow("Environment",i.arch + " " + i.core + ( i.lwip ? " (" + i.lwip + ")" : ""))} ${i.repo?inforow("GitHub","" + i.repo + ""):""}



`; gId('kv').innerHTML = cn; // update all sliders in Info d.querySelectorAll('#kv .sliderdisplay').forEach((sd,i) => { let s = sd.previousElementSibling; if (s) updateTrail(s); }); } function populateSegments(s) { var cn = ""; let li = lastinfo; segCount = 0; lowestUnused = 0; lSeg = 0; for (var inst of (s.seg||[])) { segCount++; let i = parseInt(inst.id); if (i == lowestUnused) lowestUnused = i+1; if (i > lSeg) lSeg = i; let sg = gId(`seg${i}`); let exp = sg ? (sg.classList.contains('expanded') || (i===0 && cfg.comp.segexp)) : false; // segment set icon color let cG = "var(--c-b)"; switch (inst.set) { case 1: cG = "var(--c-r)"; break; case 2: cG = "var(--c-g)"; break; case 3: cG = "var(--c-l)"; break; } let segp = `
`+ ``+ `
`+ ``+ `
`+ `
`+ `
`; let staX = inst.start; let stoX = inst.stop; let staY = inst.startY; let stoY = inst.stopY; let isMSeg = isM && staXReverse ${isM?'':'direction'}`; let miXck = ``; let rvYck = "", miYck =""; let smpl = simplifiedUI ? 'hide' : ''; if (isMSeg) { rvYck = ``; miYck = ``; } let map2D = `
Expand 1D FX
`+ `
`+ `
`; let blend = `
Blend mode
`+ `
`+ `
`; let sndSim = `
Sound sim
`+ `
`+ `
`; cn += `
`+ ``+ `
`+ `&#x${inst.frz ? (li.live && li.liveseg==i?'e410':'e0e8') : 'e325'};`+ (inst.n ? inst.n : "Segment "+i) + `
`+ `ɸ${String.fromCharCode(inst.set+"A".charCodeAt(0))};`+ `
`+ `
`+ ``+ `
`+ ``+ (cfg.comp.segpwr ? segp : '') + `
`+ ``+ ``+ ``+ ``+ ``+ ``+ ``+ ``+ ``+ ``+ ``+ ``+ (isMSeg ? ''+ ''+ ''+ ''+ ''+ '' : '') + ``+ ``+ ``+ ``+ ``+ ``+ ``+ ``+ ``+ ``+ `
${isMSeg?'Start X':'Start LED'}${isMSeg?(cfg.comp.seglen?"Width":"Stop X"):(cfg.comp.seglen?"LED count":"Stop LED")}${isMSeg?'':'Offset'}
${isMSeg?miXck+'
'+rvXck:''}
Start Y'+(cfg.comp.seglen?'Height':'Stop Y')+'
'+miYck+'
'+rvYck+'
GroupingSpacing
`+ `
`+ blend + (!isMSeg ? rvXck : '') + (isMSeg&&stoY-staY>1&&stoX-staX>1 ? map2D : '') + (s.AudioReactive && s.AudioReactive.on ? "" : sndSim) + ``+ `
`+ ``+ ``+ `
`+ `
`+ (cfg.comp.segpwr ? '' : segp) + `
`; } gId('segcont').innerHTML = cn; gId("segcont").classList.remove("hide"); let noNewSegs = (lowestUnused >= maxSeg); resetUtil(noNewSegs); if (segCount === 0) return; // no segments to populate for (var i = 0; i <= lSeg; i++) { if (!gId(`seg${i}`)) continue; updateLen(i); updateTrail(gId(`seg${i}bri`)); gId(`segr${i}`).classList.add("hide"); } if (segCount < 2) { gId(`segd${lSeg}`).classList.add("hide"); // hide delete if only one segment if (parseInt(gId("seg0bri").value)==255) gId(`segp0`).classList.add("hide"); // hide segment controls if there is only one segment in simplified UI if (simplifiedUI) gId("segcont").classList.add("hide"); } if (!isM && !noNewSegs && (cfg.comp.seglen?parseInt(gId(`seg${lSeg}s`).value):0)+parseInt(gId(`seg${lSeg}e`).value) 1) ? "block":"none"; // rsbtn parent if (Array.isArray(li.maps) && li.maps.length>1) { let cont = `Ledmap: "; gId("ledmap").innerHTML = cont; gId("ledmap").classList.remove('hide'); } else { gId("ledmap").classList.add('hide'); } tooltip("#Segments"); } function populateEffects() { var effects = eJson; var html = ""; effects.shift(); // temporary remove solid for (let i = 0; i < effects.length; i++) { effects[i] = { id: effects[i][0], name:effects[i][1] }; } effects.sort((a,b) => (a.name).localeCompare(b.name)); effects.unshift({ "id": 0, "name": "Solid" }); for (let ef of effects) { // add slider and color control to setFX (used by requestjson) let id = ef.id; let nm = ef.name+" "; let fd = ""; if (ef.name.indexOf("RSVD") < 0) { if (Array.isArray(fxdata) && fxdata.length>id) { if (fxdata[id].length==0) fd = ";;!;1" else fd = fxdata[id]; let eP = (fd == '')?[]:fd.split(";"); // effect parameters let p = (eP.length<3 || eP[2]==='')?[]:eP[2].split(","); // palette data if (p.length>0 && (p[0] !== "" && !isNumeric(p[0]))) nm += "🎨"; // effects using palette let m = (eP.length<4 || eP[3]==='')?'1':eP[3]; // flags if (id == 0) m = ''; // solid has no flags if (m.length>0) { if (m.includes('0')) nm += "•"; // 0D effects (PWM & On/Off) if (m.includes('1')) nm += "⋮"; // 1D effects if (m.includes('2')) nm += "▦"; // 2D effects if (m.includes('v')) nm += "♪"; // volume effects if (m.includes('f')) nm += "♫"; // frequency effects } } html += generateListItemHtml('fx',id,nm,'setFX','',fd); } } gId('fxlist').innerHTML=html; } function populatePalettes() { lJson.shift(); // temporary remove default lJson.sort((a,b) => (a[1]).localeCompare(b[1])); lJson.unshift([0,"Default"]); var html = ""; for (let pa of lJson) { html += generateListItemHtml( 'palette', pa[0], pa[1], 'setPalette', `
` ); } gId('pallist').innerHTML=html; // append custom palettes (when loading for the 1st time) let li = lastinfo; if (!isEmpty(li) && li.cpalcount) { for (let j = 0; j` ); } } } function redrawPalPrev() { d.querySelectorAll('#pallist .lstI').forEach((pal,i) =>{ let lP = pal.querySelector('.lstIprev'); if (lP) { lP.style = genPalPrevCss(pal.dataset.id); } }); } function genPalPrevCss(id) { if (!palettesData) return; var paletteData = palettesData[id]; if (!paletteData) return 'display: none'; // We need at least two colors for a gradient if (paletteData.length == 1) { paletteData[1] = paletteData[0]; if (Array.isArray(paletteData[1])) { paletteData[1][0] = 255; } } var gradient = []; paletteData.forEach((e,j) => { let r, g, b; let index = false; if (Array.isArray(e)) { index = Math.round(e[0]/255*100); r = e[1]; g = e[2]; b = e[3]; } else if (e == 'r') { r = Math.random() * 255; g = Math.random() * 255; b = Math.random() * 255; } else { let i = e[1] - 1; var cd = gId('csl').children; r = parseInt(cd[i].dataset.r); g = parseInt(cd[i].dataset.g); b = parseInt(cd[i].dataset.b); } if (index === false) { index = Math.round(j / paletteData.length * 100); } gradient.push(`rgb(${r},${g},${b}) ${index}%`); }); return `background: linear-gradient(to right,${gradient.join()});`; } function generateListItemHtml(listName, id, name, clickAction, extraHtml = '', effectPar = '') { return `
`+ ``+ extraHtml + `
`; } function btype(b) { switch (b) { case 2: case 32: return "ESP32"; case 3: case 33: return "ESP32-S2"; case 4: case 34: return "ESP32-S3"; case 5: case 35: return "ESP32-C3"; case 39: return "ESP32-C6"; case 40: return "ESP32-C61"; case 41: return "ESP32-C5"; case 42: case 43: return "ESP32-P4"; case 1: case 82: return "ESP8266"; } return "?"; } function bname(o) { if (o.name=="WLED") return o.ip; return o.name; } function populateNodes(i,n) { var cn=""; var urows=""; var nnodes = 0; if (n.nodes) { n.nodes.sort((a,b) => (a.name).localeCompare(b.name)); for (var o of n.nodes) { if (o.name) { let onoff = ``; var url = `${bname(o)}${o.vid<2307130?'':onoff}`; urows += inforow(url,`${btype(o.type&0x7F)}
${o.vid==0?"N/A":o.vid}`); nnodes++; } } } if (i.ndc < 0) cn += `Instance List is disabled.`; else if (nnodes == 0) cn += `No other instances found.`; cn += ` ${inforow("Current instance:",i.name)} ${urows}
`; gId('kn').innerHTML = cn; } function loadNodes() { fetch(getURL('/json/nodes'), { method: 'get' }) .then((res)=>{ if (!res.ok) showToast('Could not load Node list!', true); return res.json(); }) .then((json)=>{ clearErrorToast(100); populateNodes(lastinfo, json); }) .catch((e)=>{ showToast(e, true); }); } // update the 'sliderdisplay' background div of a slider for a visual indication of slider position function updateTrail(e) { if (e==null) return; let sd = e.parentNode.getElementsByClassName('sliderdisplay')[0]; if (sd && getComputedStyle(sd).getPropertyValue("--bg").trim() !== "none") { // trim() for Safari var max = e.hasAttribute('max') ? e.attributes.max.value : 255; var perc = Math.round(e.value * 100 / max); if (perc < 50) perc += 2; var val = `linear-gradient(90deg, var(--bg) ${perc}%, var(--c-6) ${perc}%)`; sd.style.backgroundImage = val; } var b = e.parentNode.parentNode.getElementsByTagName('output')[0]; if (b) b.innerHTML = e.value; } // rangetouch slider function function toggleBubble(e) { var b = e.target.parentNode.parentNode.getElementsByTagName('output')[0]; b.classList.toggle('sliderbubbleshow'); } // updates segment length upon input of segment values function updateLen(s) { if (!gId(`seg${s}s`)) return; var start = parseInt(gId(`seg${s}s`).value); var stop = parseInt(gId(`seg${s}e`).value) + (cfg.comp.seglen?start:0); var len = stop - start; let sY = gId(`seg${s}sY`); let eY = gId(`seg${s}eY`); let sX = gId(`seg${s}s`); let eX = gId(`seg${s}e`); let of = gId(`seg${s}of`); let mySH = gId("mkSYH"); let mySD = gId("mkSYD"); if (isM) { // do we have 1D segment *after* the matrix? if (start >= mw*mh) { if (sY) { sY.value = 0; sY.max = 0; sY.min = 0; } if (eY) { eY.value = 1; eY.max = 1; eY.min = 0; } sX.min = mw*mh; sX.max = ledCount-1; eX.min = mw*mh+1; eX.max = ledCount; if (mySH) mySH.classList.add("hide"); if (mySD) mySD.classList.add("hide"); if (of) of.classList.remove("hide"); } else { // matrix setup if (mySH) mySH.classList.remove("hide"); if (mySD) mySD.classList.remove("hide"); if (of) of.classList.add("hide"); let startY = parseInt(sY.value); let stopY = parseInt(eY.value) + (cfg.comp.seglen?startY:0); len *= (stopY-startY); let tPL = gId(`seg${s}lbtm`); if (stop-start>1 && stopY-startY>1) { // 2D segment if (tPL) tPL.classList.remove('hide'); // unhide transpose checkbox let sE = gId('fxlist').querySelector(`.lstI[data-id="${selectedFx}"]`); if (sE) { let sN = sE.querySelector(".lstIname").innerText; let seg = gId(`seg${s}map2D`); if (seg) { if(sN.indexOf("\u25A6")<0) seg.classList.remove('hide'); // unhide mapping for 1D effects (| in name) else seg.classList.add('hide'); // hide mapping otherwise } } } else { // 1D segment in 2D set-up if (tPL) { tPL.classList.add('hide'); // hide transpose checkbox gId(`seg${s}tp`).checked = false; // and uncheck it } } } } var out = "(delete)"; if (len > 1) { out = `${len} LEDs`; } else if (len == 1) { out = "1 LED"; } if (gId(`seg${s}grp`) != null) { var grp = parseInt(gId(`seg${s}grp`).value); var spc = parseInt(gId(`seg${s}spc`).value); if (grp == 0) grp = 1; var virt = Math.ceil(len/(grp + spc)); if (!isNaN(virt) && (grp > 1 || spc > 0)) out += ` (${virt} virtual)`; } if (isM && start >= mw*mh) out += " [strip]"; gId(`seg${s}len`).innerHTML = out; } // updates background color of currently selected preset function updatePA() { let ps; ps = gEBCN("pres"); for (let p of ps) p.classList.remove('selected'); ps = gEBCN("psts"); for (let p of ps) p.classList.remove('selected'); if (currentPreset > 0) { var acv = gId(`p${currentPreset}o`); if (acv /*&& !acv.classList.contains('expanded')*/) { acv.classList.add('selected'); /* // scroll selected preset into view (on WS refresh) acv.scrollIntoView({ behavior: 'smooth', block: 'center' }); */ } acv = gId(`p${currentPreset}qlb`); if (acv) acv.classList.add('selected'); } } function updateUI() { gId('buttonPower').className = (isOn) ? 'active':''; gId('buttonNl').className = (nlA) ? 'active':''; gId('buttonSync').className = (syncSend) ? 'active':''; updateSelectedFx(); updateSelectedPalette(selectedPal); // must be after updateSelectedFx() to un-hide color slots for * palettes updateTrail(gId('sliderBri')); updateTrail(gId('sliderSpeed')); updateTrail(gId('sliderIntensity')); updateTrail(gId('sliderC1')); updateTrail(gId('sliderC2')); updateTrail(gId('sliderC3')); if (hasRGB) { updateTrail(gId('sliderR')); updateTrail(gId('sliderG')); updateTrail(gId('sliderB')); } if (hasWhite) updateTrail(gId('sliderW')); var ccfg = cfg.comp.colors; gId('wwrap').style.display = (hasWhite) ? "block":"none"; // white channel gId('wbal').style.display = (hasCCT) ? "block":"none"; // white balance gId('hexw').style.display = (ccfg.hex) ? "block":"none"; // HEX input gId('picker').style.display = (hasRGB && ccfg.picker) ? "block":"none"; // color picker wheel gId('hwrap').style.display = (hasRGB && !ccfg.picker) ? "block":"none"; // hue slider gId('swrap').style.display = (hasRGB && !ccfg.picker) ? "block":"none"; // saturation slider gId('vwrap').style.display = (hasRGB) ? "block":"none"; // brightness (value) slider gId('kwrap').style.display = (hasRGB && !hasCCT) ? "block":"none"; // Kelvin slider gId('rgbwrap').style.display = (hasRGB && ccfg.rgb) ? "block":"none"; // RGB sliders gId('qcs-w').style.display = (hasRGB && ccfg.quick) ? "block":"none"; // quick selection //gId('csl').style.display = (hasRGB || hasWhite) ? "block":"none"; // color selectors (hide for On/Off bus) //gId('palw').style.display = (hasRGB) ? "inline-block":"none"; // palettes are shown/hidden in setEffectParameters() updatePA(); updatePSliders(); } function updateSelectedPalette(s) { var parent = gId('pallist'); var selPaletteInput = parent.querySelector(`input[name="palette"][value="${s}"]`); if (selPaletteInput) selPaletteInput.checked = true; var selElement = parent.querySelector('.selected'); if (selElement) selElement.classList.remove('selected'); var selectedPalette = parent.querySelector(`.lstI[data-id="${s}"]`); if (!selectedPalette) return; // palette not yet loaded (custom palette on initial load) selectedPalette.classList.add('selected'); // Display selected palette name on button in simplified UI let selectedName = selectedPalette.querySelector(".lstIname").innerText; if (simplifiedUI) { gId("palwbtn").innerText = "Palette: " + selectedName; } // in case of special palettes (* Colors...), force show color selectors (if hidden by effect data) let cd = gId('csl').children; // color selectors if (s > 1 && s < 6) { cd[0].classList.remove('hide'); // * Color 1 if (s > 2) cd[1].classList.remove('hide'); // * Color 1 & 2 if (s > 3) cd[2].classList.remove('hide'); // all colors } else { for (let i of cd) if (i.dataset.hide == '1') i.classList.add('hide'); } } function updateSelectedFx() { var parent = gId('fxlist'); var selEffectInput = parent.querySelector(`input[name="fx"][value="${selectedFx}"]`); if (selEffectInput) selEffectInput.checked = true; var selElement = parent.querySelector('.selected'); if (selElement) { selElement.classList.remove('selected'); selElement.style.bottom = null; // remove element style added in slider handling } var selectedEffect = parent.querySelector(`.lstI[data-id="${selectedFx}"]`); if (selectedEffect) { selectedEffect.classList.add('selected'); setEffectParameters(selectedFx); // hide non-0D effects if segment only has 1 pixel (0D) parent.querySelectorAll('.lstI').forEach((fx)=>{ let ds = fx.dataset; if (ds.opt) { let opts = ds.opt.split(";"); if (ds.id>0) { if (segLmax==0) fx.classList.add('hide'); // none of the segments selected (hide all effects) else { if ((segLmax==1 && (!opts[3] || opts[3].indexOf("0")<0)) || (!has2D && opts[3] && ((opts[3].indexOf("2")>=0 && opts[3].indexOf("1")<0)))) fx.classList.add('hide'); else fx.classList.remove('hide'); } } } }); var selectedName = selectedEffect.querySelector(".lstIname").innerText; // Display selected effect name on button in simplified UI let selectedNameOnlyAscii = selectedName.replace(/[^\x00-\x7F]/g, ""); if (simplifiedUI) { gId("fxbtn").innerText = "Effect: " + selectedNameOnlyAscii; } // hide 2D mapping and/or sound simulation options gId("segcont").querySelectorAll(`div[data-map="map2D"]`).forEach((seg)=>{ if (selectedName.indexOf("\u25A6")<0) seg.classList.remove('hide'); else seg.classList.add('hide'); }); gId("segcont").querySelectorAll(`div[data-snd="si"]`).forEach((seg)=>{ if (selectedName.indexOf("\u266A")<0 && selectedName.indexOf("\u266B")<0) seg.classList.add('hide'); else seg.classList.remove('hide'); // also "♫ "? }); } } function displayRover(i,s) { gId('rover').style.transform = (i.live && s.lor == 0 && i.liveseg<0) ? "translateY(0px)":"translateY(100%)"; var sour = i.lip ? i.lip:""; if (sour.length > 2) sour = " from " + sour; gId('lv').innerHTML = `WLED is receiving live ${i.lm} data${sour}`; gId('roverstar').style.display = (i.live && s.lor) ? "block":"none"; } function cmpP(a, b) { if (cfg.comp.idsort || !a[1].n) return (parseInt(a[0]) > parseInt(b[0])); // sort playlists first, followed by presets with characters and last presets with special 1st character const c = a[1].n.charCodeAt(0); const d = b[1].n.charCodeAt(0); if ((c>47 && c<58) || (c>64 && c<91) || (c>96 && c<123) || c>255) x = '='; else x = '>'; if ((d>47 && d<58) || (d>64 && d<91) || (d>96 && d<123) || d>255) y = '='; else y = '>'; const n = (a[1].playlist ? '<' : x) + a[1].n; return n.localeCompare((b[1].playlist ? '<' : y) + b[1].n, undefined, {numeric: true}); } function makeWS() { if (ws || lastinfo.ws < 0) return; let url = loc ? getURL('/ws').replace("http","ws") : "ws://"+window.location.hostname+"/ws"; ws = new WebSocket(url); ws.binaryType = "arraybuffer"; ws.onmessage = (e)=>{ if (e.data instanceof ArrayBuffer) return; // liveview packet var json = JSON.parse(e.data); if (json.leds) return; // JSON liveview packet clearTimeout(jsonTimeout); jsonTimeout = null; lastUpdate = new Date(); clearErrorToast(); gId('connind').style.backgroundColor = "var(--c-l)"; // json object should contain json.info AND json.state (but may not) var i = json.info; if (i) { parseInfo(i); if (isInfo) populateInfo(i); } else i = lastinfo; var s = json.state ? json.state : json; displayRover(i, s); readState(s); }; ws.onclose = (e)=>{ gId('connind').style.backgroundColor = "var(--c-r)"; if (wsRpt++ < 10) setTimeout(makeWS,wsRpt * 200); // retry WS connection ws = null; } ws.onopen = (e)=>{ //ws.send("{'v':true}"); // unnecessary (https://github.com/wled/WLED/blob/master/wled00/ws.cpp#L18) wsRpt = 0; reqsLegal = true; } } function readState(s,command=false) { if (!s) return false; if (s.success) return true; // no data to process isOn = s.on; gId('sliderBri').value = s.bri; nlA = s.nl.on; nlDur = s.nl.dur; nlTar = s.nl.tbri; nlFade = s.nl.fade; syncSend = s.udpn.send; if (s.pl<0) currentPreset = s.ps; else currentPreset = s.pl; tr = s.transition; gId('tt').value = tr/10; gId('bs').value = s.bs || 0; if (tr===0) gId('bsp').classList.add('hide') else gId('bsp').classList.remove('hide') populateSegments(s); hasRGB = hasWhite = hasCCT = has2D = false; segLmax = 0; // reset max selected segment length let i = {}; // determine light capabilities from selected segments for (let seg of (s.seg||[])) { let w = (seg.stop - seg.start); let h = seg.stopY ? (seg.stopY - seg.startY) : 1; let lc = seg.lc; if (w*h > segLmax) segLmax = w*h; if (seg.sel) { if (isEmpty(i) || (i.id == s.mainseg && !i.sel)) i = seg; // get first selected segment (and replace mainseg if it is not selected) hasRGB |= !!(lc & 0x01); hasWhite |= !!(lc & 0x02); hasCCT |= !!(lc & 0x04); has2D |= w > 1 && h > 1; } else if (isEmpty(i) && seg.id == s.mainseg) i = seg; // assign mainseg if no segments are selected } if (isEmpty(i)) { showToast('No segments!', true); updateUI(); return true; } else if (i.id == s.mainseg) { // fallback if no segments are selected but we have mainseg hasRGB |= !!(i.lc & 0x01); hasWhite |= !!(i.lc & 0x02); hasCCT |= !!(i.lc & 0x04); has2D |= (i.stop - i.start) > 1 && (i.stopY ? (i.stopY - i.startY) : 1) > 1; } var cd = gId('csl').querySelectorAll("button"); for (let e = cd.length-1; e >= 0; e--) { cd[e].dataset.r = i.col[e][0]; cd[e].dataset.g = i.col[e][1]; cd[e].dataset.b = i.col[e][2]; if (hasWhite || (!hasRGB && !hasWhite)) { cd[e].dataset.w = i.col[e][3]; } setCSL(cd[e]); } selectSlot(csel); if (i.cct != null && i.cct>=0) gId("sliderA").value = i.cct; gId('sliderSpeed').value = i.sx; gId('sliderIntensity').value = i.ix; gId('sliderC1').value = i.c1 ? i.c1 : 0; gId('sliderC2').value = i.c2 ? i.c2 : 0; gId('sliderC3').value = i.c3 ? i.c3 : 0; gId('checkO1').checked = !(!i.o1); gId('checkO2').checked = !(!i.o2); gId('checkO3').checked = !(!i.o3); if (s.error && s.error != 0) { var errstr = ""; switch (s.error) { case 1: errstr = "Denied!"; break; case 3: errstr = "Buffer locked!"; break; case 7: errstr = "No RAM for buffer!"; break; case 8: errstr = "Effect RAM depleted!"; break; case 9: errstr = "JSON parsing error!"; break; case 10: errstr = "Could not mount filesystem!"; break; case 11: errstr = "Not enough space to save preset!"; break; case 12: errstr = "Preset not found."; break; case 13: errstr = "Missing ir.json."; break; case 19: errstr = "A filesystem error has occured."; break; } showToast('Error ' + s.error + ": " + errstr, true); } selectedPal = i.pal; selectedFx = i.fx; redrawPalPrev(); // if any color changed (random palette did at least) updateUI(); return true; } // control HTML elements for Slider and Color Control (original ported form WLED-SR) // Technical notes // =============== // If an effect name is followed by an @, slider and color control is effective. // If not effective then: // - For AC effects (id<128) 2 sliders and 3 colors and the palette will be shown // - For SR effects (id>128) 5 sliders and 3 colors and the palette will be shown // If effective (@) // - a ; separates slider controls (left) from color controls (middle) and palette control (right) // - if left, middle or right is empty no controls are shown // - a , separates slider controls (max 5) or color controls (max 3). Palette has only one value // - a ! means that the default is used. // - For sliders: Effect speeds, Effect intensity, Custom 1, Custom 2, Custom 3 // - For colors: Fx color, Background color, Custom // - For palette: prompt for color palette OR palette ID if numeric (will hide palette selection) // // Note: If palette is on and no colors are specified 1,2 and 3 is shown in each color circle. // If a color is specified, the 1,2 or 3 is replaced by that specification. // Note: Effects can override default pattern behaviour // - FadeToBlack can override the background setting // - Defining SEGCOL() can override a specific palette using these values (e.g. Color Gradient) function setEffectParameters(idx) { if (!(Array.isArray(fxdata) && fxdata.length>idx)) return; var controlDefined = fxdata[idx].length; var effectPar = fxdata[idx]; var effectPars = (effectPar == '')?[]:effectPar.split(";"); var slOnOff = (effectPars.length==0 || effectPars[0]=='')?[]:effectPars[0].split(","); var coOnOff = (effectPars.length<2 || effectPars[1]=='')?[]:effectPars[1].split(","); var paOnOff = (effectPars.length<3 || effectPars[2]=='')?[]:effectPars[2].split(","); // set html slider items on/off d.querySelectorAll("#sliders .sliderwrap").forEach((slider, i)=>{ let text = slider.getAttribute("title"); if ((!controlDefined && i<((idx<128)?2:nSliders)) || (slOnOff.length>i && slOnOff[i]!="")) { if (slOnOff.length>i && slOnOff[i]!="!") text = slOnOff[i]; // restore overwritten default tooltips if (i<2 && slOnOff[i]==="!") text = i==0 ? "Effect speed" : "Effect intensity"; slider.setAttribute("title", text); slider.parentElement.classList.remove('hide'); } else slider.parentElement.classList.add('hide'); }); if (slOnOff.length > 5) { // up to 3 checkboxes gId('fxopt').classList.remove('fade'); d.querySelectorAll("#sliders .ochkl").forEach((check, i)=>{ let text = check.getAttribute("title"); if (5+i5+i && slOnOff[5+i]!="!") text = slOnOff[5+i]; check.setAttribute("title", text); check.classList.remove('hide'); } else check.classList.add('hide'); }); } else gId('fxopt').classList.add('fade'); // set the bottom position of selected effect (sticky) as the top of sliders div function setSelectedEffectPosition() { if (simplifiedUI) return; let top = parseInt(getComputedStyle(gId("sliders")).height); top += 5; let sel = d.querySelector('#fxlist .selected'); if (sel) sel.style.bottom = top + "px"; // we will need to remove this when unselected (in setFX()) } setSelectedEffectPosition(); setInterval(setSelectedEffectPosition,750); // set html color items on/off var cslLabel = ''; var sep = ''; var cslCnt = 0, oCsel = csel; d.querySelectorAll("#csl button").forEach((e,i)=>{ var btn = gId("csl" + i); // if no controlDefined or coOnOff has a value if (coOnOff.length>i && coOnOff[i] != "") { btn.classList.remove('hide'); btn.dataset.hide = 0; if (coOnOff[i] != "!") { var abbreviation = coOnOff[i].substr(0,2); btn.innerHTML = abbreviation; if (abbreviation != coOnOff[i]) { cslLabel += sep + abbreviation + '=' + coOnOff[i]; sep = ', '; } } else if (i==0) btn.innerHTML = "Fx"; else if (i==1) btn.innerHTML = "Bg"; else btn.innerHTML = "Cs"; if (!cslCnt || oCsel==i) selectSlot(i); // select 1st displayed slot or old one cslCnt++; } else if (!controlDefined) { // if no controls then all buttons should be shown for color 1..3 btn.classList.remove('hide'); btn.dataset.hide = 0; btn.innerHTML = `${i+1}`; if (!cslCnt || oCsel==i) selectSlot(i); // select 1st displayed slot or old one cslCnt++; } else { btn.classList.add('hide'); btn.dataset.hide = 1; btn.innerHTML = `${i+1}`; // name hidden buttons 1..3 for * palettes } }); gId("cslLabel").innerHTML = cslLabel; if (cslLabel!=="") gId("cslLabel").classList.remove("hide"); else gId("cslLabel").classList.add("hide"); // set palette on/off var palw = gId("palw"); // wrapper var pall = gId("pall"); // label var icon = ' '; var text = 'Color palette'; // if not controlDefined or palette has a value if (hasRGB && ((!controlDefined) || (paOnOff.length>0 && paOnOff[0]!="" && isNaN(paOnOff[0])))) { palw.style.display = "inline-block"; if (paOnOff.length>0 && paOnOff[0].indexOf("=")>0) { // embeded default values var dPos = paOnOff[0].indexOf("="); var v = Math.max(0,Math.min(255,parseInt(paOnOff[0].substr(dPos+1)))); paOnOff[0] = paOnOff[0].substring(0,dPos); } if (paOnOff.length>0 && paOnOff[0] != "!") text = paOnOff[0]; } else { // disable palette list text += ' not used'; palw.style.display = "none"; // Close palette dialog if not available if (palw.lastElementChild.tagName == "DIALOG") { palw.lastElementChild.close(); } } pall.innerHTML = icon + text; // not all color selectors shown, hide palettes created from color selectors // NOTE: this will disallow user to select "* Color ..." palettes which may be undesirable in some cases or for some users //for (let e of (gId('pallist').querySelectorAll('.lstI')||[])) { // let fltr = "* C"; // if (cslCnt==1 && csel==0) fltr = "* Colors"; // else if (cslCnt==2) fltr = "* Colors Only"; // if (cslCnt < 3 && e.querySelector('.lstIname').innerText.indexOf(fltr)>=0) e.classList.add('hide'); else e.classList.remove('hide'); //} } var jsonTimeout; var reqsLegal = false; async function requestJson(command=null, retry=0) { return new Promise((resolve, reject) => { gId('connind').style.backgroundColor = "var(--c-y)"; if (command && !reqsLegal) {resolve(); return;} if (!jsonTimeout) jsonTimeout = setTimeout(()=>{if (ws) ws.close(); ws=null; showErrorToast()}, 3000); var useWs = (ws && ws.readyState === WebSocket.OPEN); var req = null; if (command) { command.v = true; command.time = Math.floor(Date.now() / 1000); var t = gId('tt'); if (t && t.validity.valid && command.transition==null) { var tn = parseInt(t.value*10); if (tn != tr) command.transition = tn; } req = JSON.stringify(command); if (req.length > 1340) useWs = false; if (req.length > 500 && lastinfo && lastinfo.arch == "esp8266") useWs = false; } if (useWs) { ws.send(req?req:'{"v":true}'); resolve(); return; } fetch(getURL('/json/si'), { method: command ? 'post' : 'get', headers: {"Content-Type": "application/json; charset=UTF-8"}, body: req }) .then(res => { clearTimeout(jsonTimeout); jsonTimeout = null; return res.ok ? res.json() : Promise.reject(); }) .then(json => { lastUpdate = new Date(); clearErrorToast(3000); gId('connind').style.backgroundColor = "var(--c-g)"; if (!json) { showToast('Empty response', true); resolve(); return; } if (json.success) {resolve(); return;} if (json.info) { parseInfo(json.info); if (isInfo) populateInfo(json.info); if (simplifiedUI) simplifyUI(); } var s = json.state ? json.state : json; readState(s); reqsLegal = true; resolve(); }) .catch((e)=>{ if (retry<10) { setTimeout(() => requestJson(command,retry+1).then(resolve).catch(reject), retry*50); } else { showToast(e, true); resolve(); } }); }); } function togglePower() { isOn = !isOn; var obj = {"on": isOn}; if (isOn && lastinfo && lastinfo.live && lastinfo.liveseg>=0) { obj.live = false; obj.seg = []; obj.seg[0] = {"id": lastinfo.liveseg, "frz": false}; } if (cfg.comp.on >0 && isOn) obj = {"ps": cfg.comp.on }; // don't use setPreset() if (cfg.comp.off>0 && !isOn) obj = {"ps": cfg.comp.off}; // don't use setPreset() requestJson(obj); } function toggleNl() { nlA = !nlA; if (nlA) { showToast(`Timer active. Your light will turn ${nlTar > 0 ? "on":"off"} ${nlMode ? "over":"after"} ${nlDur} minutes.`); } else { showToast('Timer deactivated.'); } var obj = {"nl": {"on": nlA}}; requestJson(obj); } function toggleSync() { syncSend = !syncSend; if (syncSend) showToast('Other lights in the network will now sync to this one.'); else showToast('This light and other lights in the network will no longer sync.'); var obj = {"udpn": {"send": syncSend}}; //if (syncTglRecv) obj.udpn.recv = syncSend; requestJson(obj); } function toggleLiveview() { if (isInfo && isM) toggleInfo(); if (isNodes && isM) toggleNodes(); isLv = !isLv; let wsOn = ws && ws.readyState === WebSocket.OPEN; var lvID = "liveview"; if (isM && wsOn) { lvID += "2D"; if (isLv) gId('klv2D').innerHTML = ``; gId('mlv2D').style.transform = (isLv) ? "translateY(0px)":"translateY(100%)"; } gId(lvID).style.display = (isLv) ? "block":"none"; gId(lvID).src = (isLv) ? getURL("/" + lvID + ((wsOn) ? "?ws":"")):"about:blank"; gId('buttonSr').classList.toggle("active"); if (!isLv && wsOn) ws.send('{"lv":false}'); size(); } function toggleInfo() { if (isNodes) toggleNodes(); if (isLv && isM) toggleLiveview(); isInfo = !isInfo; if (isInfo) requestJson(); gId('info').style.transform = (isInfo) ? "translateY(0px)":"translateY(100%)"; gId('buttonI').className = (isInfo) ? "active":""; } function toggleNodes() { if (isInfo) toggleInfo(); if (isLv && isM) toggleLiveview(); isNodes = !isNodes; if (isNodes) loadNodes(); gId('nodes').style.transform = (isNodes) ? "translateY(0px)":"translateY(100%)"; gId('buttonNodes').className = (isNodes) ? "active":""; } function makeSeg() { var ns = 0, ct = isM ? mw : ledCount; var lu = lowestUnused; let li = lastinfo; if (lu > 0) { let xend = parseInt(gId(`seg${lu -1}e`).value,10) + (cfg.comp.seglen?parseInt(gId(`seg${lu -1}s`).value,10):0); if (isM) { ns = 0; } else { if (xend < ledCount) ns = xend; ct -= cfg.comp.seglen?ns:0; } } gId('segutil').scrollIntoView({ behavior: 'smooth', block: 'start', }); var cn = `
`+ `
`+ ``+ ``+ ``+ ``+ ``+ ``+ ``+ ``+ ``+ ``+ ``+ ``+ ``+ ``+ ``+ ``+ `
${isM?'Start X':'Start LED'}${isM?(cfg.comp.seglen?"Width":"Stop X"):(cfg.comp.seglen?"LED count":"Stop LED")}
Start Y${cfg.comp.seglen?'Height':'Stop Y'}
`+ `
${ledCount - ns} LEDs
`+ `
`+ `
`+ `
`; gId('segutil').innerHTML = cn; } function resetUtil(off=false) { gId('segutil').innerHTML = `
` + '' + `
Add segment
` + '
' + `` + '
' + '
'; gId('selall').checked = true; for (var i = 0; i <= lSeg; i++) { if (!gId(`seg${i}`)) continue; if (!gId(`seg${i}sel`).checked) gId('selall').checked = false; // uncheck if at least one is unselected. } if (lSeg>2) d.querySelectorAll("#Segments .pop").forEach((e)=>{e.classList.remove("hide");}); } function makePlSel(p, el) { var plSelContent = ""; delete pJson["0"]; // remove filler preset Object.entries(pJson).sort(cmpP).forEach((a)=>{ var n = a[1].n ? a[1].n : "Preset " + a[0]; if (isPlaylist(a[1])) n += ' ▶'; // mark playlist if (cfg.comp.idsort) n = a[0] + ' ' + n; // skip endless playlists and itself if (!isPlaylist(a[1]) || (a[1].playlist.repeat > 0 && a[0]!=p)) plSelContent += ``; }); return plSelContent; } function refreshPlE(p) { var plEDiv = gId(`ple${p}`); if (!plEDiv) return; var content = "
Playlist entries
"; plJson[p].ps.forEach((e,i)=>{content += makePlEntry(p,i);}); content += `
`; plEDiv.innerHTML = content; var dels = plEDiv.getElementsByClassName("btn-pl-del"); if (dels.length < 2) dels[0].style.display = "none"; d.querySelectorAll(`#seg${p+100} .sel`).forEach((i)=>{ if (i.dataset.val) { if (parseInt(i.dataset.val) > 0) i.value = i.dataset.val; else plJson[p].ps[i.dataset.index] = parseInt(i.value); } }); } // p: preset ID, i: playlist item index function addPl(p,i) { const pl = plJson[p]; pl.ps.splice(i+1,0,1); pl.dur.splice(i+1,0,pl.dur[i]); pl.transition.splice(i+1,0,pl.transition[i]); refreshPlE(p); } function delPl(p,i) { const pl = plJson[p]; if (pl.ps.length < 2) return; pl.ps.splice(i,1); pl.dur.splice(i,1); pl.transition.splice(i,1); refreshPlE(p); } function plePs(p,i,field) { plJson[p].ps[i] = parseInt(field.value); } function pleDur(p,i,field) { if (field.validity.valid) plJson[p].dur[i] = Math.floor(field.value*10); } function pleTr(p,i,field) { const du = gId(`pl${p}du${i}`); const dv = parseFloat(du.value); if (dv > 0) { field.max = dv; if (parseFloat(field.value) > dv) field.value = du.value; } if (field.validity.valid) plJson[p].transition[i] = Math.floor(field.value*10); } function plR(p) { var pl = plJson[p]; pl.r = gId(`pl${p}rtgl`).checked; if (gId(`pl${p}rptgl`).checked) { // infinite pl.repeat = 0; delete pl.end; gId(`pl${p}o1`).style.display = "none"; } else { pl.repeat = parseInt(gId(`pl${p}rp`).value); pl.end = parseInt(gId(`pl${p}selEnd`).value); gId(`pl${p}o1`).style.display = "block"; } } function plM(p) { const man = gId(`pl${p}manual`).checked; plJson[p].dur.forEach((e,i)=>{ const d = gId(`pl${p}du${i}`); plJson[p].dur[i] = e = man ? 0 : 100; d.value = e/10; // 10s default d.readOnly = man; }); } function makeP(i,pl) { var content = ""; const bps = lastinfo.leds.bootps; if (pl) { if (i===0) plJson[0] = { ps: [1], dur: [100], transition: [tr], repeat: 0, r: false, end: 0 }; const rep = plJson[i].repeat ? plJson[i].repeat : 0; const man = plJson[i].dur == 0; content = `
Repeat 0?rep:1}> times
End preset:
`; } else { content = ` `; if (Array.isArray(lastinfo.maps) && lastinfo.maps.length>1) { content += `
Ledmap: 
"; } } return `
Quick load label:
(leave empty for no Quick load button)
API command
${content}
Save to ID 0)?i:getLowestUnusedP()}>
${(i>0)?'
${(i>0)? ('
ID ' +i+ '
'):""}`; } function makePUtil() { let p = gId('putil'); p.classList.remove('staybot'); p.classList.add('pres'); p.innerHTML = `
${makeP(0)}
`; let pTx = gId('p0txt'); pTx.focus(); pTx.value = eJson.find((o)=>{return o.id==selectedFx}).name; pTx.select(); p.scrollIntoView({ behavior: 'smooth', block: 'center' }); gId('psFind').classList.remove('staytop'); } function makePlEntry(p,i) { const man = gId(`pl${p}manual`).checked; return `
Duration (0=inf.) Transition #${i+1}
s s
`; } function makePlUtil() { if (pNum < 2) { showToast("You need at least 2 presets to make a playlist!"); //return; } let p = gId('putil'); p.classList.remove('staybot'); p.classList.add('pres'); p.innerHTML = `
${makeP(0,true)}
`; refreshPlE(0); gId('p0txt').focus(); p.scrollIntoView({ behavior: 'smooth', block: 'center' }); gId('psFind').classList.remove('staytop'); } function resetPUtil() { gId('psFind').classList.add('staytop'); let p = gId('putil'); p.classList.add('staybot'); p.classList.remove('pres'); p.innerHTML = `` + ``; } function tglCs(i) { var pss = gId(`p${i}cstgl`).checked; gId(`p${i}o1`).style.display = pss? "block" : "none"; gId(`p${i}o2`).style.display = !pss? "block" : "none"; } function tglSegn(s) { let t = gId(s<100?`seg${s}t`:`p${s-100}txt`); if (t) { t.classList.toggle('show'); t.focus(); t.select(); } event.preventDefault(); event.stopPropagation(); } function selSegAll(o) { var obj = {"seg":[]}; for (let i=0; i<=lSeg; i++) if (gId(`seg${i}`)) obj.seg.push({"id":i,"sel":o.checked}); requestJson(obj); } function selSegEx(s) { var obj = {"seg":[]}; for (let i=0; i<=lSeg; i++) if (gId(`seg${i}`)) obj.seg.push({"id":i,"sel":(i==s)}); obj.mainseg = s; requestJson(obj); } function selSeg(s) { var sel = gId(`seg${s}sel`).checked; var obj = {"seg": {"id": s, "sel": sel}}; requestJson(obj); } function selGrp(g) { event.preventDefault(); event.stopPropagation(); var obj = {"seg":[]}; for (let i=0; i<=lSeg; i++) if (gId(`seg${i}`)) obj.seg.push({"id":i,"sel":false}); gId(`segcont`).querySelectorAll(`div[data-set="${g}"]`).forEach((s)=>{ let i = parseInt(s.id.substring(3)); obj.seg[i] = {"id":i,"sel":true}; }); if (obj.seg.length) requestJson(obj); } function rptSeg(s) { //TODO: 2D support var name = gId(`seg${s}t`).value; var start = parseInt(gId(`seg${s}s`).value); var stop = parseInt(gId(`seg${s}e`).value); if (stop == 0) {return;} var rev = gId(`seg${s}rev`).checked; var mi = gId(`seg${s}mi`).checked; var sel = gId(`seg${s}sel`).checked; var pwr = gId(`seg${s}pwr`).classList.contains('act'); var obj = {"seg": {"id": s, "n": name, "start": start, "stop": (cfg.comp.seglen?start:0)+stop, "rev": rev, "mi": mi, "on": pwr, "bri": parseInt(gId(`seg${s}bri`).value), "sel": sel}}; if (gId(`seg${s}grp`)) { var grp = parseInt(gId(`seg${s}grp`).value); var spc = parseInt(gId(`seg${s}spc`).value); var ofs = parseInt(gId(`seg${s}of` ).value); obj.seg.grp = grp; obj.seg.spc = spc; obj.seg.of = ofs; } obj.seg.rpt = true; expand(s); requestJson(obj); } function setSeg(s) { var name = gId(`seg${s}t`).value; let sX = gId(`seg${s}s`); let eX = gId(`seg${s}e`); var start = parseInt(sX.value); var stop = parseInt(eX.value) + (cfg.comp.seglen?start:0); if (startsX.max) {sX.value=sX.min; return;} // prevent out of bounds if (stopeX.max) {eX.value=eX.max; return;} // prevent out of bounds if ((cfg.comp.seglen && stop == 0) || (!cfg.comp.seglen && stop <= start)) {delSeg(s); return;} var obj = {"seg": {"id": s, "n": name, "start": start, "stop": stop}}; if (isM && startsY.max) {sY.value=sY.min; return;} // prevent out of bounds if (stopYeY.max) {eY.value=eY.max; return;} // prevent out of bounds obj.seg.startY = startY; obj.seg.stopY = stopY; } let g = gId(`seg${s}grp`); if (g) { // advanced options, not present in new segment dialog (makeSeg()) let grp = parseInt(g.value); let spc = parseInt(gId(`seg${s}spc`).value); let ofs = parseInt(gId(`seg${s}of` ).value); obj.seg.grp = grp; obj.seg.spc = spc; obj.seg.of = ofs; if (isM && gId(`seg${s}tp`)) obj.seg.tp = gId(`seg${s}tp`).checked; } resetUtil(); // close add segment dialog just in case requestJson(obj); } function delSeg(s) { if (segCount < 2) { showToast("You need to have multiple segments to delete one!"); return; } segCount--; var obj = {"seg": {"id": s, "stop": 0}}; requestJson(obj); } function setRev(s) { var rev = gId(`seg${s}rev`).checked; var obj = {"seg": {"id": s, "rev": rev}}; requestJson(obj); } function setRevY(s) { var rev = gId(`seg${s}rY`).checked; var obj = {"seg": {"id": s, "rY": rev}}; requestJson(obj); } function setMi(s) { var mi = gId(`seg${s}mi`).checked; var obj = {"seg": {"id": s, "mi": mi}}; requestJson(obj); } function setMiY(s) { var mi = gId(`seg${s}mY`).checked; var obj = {"seg": {"id": s, "mY": mi}}; requestJson(obj); } function setM12(s) { var value = gId(`seg${s}m12`).selectedIndex; var obj = {"seg": {"id": s, "m12": value}}; requestJson(obj); } function setSi(s) { var value = gId(`seg${s}si`).selectedIndex; var obj = {"seg": {"id": s, "si": value}}; requestJson(obj); } function setBm(s) { var value = gId(`seg${s}bm`).selectedIndex; var obj = {"seg": {"id": s, "bm": value}}; requestJson(obj); } function setTp(s) { var tp = gId(`seg${s}tp`).checked; var obj = {"seg": {"id": s, "tp": tp}}; requestJson(obj); } function setGrp(s, g) { event.preventDefault(); event.stopPropagation(); var obj = {"seg": {"id": s, "set": g}}; requestJson(obj); } function setSegPwr(s) { var pwr = gId(`seg${s}pwr`).classList.contains('act'); var obj = {"seg": {"id": s, "on": !pwr}}; requestJson(obj); } function setSegBri(s) { var obj = {"seg": {"id": s, "bri": parseInt(gId(`seg${s}bri`).value)}}; requestJson(obj); } function tglFreeze(s=null) { var obj = {"seg": {"frz": "t"}}; // toggle if (s!==null) { obj.seg.id = s; // if live segment, enter live override (which also unfreezes) if (lastinfo && s==lastinfo.liveseg && lastinfo.live) obj = {"lor":1}; } requestJson(obj); } function setFX(ind = null) { if (ind === null) { ind = parseInt(d.querySelector('#fxlist input[name="fx"]:checked').value); } else { d.querySelector(`#fxlist input[name="fx"][value="${ind}"]`).checked = true; } // Close effect dialog in simplified UI if (simplifiedUI) { gId("fx").lastElementChild.close(); } var obj = {"seg": {"fx": parseInt(ind), "fxdef": cfg.comp.fxdef}}; // fxdef sets effect parameters to default values requestJson(obj); } function setPalette(paletteId = null) { if (paletteId === null) { paletteId = parseInt(d.querySelector('#pallist input[name="palette"]:checked').value); } else { d.querySelector(`#pallist input[name="palette"][value="${paletteId}"]`).checked = true; } // Close palette dialog in simplified UI if (simplifiedUI) { gId("palw").lastElementChild.close(); } var obj = {"seg": {"pal": paletteId}}; requestJson(obj); } function setBri() { var obj = {"bri": parseInt(gId('sliderBri').value)}; requestJson(obj); } function setSpeed() { var obj = {"seg": {"sx": parseInt(gId('sliderSpeed').value)}}; requestJson(obj); } function setIntensity() { var obj = {"seg": {"ix": parseInt(gId('sliderIntensity').value)}}; requestJson(obj); } function setCustom(i=1) { if (i<1 || i>3) return; var obj = {"seg": {}}; var val = parseInt(gId(`sliderC${i}`).value); if (i===3) obj.seg.c3 = val; else if (i===2) obj.seg.c2 = val; else obj.seg.c1 = val; requestJson(obj); } function setOption(i=1, v=false) { if (i<1 || i>3) return; var obj = {"seg": {}}; if (i===3) obj.seg.o3 = !(!v); //make sure it is bool else if (i===2) obj.seg.o2 = !(!v); //make sure it is bool else obj.seg.o1 = !(!v); //make sure it is bool requestJson(obj); } function setLor(i) { var obj = {"lor": i}; requestJson(obj); } function setPreset(i) { var obj = {"ps":i}; if (!isPlaylist(i) && pJson && pJson[i] && (!pJson[i].win || pJson[i].win.indexOf("Please") <= 0)) { // we will send the complete preset content as to avoid delay introduced by // async nature of applyPreset() and having to read the preset from file system. obj = {"pd":i}; // use "pd" instead of "ps" to indicate that we are sending the preset content directly Object.assign(obj, pJson[i]); delete obj.ql; // no need for quick load delete obj.n; // no need for name } if (isPlaylist(i)) obj.on = true; // force on showToast("Loading preset " + pName(i) +" (" + i + ")"); requestJson(obj); } function saveP(i,pl) { pI = parseInt(gId(`p${i}id`).value); if (!pI || pI < 1) pI = (i>0) ? i : getLowestUnusedP(); if (pI > 250) {alert("Preset ID must be 250 or less."); return;} pN = gId(`p${i}txt`).value; if (pN == "") pN = (pl?"Playlist ":"Preset ") + pI; var obj = {}; if (!gId(`p${i}cstgl`).checked) { var raw = gId(`p${i}api`).value; try { obj = JSON.parse(raw); } catch (e) { obj.win = raw; if (raw.length < 2) { gId(`p${i}warn`).innerHTML = "⚠ Please enter your API command first"; return; } else if (raw.indexOf('{') > -1) { gId(`p${i}warn`).innerHTML = "⚠ Syntax error in custom JSON API command"; return; } else if (raw.indexOf("Please") == 0) { gId(`p${i}warn`).innerHTML = "⚠ Please refresh the page before modifying this preset"; return; } } obj.o = true; } else { if (pl) { obj.playlist = plJson[i]; obj.on = true; obj.o = true; } else { obj.ib = gId(`p${i}ibtgl`).checked; obj.sb = gId(`p${i}sbtgl`).checked; obj.sc = gId(`p${i}sbchk`).checked; if (gId(`p${i}lmp`) && gId(`p${i}lmp`).value!=="") obj.ledmap = parseInt(gId(`p${i}lmp`).value); } } if (gId(`p${i}bps`).checked) obj.bootps = pI; obj.psave = pI; obj.n = pN; var pQN = gId(`p${i}ql`).value; if (pQN.length > 0) obj.ql = pQN; showToast("Saving " + pN +" (" + pI + ")"); requestJson(obj); if (obj.o) { pJson[pI] = obj; delete pJson[pI].psave; delete pJson[pI].o; delete pJson[pI].v; delete pJson[pI].time; } else { pJson[pI] = {"n":pN, "win":"Please refresh the page to see this newly saved command."}; if (obj.win) pJson[pI].win = obj.win; if (obj.ql) pJson[pI].ql = obj.ql; } populatePresets(); resetPUtil(); setTimeout(()=>{loadPresets();}, 750); // force reloading of presets } function testPl(i,bt) { if (bt.dataset.test == 1) { bt.dataset.test = 0; bt.innerHTML = "Test"; stopPl(); return; } bt.dataset.test = 1; bt.innerHTML = "Stop"; var obj = {}; obj.playlist = plJson[i]; obj.on = true; requestJson(obj); } function stopPl() { requestJson({playlist:{}}) } function delP(i) { var bt = gId(`p${i}del`); if (bt.dataset.cnf == 1) { var obj = {"pdel": i}; requestJson(obj); delete pJson[i]; populatePresets(); gId('putil').classList.add('staybot'); } else { bt.style.color = "var(--c-r)"; bt.innerHTML = "Delete!"; bt.dataset.cnf = 1; } } function selectSlot(b) { csel = b; var cd = gId('csl').children; for (let i of cd) i.classList.remove('sl'); cd[b].classList.add('sl'); setPicker(rgbStr(cd[b].dataset)); // force slider update on initial load (picker "color:change" not fired if black) if (cpick.color.value == 0) updatePSliders(); gId('sliderW').value = parseInt(cd[b].dataset.w); updateTrail(gId('sliderW')); redrawPalPrev(); } // set the color from a hex string. Used by quick color selectors var lasth = 0; function pC(col) { if (col == "rnd") { col = {h: 0, s: 0, v: 100}; col.s = Math.floor((Math.random() * 50) + 50); do { col.h = Math.floor(Math.random() * 360); } while (Math.abs(col.h - lasth) < 50); lasth = col.h; } setPicker(col); setColor(0); } function updatePSliders() { // update RGB sliders var col = cpick.color.rgb; gId('sliderR').value = col.r; gId('sliderG').value = col.g; gId('sliderB').value = col.b; // update hex field var str = cpick.color.hexString.substring(1); var w = parseInt(gId("csl").children[csel].dataset.w); if (w > 0) str += w.toString(16); gId('hexc').value = str; gId('hexcnf').style.backgroundColor = "var(--c-3)"; // update HSV sliders var c; let h = cpick.color.hue; let s = cpick.color.saturation; let v = cpick.color.value; gId("sliderH").value = h; gId("sliderS").value = s; gId('sliderV').value = v; c = iro.Color.hsvToRgb({"h":h,"s":100,"v":100}); gId("sliderS").nextElementSibling.style.backgroundImage = 'linear-gradient(90deg, #aaa -15%, rgb('+c.r+','+c.g+','+c.b+'))'; c = iro.Color.hsvToRgb({"h":h,"s":s,"v":100}); gId('sliderV').nextElementSibling.style.backgroundImage = 'linear-gradient(90deg, #000 -15%, rgb('+c.r+','+c.g+','+c.b+'))'; // update Kelvin slider gId('sliderK').value = cpick.color.kelvin; } function hexEnter() { if(event.keyCode == 13) fromHex(); } function segEnter(s) { if(event.keyCode == 13) setSeg(s); } function fromHex() { var str = gId('hexc').value; let w = parseInt(str.substring(6), 16); try { setPicker("#" + str.substring(0,6)); } catch (e) { setPicker("#ffaa00"); } gId("csl").children[csel].dataset.w = isNaN(w) ? 0 : w; setColor(2); } function setPicker(rgb) { var c = new iro.Color(rgb); if (c.value > 0) cpick.color.set(c); else cpick.color.setChannel('hsv', 'v', 0); updateTrail(gId('sliderR')); updateTrail(gId('sliderG')); updateTrail(gId('sliderB')); } function fromH() { cpick.color.setChannel('hsv', 'h', gId('sliderH').value); } function fromS() { cpick.color.setChannel('hsv', 's', gId('sliderS').value); } function fromV() { cpick.color.setChannel('hsv', 'v', gId('sliderV').value); } function fromK() { cpick.color.set({ kelvin: gId('sliderK').value }); } function fromRgb() { var r = gId('sliderR').value; var g = gId('sliderG').value; var b = gId('sliderB').value; setPicker(`rgb(${r},${g},${b})`); let cd = gId('csl').children[csel]; // color slots cd.dataset.r = r; cd.dataset.g = g; cd.dataset.b = b; setCSL(cd); } function fromW() { let w = gId('sliderW'); let cd = gId('csl').children[csel]; // color slots cd.dataset.w = w.value; setCSL(cd); updateTrail(w); } // sr 0: from RGB sliders, 1: from picker, 2: from hex function setColor(sr) { var cd = gId('csl').children[csel]; // color slots let cdd = cd.dataset; let w = parseInt(cdd.w), r = parseInt(cdd.r), g = parseInt(cdd.g), b = parseInt(cdd.b); if (sr == 1 && isRgbBlack(cdd)) cpick.color.setChannel('hsv', 'v', 100); if (sr != 2 && hasWhite) w = parseInt(gId('sliderW').value); var col = cpick.color.rgb; cdd.r = r = hasRGB ? col.r : w; cdd.g = g = hasRGB ? col.g : w; cdd.b = b = hasRGB ? col.b : w; cdd.w = w; setCSL(cd); var obj = {"seg": {"col": [[],[],[]]}}; obj.seg.col[csel] = [r, g, b, w]; requestJson(obj); } function setBalance(b) { var obj = {"seg": {"cct": parseInt(b)}}; requestJson(obj); } function rmtTgl(ip,i) { event.preventDefault(); event.stopPropagation(); fetch(`http://${ip}/win&T=2`, { method: 'get' }) .then((r)=>{ return r.text(); }) .then((t)=>{ let c = (new window.DOMParser()).parseFromString(t, "text/xml"); // perhaps just i.classList.toggle("off"); would be enough if (c.getElementsByTagName('ac')[0].textContent === "0") { i.classList.add("off"); } else { i.classList.remove("off"); } }); } var hc = 0; setInterval(()=>{ if (!isInfo) return; hc+=18; if (hc>300) hc=0; if (hc>200)hc=306; if (hc==144) hc+=36; if (hc==108) hc+=18; gId('heart').style.color = `hsl(${hc}, 100%, 50%)`; }, 910); function openGH() { window.open("https://github.com/wled/WLED/wiki"); } var cnfr = false; function cnfReset() { if (!cnfr) { var bt = gId('resetbtn'); bt.style.color = "var(--c-r)"; bt.innerHTML = "Confirm Reboot"; cnfr = true; return; } window.location.href = getURL("/reset"); } var cnfrS = false; function rSegs() { var bt = gId('rsbtn'); if (!cnfrS) { bt.style.color = "var(--c-r)"; bt.innerHTML = "Confirm reset"; cnfrS = true; return; } cnfrS = false; bt.style.color = "var(--c-f)"; bt.innerHTML = "Reset segments"; var obj = {"seg":[{"start":0,"stop":ledCount,"sel":true}]}; if (isM) { obj.seg[0].stop = mw; obj.seg[0].startX = 0; obj.seg[0].stopY = mh; } for (let i=1; i<=lSeg; i++) obj.seg.push({"stop":0}); requestJson(obj); } function loadPalettesData() { return new Promise((resolve) => { if (palettesData) return resolve(); // already loaded var lsPalData = localStorage.getItem("wledPalx"); if (lsPalData) { try { var d = JSON.parse(lsPalData); if (d && d.vid == lastinfo.vid) { palettesData = d.p; redrawPalPrev(); return resolve(); } } catch (e) {} } palettesData = {}; getPalettesData(0, () => { localStorage.setItem("wledPalx", JSON.stringify({ p: palettesData, vid: lastinfo.vid })); redrawPalPrev(); setTimeout(resolve, 99); // delay optional }); }); } function getPalettesData(page, callback, retry=0) { fetch(getURL(`/json/palx?page=${page}`), {method: 'get'}) .then(res => res.ok ? res.json() : Promise.reject()) .then(json => { palettesData = Object.assign({}, palettesData, json.p); if (page < json.m) setTimeout(()=>{ getPalettesData(page + 1, callback); }, 75); else callback(); }) .catch((error)=>{ if (retry<5) { setTimeout(()=>{getPalettesData(page,callback,retry+1);}, 100); } else { showToast(error, true); callback(); } }); } /* function hideModes(txt) { for (let e of (gId('fxlist').querySelectorAll('.lstI')||[])) { let iT = e.querySelector('.lstIname').innerText; let f = false; if (txt==="2D") f = iT.indexOf("\u25A6") >= 0 && iT.indexOf("\u22EE") < 0; // 2D && !1D else f = iT.indexOf(txt) >= 0; if (f) e.classList.add('hide'); //else e.classList.remove('hide'); } } */ function search(field, listId = null) { field.nextElementSibling.style.display = (field.value !== '') ? 'block' : 'none'; if (!listId) return; const search = field.value !== ''; // restore default preset sorting if no search term is entered if (!search) { if (listId === 'pcont') { populatePresets(); return; } if (listId === 'pallist') { let id = parseInt(d.querySelector('#pallist input[name="palette"]:checked').value); // preserve selected palette populatePalettes(); updateSelectedPalette(id); return; } } // clear filter if searching in fxlist if (listId === 'fxlist' && search) { gId("filters").querySelectorAll("input[type=checkbox]").forEach((e) => { e.checked = false; }); } // do not search if filter is active if (gId("filters").querySelectorAll("input[type=checkbox]:checked").length) return; // filter list items but leave (Default & Solid) always visible const listItems = gId(listId).querySelectorAll('.lstI'); listItems.forEach((listItem, i) => { if (listId !== 'pcont' && i === 0) return; const listItemName = listItem.querySelector('.lstIname').innerText.toUpperCase(); const searchIndex = listItemName.indexOf(field.value.toUpperCase()); if (searchIndex < 0) { listItem.dataset.searchIndex = Number.MAX_SAFE_INTEGER; } else { listItem.dataset.searchIndex = searchIndex; } listItem.style.display = (searchIndex < 0) && !listItem.classList.contains("selected") ? 'none' : ''; }); // sort list items by search index and name const sortedListItems = Array.from(listItems).sort((a, b) => { const aSearchIndex = parseInt(a.dataset.searchIndex); const bSearchIndex = parseInt(b.dataset.searchIndex); if (aSearchIndex !== bSearchIndex) { return aSearchIndex - bSearchIndex; } const aName = a.querySelector('.lstIname').innerText.toUpperCase(); const bName = b.querySelector('.lstIname').innerText.toUpperCase(); return aName.localeCompare(bName); }); sortedListItems.forEach(item => { gId(listId).append(item); }); // scroll to first search result const firstVisibleItem = sortedListItems.find(item => item.style.display !== 'none' && !item.classList.contains('sticky') && !item.classList.contains('selected')); if (firstVisibleItem && search) { firstVisibleItem.scrollIntoView({ behavior: "instant", block: "center" }); } } function clean(clearButton) { clearButton.style.display = 'none'; const inputField = clearButton.previousElementSibling; inputField.value = ''; search(inputField, clearButton.parentElement.nextElementSibling.id); } function initFilters() { gId("filters").querySelectorAll("input[type=checkbox]").forEach((e) => { e.checked = false; }); } function filterFocus(e) { const f = gId("filters"); const c = !!f.querySelectorAll("input[type=checkbox]:checked").length; const h = f.offsetHeight; const sti = parseInt(getComputedStyle(d.documentElement).getPropertyValue('--sti')); if (e.type === "focus") { // compute sticky top (with delay for transition) if (!h) setTimeout(() => { sCol('--sti', (sti+f.offsetHeight) + "px"); // has an unpleasant consequence on palette offset }, 255); f.classList.remove('fade'); // immediately show (still has transition) } if (e.type === "blur") { setTimeout(() => { if (e.target === d.activeElement && d.hasFocus()) return; // do not hide if filter is active if (!c) { // compute sticky top sCol('--sti', (sti-h) + "px"); // has an unpleasant consequence on palette offset f.classList.add('fade'); } }, 255); // wait with hiding } } function filterFx() { const inputField = gId('fxFind').children[0]; inputField.value = ''; inputField.focus(); clean(inputField.nextElementSibling); gId("fxlist").querySelectorAll('.lstI').forEach((listItem, i) => { const listItemName = listItem.querySelector('.lstIname').innerText; let hide = false; gId("filters").querySelectorAll("input[type=checkbox]").forEach((e) => { if (e.checked && !listItemName.includes(e.dataset.flt)) hide = i > 0 /*true*/; }); listItem.style.display = hide && !listItem.classList.contains("selected") ? 'none' : ''; }); } function preventBlur(e) { if (e.target === gId("fxFind").children[0] || e.target === gId("filters")) return; e.preventDefault(); } // make sure "dur" and "transition" are arrays with at least the length of "ps" function formatArr(pl) { var l = pl.ps.length; if (!Array.isArray(pl.dur)) { var v = pl.dur; if (isNaN(v)) v = 100; pl.dur = [v]; } var l2 = pl.dur.length; if (l2 < l) { for (var i = 0; i < l - l2; i++) pl.dur.push(pl.dur[l2-1]); } if (!Array.isArray(pl.transition)) { var v = pl.transition; if (isNaN(v)) v = tr; pl.transition = [v]; } var l2 = pl.transition.length; if (l2 < l) { for (var i = 0; i < l - l2; i++) pl.transition.push(pl.transition[l2-1]); } } function expand(i) { var seg = i<100 ? gId('seg' +i) : gId(`p${i-100}o`); let ps = gId("pcont").children; // preset wrapper if (i>100) for (let p of ps) { p.classList.remove('selected'); if (p!==seg) p.classList.remove('expanded'); } // collapse all other presets & remove selected seg.classList.toggle('expanded'); // presets if (i >= 100) { var p = i-100; if (seg.classList.contains('expanded')) { if (isPlaylist(p)) { plJson[p] = pJson[p].playlist; // make sure all keys are present in plJson[p] formatArr(plJson[p]); if (isNaN(plJson[p].repeat)) plJson[p].repeat = 0; if (!plJson[p].r) plJson[p].r = false; if (isNaN(plJson[p].end)) plJson[p].end = 0; gId('seg' +i).innerHTML = makeP(p,true); refreshPlE(p); } else { gId('seg' +i).innerHTML = makeP(p); } var papi = papiVal(p); gId(`p${p}api`).value = papi; if (papi.indexOf("Please") == 0) gId(`p${p}cstgl`).checked = false; tglCs(p); gId('putil').classList.remove('staybot'); } else { updatePA(); gId('seg' +i).innerHTML = ""; gId('putil').classList.add('staybot'); } } seg.scrollIntoView({ behavior: 'smooth', block: 'center' }); } function unfocusSliders() { gId("sliderBri").blur(); gId("sliderSpeed").blur(); gId("sliderIntensity").blur(); } // sliding UI const _C = d.querySelector('.container'), N = 4; let iSlide = 0, x0 = null, scrollS = 0, locked = false; function unify(e) { return e.changedTouches ? e.changedTouches[0] : e; } function hasIroClass(classList) { let found = false; classList.forEach((e)=>{ if (e.startsWith('Iro')) found = true; }); return found; } //required by rangetouch.js function lock(e) { if (pcMode || simplifiedUI) return; var l = e.target.classList; var pl = e.target.parentElement.classList; if (l.contains('noslide') || hasIroClass(l) || hasIroClass(pl)) return; x0 = unify(e).clientX; scrollS = gEBCN("tabcontent")[iSlide].scrollTop; _C.classList.toggle('smooth', !(locked = true)); } //required by rangetouch.js function move(e) { if(!locked || pcMode || simplifiedUI) return; var clientX = unify(e).clientX; var dx = clientX - x0; var s = Math.sign(dx); var f = +(s*dx/wW).toFixed(2); if((clientX != 0) && (iSlide > 0 || s < 0) && (iSlide < N - 1 || s > 0) && f > 0.12 && gEBCN("tabcontent")[iSlide].scrollTop == scrollS) { _C.style.setProperty('--i', iSlide -= s); f = 1 - f; updateTablinks(iSlide); } _C.style.setProperty('--f', f); _C.classList.toggle('smooth', !(locked = false)); x0 = null; } function size() { wW = window.innerWidth; var h = gId('top').clientHeight; sCol('--th', h + "px"); sCol('--bh', gId('bot').clientHeight + "px"); if (isLv) h -= 4; sCol('--tp', h + "px"); togglePcMode(); lastw = wW; } function togglePcMode(fromB = false) { let ap = (fromB && !lastinfo) || (lastinfo && lastinfo.wifi && lastinfo.wifi.ap); if (fromB) { pcModeA = !pcModeA; localStorage.setItem('pcm', pcModeA); } pcMode = (wW >= 1024) && pcModeA; if (cpick) cpick.resize(pcMode && wW>1023 && wW<1250 ? 230 : 260); // for tablet in landscape if (!fromB && ((wW < 1024 && lastw < 1024) || (wW >= 1024 && lastw >= 1024))) return; // no change in size and called from size() if (pcMode) openTab(0, true); gId('buttonPcm').className = (pcMode) ? "active":""; gId('bot').style.height = (pcMode && !cfg.comp.pcmbot) ? "0":"auto"; sCol('--bh', gId('bot').clientHeight + "px"); _C.style.width = (pcMode || simplifiedUI)?'100%':'400%'; } function mergeDeep(target, ...sources) { if (!sources.length) return target; const source = sources.shift(); if (isObj(target) && isObj(source)) { for (const key in source) { if (isObj(source[key])) { if (!target[key]) Object.assign(target, { [key]: {} }); mergeDeep(target[key], source[key]); } else { Object.assign(target, { [key]: source[key] }); } } } return mergeDeep(target, ...sources); } function tooltip(cont=null) { d.querySelectorAll((cont?cont+" ":"")+"[title]").forEach((element)=>{ element.addEventListener("pointerover", ()=>{ // save title element.setAttribute("data-title", element.getAttribute("title")); const tooltip = d.createElement("span"); tooltip.className = "tooltip"; tooltip.textContent = element.getAttribute("title"); // prevent default title popup element.removeAttribute("title"); let { top, left, width } = element.getBoundingClientRect(); d.body.appendChild(tooltip); const { offsetHeight, offsetWidth } = tooltip; const offset = element.classList.contains("sliderwrap") ? 4 : 10; top -= offsetHeight + offset; left += (width - offsetWidth) / 2; tooltip.style.top = top + "px"; tooltip.style.left = left + "px"; tooltip.classList.add("visible"); }); element.addEventListener("pointerout", ()=>{ d.querySelectorAll('.tooltip').forEach((tooltip)=>{ tooltip.classList.remove("visible"); d.body.removeChild(tooltip); }); // restore title element.setAttribute("title", element.getAttribute("data-title")); }); }); }; // Transforms the default UI into the simple UI function simplifyUI() { // Create dropdown dialog function createDropdown(id, buttonText, dialogElements = null) { // Create dropdown dialog const dialog = d.createElement("dialog"); // Move every dialogElement to the dropdown dialog or if none are given, move all children of the element with the given id if (dialogElements) { dialogElements.forEach((e) => { dialog.appendChild(e); }); } else { while (gId(id).firstChild) { dialog.appendChild(gId(id).firstChild); } } // Create button for the dropdown const btn = d.createElement("button"); btn.id = id + "btn"; btn.classList.add("btn"); btn.innerText = buttonText; function toggleDialog(e) { if (e.target != btn && e.target != dialog) return; if (dialog.open) { dialog.close(); return; } // Prevent autofocus on dialog open dialog.inert = true; dialog.showModal(); dialog.inert = false; clean(dialog.firstElementChild.children[1]); dialog.scrollTop = 0; }; btn.addEventListener("click", toggleDialog); dialog.addEventListener("click", toggleDialog); // Add the dialog and button to the element with the given id gId(id).append(btn); gId(id).append(dialog); } // Check if the UI was already simplified if (gId("Colors").classList.contains("simplified")) return; // Disable PC Mode as it does not exist in simple UI if (pcMode) togglePcMode(true); _C.style.width = '100%' _C.style.setProperty('--n', 1); gId("Colors").classList.add("simplified"); // Put effects below palett list gId("Colors").append(gId("fx")); gId("Colors").append(gId("sliders")); // Put segments before palette list gId("Colors").insertBefore(gId("segcont"), gId("pall")); // Put preset quick load before palette list and segemts gId("Colors").insertBefore(gId("pql"), gId("pall")); // Create dropdown for palette list createDropdown("palw", "Change palette"); createDropdown("fx", "Change effect", [gId("fxFind"), gId("fxlist")]); // Hide palette label gId("pall").style.display = "none"; gId("Colors").insertBefore(d.createElement("br"), gId("pall")); // Hide effect label gId("modeLabel").style.display = "none"; // Hide buttons in top bar gId("buttonNl").style.display = "none"; gId("buttonSync").style.display = "none"; gId("buttonSr").style.display = "none"; gId("buttonPcm").style.display = "none"; // Hide bottom bar gId("bot").style.display = "none"; d.documentElement.style.setProperty('--bh', '0px'); // Hide other tabs gId("Effects").style.display = "none"; gId("Segments").style.display = "none"; gId("Presets").style.display = "none"; // Hide filter options gId("filters").style.display = "none"; // Hide buttons for pixel art and custom palettes (add / delete) gId("btns").style.display = "none"; } // Version reporting feature var versionCheckDone = false; function checkVersionUpgrade(info) { // Only check once per page load if (versionCheckDone) return; versionCheckDone = true; // Suppress feature if in AP mode (no internet connection available) if (info.wifi && info.wifi.ap) return; // Fetch version-info.json using existing /edit endpoint fetch(getURL('/edit?func=edit&path=/version-info.json'), { method: 'get' }) .then(res => { if (res.status === 404) { // File doesn't exist - first install, show install prompt showVersionUpgradePrompt(info, null, info.ver); return null; } if (!res.ok) { throw new Error('Failed to fetch version-info.json'); } return res.json(); }) .then(versionInfo => { if (!versionInfo) return; // 404 case already handled // Check if user opted out if (versionInfo.neverAsk) return; // Check if version has changed const currentVersion = info.ver; const storedVersion = versionInfo.version || ''; if (storedVersion && storedVersion !== currentVersion) { // Version has changed if (versionInfo.alwaysReport) { // Automatically report if user opted in for always reporting reportUpgradeEvent(info, storedVersion, true); } else { // Show upgrade prompt showVersionUpgradePrompt(info, storedVersion, currentVersion); } } else if (!storedVersion) { // Empty version in file, show install prompt showVersionUpgradePrompt(info, null, currentVersion); } }) .catch(e => { console.log('Failed to load version-info.json', e); // On error, save current version for next time if (info && info.ver) { updateVersionInfo(info.ver, false, false); } }); } function showVersionUpgradePrompt(info, oldVersion, newVersion) { // Determine if this is an install or upgrade const isInstall = !oldVersion; // Create overlay and dialog const overlay = d.createElement('div'); overlay.id = 'versionUpgradeOverlay'; overlay.style.cssText = 'position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,0.7);z-index:10000;display:flex;align-items:center;justify-content:center;'; const dialog = d.createElement('div'); dialog.style.cssText = 'background:var(--c-1);border-radius:10px;padding:25px;max-width:500px;margin:20px;box-shadow:0 4px 6px rgba(0,0,0,0.3);'; // Build contextual message based on install vs upgrade const title = isInstall ? '🎉 Thank you for installing WLED!' : '🎉 WLED Upgrade Detected!'; const description = isInstall ? `You are now running WLED ${newVersion}.` : `Your WLED has been upgraded from ${oldVersion} to ${newVersion}.`; const question = 'Help make WLED better by sharing hardware details like chip type and LED count? This helps us understand how WLED is used and prioritize features — we never collect personal data or your activities.' dialog.innerHTML = `

${title}

${description}

${question}

Learn more about what data is collected and why

`; overlay.appendChild(dialog); d.body.appendChild(overlay); // Add event listeners gId('versionReportYes').addEventListener('click', () => { const saveChoice = gId('versionSaveChoice').checked; d.body.removeChild(overlay); // Pass saveChoice as alwaysReport parameter reportUpgradeEvent(info, oldVersion, saveChoice); }); gId('versionReportNo').addEventListener('click', () => { const saveChoice = gId('versionSaveChoice').checked; d.body.removeChild(overlay); if (saveChoice) { // Save "never ask" preference updateVersionInfo(newVersion, true, false); showToast('You will not be asked again.'); } else { // Save current version to prevent re-prompting until version changes updateVersionInfo(newVersion, false, false); } }); } function reportUpgradeEvent(info, oldVersion, alwaysReport) { showToast('Reporting upgrade...'); // Fetch fresh data from /json/info endpoint as requested fetch(getURL('/json/info'), { method: 'get' }) .then(res => res.json()) .then(infoData => { // Map to UpgradeEventRequest structure per OpenAPI spec // Required fields: deviceId, version, previousVersion, releaseName, chip, ledCount, isMatrix, bootloaderSHA256 const upgradeData = { deviceId: infoData.deviceId, // Use anonymous unique device ID version: infoData.ver || '', // Current version string previousVersion: oldVersion || '', // Previous version from version-info.json releaseName: infoData.release || '', // Release name (e.g., "WLED 0.15.0") chip: infoData.arch || '', // Chip architecture (esp32, esp8266, etc) ledCount: infoData.leds ? infoData.leds.count : 0, // Number of LEDs isMatrix: !!(infoData.leds && infoData.leds.matrix), // Whether it's a 2D matrix setup bootloaderSHA256: infoData.bootloaderSHA256 || '', // Bootloader SHA256 hash brand: infoData.brand, // Device brand (always present) product: infoData.product, // Product name (always present) flashSize: infoData.flash, // Flash size (always present) repo: infoData.repo // GitHub repository (always present) }; // Add optional fields if available if (infoData.psrSz !== undefined) upgradeData.psramSize = infoData.psrSz; // Total PSRAM size in MB; can be 0 // Note: partitionSizes not currently available in /json/info endpoint // Make AJAX call to postUpgradeEvent API return fetch('https://usage.wled.me/api/usage/upgrade', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(upgradeData) }); }) .then(res => { if (res.ok) { if (alwaysReport) { showToast('Thank you! Future upgrades will be reported automatically.'); } else { showToast('Thank you for reporting!'); } updateVersionInfo(info.ver, false, !!alwaysReport); } else { showToast('Report failed. Please try again later.', true); // Do NOT update version info on failure - user will be prompted again } }) .catch(e => { console.log('Failed to report upgrade', e); showToast('Report failed. Please try again later.', true); // Do NOT update version info on error - user will be prompted again }); } function updateVersionInfo(version, neverAsk, alwaysReport) { const versionInfo = { version: version, neverAsk: neverAsk, alwaysReport: !!alwaysReport }; // Create a Blob with JSON content and use /upload endpoint const blob = new Blob([JSON.stringify(versionInfo)], {type: 'application/json'}); const formData = new FormData(); formData.append('data', blob, 'version-info.json'); fetch(getURL('/upload'), { method: 'POST', body: formData }) .then(res => res.text()) .then(data => { console.log('Version info updated', data); }) .catch(e => { console.log('Failed to update version-info.json', e); }); } size(); _C.style.setProperty('--n', N); window.addEventListener('resize', size, true); window.addEventListener('hashchange', handleLocationHash); _C.addEventListener('mousedown', lock, false); _C.addEventListener('touchstart', lock, false); _C.addEventListener('mouseout', move, false); _C.addEventListener('mouseup', move, false); _C.addEventListener('touchend', move, false); ================================================ FILE: wled00/data/iro.js ================================================ /*! * iro.js v5.5.2 * 2016-2021 James Daniel * Licensed under MPL 2.0 * github.com/jaames/iro.js */ !function(t,n){"object"==typeof exports&&"undefined"!=typeof module?module.exports=n():"function"==typeof define&&define.amd?define(n):(t=t||self).iro=n()}(this,function(){"use strict";var m,s,n,i,o,x={},j=[],r=/acit|ex(?:s|g|n|p|$)|rph|grid|ows|mnc|ntw|ine[ch]|zoo|^ord|^--/i;function M(t,n){for(var i in n)t[i]=n[i];return t}function y(t){var n=t.parentNode;n&&n.removeChild(t)}function h(t,n,i){var r,e,u,o,l=arguments;if(n=M({},n),3=r/i?u=n:e=n}return n},function(t,n,i){n&&g(t.prototype,n),i&&g(t,i)}(l,[{key:"hsv",get:function(){var t=this.$;return{h:t.h,s:t.s,v:t.v}},set:function(t){var n=this.$;if(t=b({},n,t),this.onChange){var i={h:!1,v:!1,s:!1,a:!1};for(var r in n)i[r]=t[r]!=n[r];this.$=t,(i.h||i.s||i.v||i.a)&&this.onChange(this,i)}else this.$=t}},{key:"hsva",get:function(){return b({},this.$)},set:function(t){this.hsv=t}},{key:"hue",get:function(){return this.$.h},set:function(t){this.hsv={h:t}}},{key:"saturation",get:function(){return this.$.s},set:function(t){this.hsv={s:t}}},{key:"value",get:function(){return this.$.v},set:function(t){this.hsv={v:t}}},{key:"alpha",get:function(){return this.$.a},set:function(t){this.hsv=b({},this.hsv,{a:t})}},{key:"kelvin",get:function(){return l.rgbToKelvin(this.rgb)},set:function(t){this.rgb=l.kelvinToRgb(t)}},{key:"red",get:function(){return this.rgb.r},set:function(t){this.rgb=b({},this.rgb,{r:t})}},{key:"green",get:function(){return this.rgb.g},set:function(t){this.rgb=b({},this.rgb,{g:t})}},{key:"blue",get:function(){return this.rgb.b},set:function(t){this.rgb=b({},this.rgb,{b:t})}},{key:"rgb",get:function(){var t=l.hsvToRgb(this.$),n=t.r,i=t.g,r=t.b;return{r:G(n),g:G(i),b:G(r)}},set:function(t){this.hsv=b({},l.rgbToHsv(t),{a:void 0===t.a?1:t.a})}},{key:"rgba",get:function(){return b({},this.rgb,{a:this.alpha})},set:function(t){this.rgb=t}},{key:"hsl",get:function(){var t=l.hsvToHsl(this.$),n=t.h,i=t.s,r=t.l;return{h:G(n),s:G(i),l:G(r)}},set:function(t){this.hsv=b({},l.hslToHsv(t),{a:void 0===t.a?1:t.a})}},{key:"hsla",get:function(){return b({},this.hsl,{a:this.alpha})},set:function(t){this.hsl=t}},{key:"rgbString",get:function(){var t=this.rgb;return"rgb("+t.r+", "+t.g+", "+t.b+")"},set:function(t){var n,i,r,e,u=1;if((n=_.exec(t))?(i=K(n[1],255),r=K(n[2],255),e=K(n[3],255)):(n=H.exec(t))&&(i=K(n[1],255),r=K(n[2],255),e=K(n[3],255),u=K(n[4],1)),!n)throw new Error("Invalid rgb string");this.rgb={r:i,g:r,b:e,a:u}}},{key:"rgbaString",get:function(){var t=this.rgba;return"rgba("+t.r+", "+t.g+", "+t.b+", "+t.a+")"},set:function(t){this.rgbString=t}},{key:"hexString",get:function(){var t=this.rgb;return"#"+U(t.r)+U(t.g)+U(t.b)},set:function(t){var n,i,r,e,u=255;if((n=D.exec(t))?(i=17*Q(n[1]),r=17*Q(n[2]),e=17*Q(n[3])):(n=F.exec(t))?(i=17*Q(n[1]),r=17*Q(n[2]),e=17*Q(n[3]),u=17*Q(n[4])):(n=L.exec(t))?(i=Q(n[1]),r=Q(n[2]),e=Q(n[3])):(n=B.exec(t))&&(i=Q(n[1]),r=Q(n[2]),e=Q(n[3]),u=Q(n[4])),!n)throw new Error("Invalid hex string");this.rgb={r:i,g:r,b:e,a:u/255}}},{key:"hex8String",get:function(){var t=this.rgba;return"#"+U(t.r)+U(t.g)+U(t.b)+U(Z(255*t.a))},set:function(t){this.hexString=t}},{key:"hslString",get:function(){var t=this.hsl;return"hsl("+t.h+", "+t.s+"%, "+t.l+"%)"},set:function(t){var n,i,r,e,u=1;if((n=P.exec(t))?(i=K(n[1],360),r=K(n[2],100),e=K(n[3],100)):(n=$.exec(t))&&(i=K(n[1],360),r=K(n[2],100),e=K(n[3],100),u=K(n[4],1)),!n)throw new Error("Invalid hsl string");this.hsl={h:i,s:r,l:e,a:u}}},{key:"hslaString",get:function(){var t=this.hsla;return"hsla("+t.h+", "+t.s+"%, "+t.l+"%, "+t.a+")"},set:function(t){this.hslString=t}}]),l}();function X(t){var n,i=t.width,r=t.sliderSize,e=t.borderWidth,u=t.handleRadius,o=t.padding,l=t.sliderShape,s="horizontal"===t.layoutDirection;return r=null!=(n=r)?n:2*o+2*u,"circle"===l?{handleStart:t.padding+t.handleRadius,handleRange:i-2*o-2*u,width:i,height:i,cx:i/2,cy:i/2,radius:i/2-e/2}:{handleStart:r/2,handleRange:i-r,radius:r/2,x:0,y:0,width:s?r:i,height:s?i:r}}function Y(t,n){var i=X(t),r=i.width,e=i.height,u=i.handleRange,o=i.handleStart,l="horizontal"===t.layoutDirection,s=l?r/2:e/2,c=o+function(t,n){var i=n.hsva,r=n.rgb;switch(t.sliderType){case"red":return r.r/2.55;case"green":return r.g/2.55;case"blue":return r.b/2.55;case"alpha":return 100*i.a;case"kelvin":var e=t.minTemperature,u=t.maxTemperature-e,o=(n.kelvin-e)/u*100;return Math.max(0,Math.min(o,100));case"hue":return i.h/=3.6;case"saturation":return i.s;case"value":default:return i.v}}(t,n)/100*u;return l&&(c=-1*c+u+2*o),{x:l?s:c,y:l?c:s}}var tt,nt=2*Math.PI,it=function(t,n){return(t%n+n)%n},rt=function(t,n){return Math.sqrt(t*t+n*n)};function et(t){return t.width/2-t.padding-t.handleRadius-t.borderWidth}function ut(t){var n=t.width/2;return{width:t.width,radius:n-t.borderWidth,cx:n,cy:n}}function ot(t,n,i){var r=t.wheelAngle,e=t.wheelDirection;return i&&"clockwise"===e?n=r+n:"clockwise"===e?n=360-r+n:i&&"anticlockwise"===e?n=r+180-n:"anticlockwise"===e&&(n=r-n),it(n,360)}function lt(t,n,i){var r=ut(t),e=r.cx,u=r.cy,o=et(t);n=e-n,i=u-i;var l=ot(t,Math.atan2(-i,-n)*(360/nt)),s=Math.min(rt(n,i),o);return{h:Math.round(l),s:Math.round(100/o*s)}}function st(t){var n=t.width,i=t.boxHeight;return{width:n,height:null!=i?i:n,radius:t.padding+t.handleRadius}}function ct(t,n,i){var r=st(t),e=r.width,u=r.height,o=r.radius,l=(n-o)/(e-2*o)*100,s=(i-o)/(u-2*o)*100;return{s:Math.max(0,Math.min(l,100)),v:Math.max(0,Math.min(100-s,100))}}function at(t,n,i,r){for(var e=0;e WLED Live Preview ================================================ FILE: wled00/data/liveviewws2D.htm ================================================ WLED Live Preview ================================================ FILE: wled00/data/msg.htm ================================================ WLED Message

Sample Message.

Sample Detail. ================================================ FILE: wled00/data/pixart/boxdraw.js ================================================ function drawBoxes(inputPixelArray, widthPixels, heightPixels) { var w = window; // Get the canvas context var ctx = canvas.getContext('2d', { willReadFrequently: true }); // Set the width and height of the canvas if (w.innerHeight < w.innerWidth) { canvas.width = Math.floor(w.innerHeight * 0.98); } else{ canvas.width = Math.floor(w.innerWidth * 0.98); } //canvas.height = w.innerWidth; let pixelSize = Math.floor(canvas.width/widthPixels); let xOffset = (w.innerWidth - (widthPixels * pixelSize))/2 //Set the canvas height to fit the right number of pixelrows canvas.height = (pixelSize * heightPixels) + 10 //Iterate through the matrix for (let y = 0; y < heightPixels; y++) { for (let x = 0; x < widthPixels; x++) { // Calculate the index of the current pixel let i = (y*widthPixels) + x; //Gets the RGB of the current pixel let pixel = inputPixelArray[i]; let pixelColor = 'rgb(' + pixel[0] + ', ' + pixel[1] + ', ' + pixel[2] + ')'; let textColor = 'rgb(128,128,128)'; // Set the fill style to the pixel color ctx.fillStyle = pixelColor; //Draw the rectangle ctx.fillRect(x * pixelSize, y * pixelSize, pixelSize, pixelSize); // Draw a border on the box ctx.strokeStyle = '#888888'; ctx.lineWidth = 1; ctx.strokeRect(x * pixelSize, y * pixelSize, pixelSize, pixelSize); //Write text to box ctx.font = "10px Arial"; ctx.fillStyle = textColor; ctx.textAlign = "center"; ctx.textBaseline = 'middle'; ctx.fillText((pixel[4] + 1), (x * pixelSize) + (pixelSize /2), (y * pixelSize) + (pixelSize /2)); } } var imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); ctx.clearRect(0, 0, canvas.width, canvas.height); canvas.width = w.innerWidth; ctx.putImageData(imageData, xOffset, 0); } ================================================ FILE: wled00/data/pixart/getPixelValues.js ================================================ function getPixelRGBValues(base64Image) { httpArray = []; fileJSON = `{"on":true,"bri":${brgh.value},"seg":{"id":${tSg.value},"i":[`; //Which object holds the secret to the segment ID let segID = 0; if(tSg.style.display == "flex"){ segID = tSg.value } else { segID = sID.value; } //const copyJSONledbutton = gId('copyJSONledbutton'); const maxNoOfColorsInCommandSting = parseInt(cLN.value); let hybridAddressing = false; let selectedIndex = -1; selectedIndex = frm.selectedIndex; const formatSelection = frm.options[selectedIndex].value; selectedIndex = lSS.selectedIndex; const ledSetupSelection = lSS.options[selectedIndex].value; selectedIndex = cFS.selectedIndex; let hexValueCheck = true; if (cFS.options[selectedIndex].value == 'dec'){ hexValueCheck = false } selectedIndex = aS.selectedIndex; let segmentValueCheck = true; //If Range or Hybrid if (aS.options[selectedIndex].value == 'single'){ segmentValueCheck = false } else if (aS.options[selectedIndex].value == 'hybrid'){ hybridAddressing = true; } let curlString = '' let haString = '' let colorSeparatorStart = '"'; let colorSeparatorEnd = '"'; if (!hexValueCheck){ colorSeparatorStart = '['; colorSeparatorEnd = ']'; } // Warnings let hasTransparency = false; //If alpha < 255 is detected on any pixel, this is set to true in code below let imageInfo = ''; // Create an off-screen canvas var canvas = cE('canvas'); var context = canvas.getContext('2d', { willReadFrequently: true }); // Create an image element and set its src to the base64 image var image = new Image(); image.src = base64Image; // Wait for the image to load before drawing it onto the canvas image.onload = function() { let scalePath = scDiv.children[0].children[0]; let color = scalePath.getAttribute("fill"); let sizeX = szX.value; let sizeY = szY.value; if (color != accentColor || sizeX < 1 || sizeY < 1){ //image will not be resized Set desired size to original size sizeX = image.width; sizeY = image.height; //failsafe for not generating huge images automatically if (image.width > 512 || image.height > 512) { sizeX = 16; sizeY = 16; } } // Set the canvas size to the same as the desired image size canvas.width = sizeX; canvas.height = sizeY; imageInfo = '

Width: ' + sizeX + ', Height: ' + sizeY + ' (make sure this matches your led matrix setup)

' // Draw the image onto the canvas context.drawImage(image, 0, 0, sizeX, sizeY); // Get the pixel data from the canvas var pixelData = context.getImageData(0, 0, sizeX, sizeY).data; // Create an array to hold the RGB values of each pixel var pixelRGBValues = []; // If the first row of the led matrix is right -> left let right2leftAdjust = 1; if (ledSetupSelection == 'l2r'){ right2leftAdjust = 0; } // Loop through the pixel data and get the RGB values of each pixel for (var i = 0; i < pixelData.length; i += 4) { var r = pixelData[i]; var g = pixelData[i + 1]; var b = pixelData[i + 2]; var a = pixelData[i + 3]; let pixel = i/4 let row = Math.floor(pixel/sizeX); let led = pixel; if (ledSetupSelection == 'matrix'){ //Do nothing, the matrix is set upp like the index in the image //Every row starts from the left, i.e. no zigzagging } else if ((row + right2leftAdjust) % 2 === 0) { //Setup is traditional zigzag //right2leftAdjust basically flips the row order if = 1 //Row is left to right //Leave led index as pixel index } else { //Setup is traditional zigzag //Row is right to left //Invert index of row for led let indexOnRow = led - (row * sizeX); let maxIndexOnRow = sizeX - 1; let reversedIndexOnRow = maxIndexOnRow - indexOnRow; led = (row * sizeX) + reversedIndexOnRow; } // Add the RGB values to the pixel RGB values array pixelRGBValues.push([r, g, b, a, led, pixel, row]); } pixelRGBValues.sort((a, b) => a[5] - b[5]); //Copy the values to a new array for resorting let ledRGBValues = [... pixelRGBValues]; //Sort the array based on led index ledRGBValues.sort((a, b) => a[4] - b[4]); //Generate JSON in WLED format let JSONledString = ''; //Set starting values for the segment check to something that is no color let segmentStart = -1; let maxi = ledRGBValues.length; let curentColorIndex = 0 let commandArray = []; //For every pixel in the LED array for (let i = 0; i < maxi; i++) { let pixel = ledRGBValues[i]; let r = pixel[0]; let g = pixel[1]; let b = pixel[2]; let a = pixel[3]; let segmentString = ''; let segmentEnd = -1; if(segmentValueCheck){ if (segmentStart < 0){ //This is the first led of a new segment segmentStart = i; } //Else we allready have a start index if (i < maxi - 1){ let iNext = i + 1; let nextPixel = ledRGBValues[iNext]; if (nextPixel[0] != r || nextPixel[1] != g || nextPixel[2] != b ){ //Next pixel has new color //The current segment ends with this pixel segmentEnd = i + 1 //WLED wants the NEXT LED as the stop led... if (segmentStart == i && hybridAddressing){ //If only one led/pixel, no segment info needed if (JSONledString == ''){ //If addressing is single, we need to start every command with a starting possition segmentString = '' + i + ','; //Fixed to b2 } else{ segmentString = '' } } else { segmentString = segmentStart + ',' + segmentEnd + ','; } } } else { //This is the last pixel, so the segment must end segmentEnd = i + 1; if (segmentStart + 1 == segmentEnd && hybridAddressing){ //If only one led/pixel, no segment info needed if (JSONledString == ''){ //If addressing is single, we need to start every command with a starting possition segmentString = '' + i + ','; //Fixed to b2 } else{ segmentString = '' } } else { segmentString = segmentStart + ',' + segmentEnd + ','; } } } else{ //Write every pixel if (JSONledString == ''){ //If addressing is single, we need to start every command with a starting possition JSONledString = i //Fixed to b2 } segmentStart = i segmentEnd = i //Segment string should be empty for when addressing single. So no need to set it again. } if (a < 255){ hasTransparency = true; //If ANY pixel has alpha < 255 then this is set to true to warn the user } if (segmentEnd > -1){ //This is the last pixel in the segment, write to the JSONledString //Return color value in selected format let colorValueString = r + ',' + g + ',' + b ; if (hexValueCheck){ const [red, green, blue] = [r, g, b]; colorValueString = `${[red, green, blue].map(x => x.toString(16).padStart(2, '0')).join('')}`; } else{ //do nothing, allready set } // Check if start and end is the same, in which case remove JSONledString += segmentString + colorSeparatorStart + colorValueString + colorSeparatorEnd; fileJSON = JSONledString + segmentString + colorSeparatorStart + colorValueString + colorSeparatorEnd; curentColorIndex = curentColorIndex + 1; // We've just added a new color to the string so up the count with one if (curentColorIndex % maxNoOfColorsInCommandSting === 0 || i == maxi - 1) { //If we have accumulated the max number of colors to send in a single command or if this is the last pixel, we should write the current colorstring to the array commandArray.push(JSONledString); JSONledString = ''; //Start on an new command string } else { //Add a comma to continue the command string JSONledString = JSONledString + ',' } //Reset segment values segmentStart = - 1; } } JSONledString = '' //For every commandString in the array for (let i = 0; i < commandArray.length; i++) { let thisJSONledString = `{"on":true,"bri":${brgh.value},"seg":{"id":${segID},"i":[${commandArray[i]}]}}`; httpArray.push(thisJSONledString); let thiscurlString = `curl -X POST "http://${gurl.value}/json/state" -d \'${thisJSONledString}\' -H "Content-Type: application/json"`; //Aggregated Strings That should be returned to the user if (i > 0){ JSONledString = JSONledString + '\n\n'; curlString = curlString + ' && '; } JSONledString += thisJSONledString; curlString += thiscurlString; } haString = `#Uncomment if you don\'t allready have these defined in your switch section of your configuration.yaml #- platform: command_line #switches: ${haIDe.value} friendly_name: ${haNe.value} unique_id: ${haUe.value} command_on: > ${curlString} command_off: > curl -X POST "http://${gurl.value}/json/state" -d \'{"on":false}\' -H "Content-Type: application/json"`; if (formatSelection == 'wled'){ JLD.value = JSONledString; } else if (formatSelection == 'curl'){ JLD.value = curlString; } else if (formatSelection == 'ha'){ JLD.value = haString; } else { JLD.value = 'ERROR!/n' + formatSelection + ' is an unknown format.' } fileJSON += ']}}'; let infoDiv = imin; let canvasDiv = imin; if (hasTransparency){ imageInfo = imageInfo + '

WARNING! Transparency info detected in image. Transparency (alpha) has been ignored. To ensure you get the result you desire, use only solid colors in your image.

' } infoDiv.innerHTML = imageInfo; canvasDiv.style.display = "block" //Drawing the image drawBoxes(pixelRGBValues, sizeX, sizeY); } } ================================================ FILE: wled00/data/pixart/pixart.css ================================================ .box { border: 2px solid #fff; } body { font-family: Arial, sans-serif; background-color: #111; } .top-part { width: 600px; margin: 0 auto; } .container { max-width: 100% -40px; border-radius: 0px; padding: 20px; text-align: center; } h1 { font-size: 2.3em; color: #ddd; margin: 1px 0; font-family: Arial, sans-serif; line-height: 0.5; /*text-align: center;*/ } h2 { font-size: 1.1em; color: rgba(221, 221, 221, 0.61); margin: 1px 0; font-family: Arial, sans-serif; line-height: 0.5; text-align: center; } h3 { font-size: 0.7em; color: rgba(221, 221, 221, 0.61); margin: 1px 0; font-family: Arial, sans-serif; line-height: 1.4; text-align: center; align-items: center; justify-content: center; display: flex; } p { font-size: 1em; color: #777; line-height: 1.5; font-family: Arial, sans-serif; } #fieldTable { font-size: 1 em; color: #777; line-height: 1; font-family: Arial, sans-serif; } #scaleTable { font-size: 1 em; color: #777; line-height: 1; font-family: Arial, sans-serif; } #drop-zone { display: block; width: 100%-40px; border: 3px dashed #ddd; border-radius: 0px; text-align: center; padding: 20px; margin: 0px; cursor: pointer; font-family: Arial, sans-serif; font-size: 15px; color: #777; } #file-picker { display: none; } .adaptiveTD{ display: flex; flex-direction: row; flex-wrap: nowrap; align-items: center; } .mainSelector { background-color: #222; color: #ddd; border: 1px solid #333; margin-top: 4px; margin-bottom: 4px; padding: 0 8px; height: 28px; font-size: 15px; border-radius: 7px; flex-grow: 1; display: flex; align-items: center; justify-content: center; } .adaptiveSelector { background-color: #222; color: #ddd; border: 1px solid #333; margin-top: 4px; margin-bottom: 4px; padding: 0 8px; height: 28px; font-size: 15px; border-radius: 7px; flex-grow: 1; display: none; } .segmentsDiv{ width: 36px; padding-left: 5px; } * input[type=range] { appearance: none; -moz-appearance: none; -webkit-appearance: none; flex-grow: 1; padding: 0; margin: 4px 8px 4px 0; background-color: transparent; cursor: pointer; background: linear-gradient(to right, #bbb 50%, #333 50%); border-radius: 7px; } input[type=range]:focus { outline: none; } input[type=range]::-webkit-slider-runnable-track { height: 28px; cursor: pointer; background: transparent; border-radius: 7px; } input[type=range]::-webkit-slider-thumb { height: 16px; width: 16px; border-radius: 50%; background: #fff; cursor: pointer; -webkit-appearance: none; margin-top: 4px; border-radius: 7px; } input[type=range]::-moz-range-track { height: 28px; background-color: rgba(0, 0, 0, 0); border-radius: 7px; } input[type=range]::-moz-range-thumb { border: 0px solid rgba(0, 0, 0, 0); height: 16px; width: 16px; border-radius: 7px; background: #fff; } .rangeNumber{ width: 20px; vertical-align: middle; } .fullTextField[type=text] { background-color: #222; border: 1px solid #333; padding-inline-start: 5px; margin-top: 4px; margin-bottom: 4px; height: 24px; border-radius: 0px; font-family: Arial, sans-serif; font-size: 15px; color: #ddd; border-radius: 7px; flex-grow: 1; display: flex; align-items: center; justify-content: center; } .flxTFld{ background-color: #222; border: 1px solid #333; padding-inline-start: 5px; height: 24px; border-radius: 0px; font-family: Arial, sans-serif; font-size: 15px; color: #ddd; border-radius: 7px; flex-grow: 1; display: flex; align-items: center; justify-content: center; } * input[type=submit] { background-color: #222; border: 1px solid #333; padding: 0.5em; width: 100%; border-radius: 24px; font-family: Arial, sans-serif; font-size: 1.3em; color: #ddd; } * button { background-color: #222; border: 1px solid #333; padding-inline: 5px; width: 100%; border-radius: 24px; font-family: Arial, sans-serif; font-size: 1em; color: #ddd; display: flex; align-items: center; justify-content: center; cursor: pointer; } #scaleDiv { display: flex; align-items: center; vertical-align: middle; } textarea { grid-row: 1 / 2; width: 100%; height: 200px; background-color: #222; border: 1px solid #333; color: #ddd; } .hide { display: none; } .svg-icon { vertical-align: middle; } #image-container { display: grid; grid-template-rows: 1fr 1fr; } #button-container { display: flex; padding-bottom: 10px; padding-top: 10px; } .buttonclass { flex: 1; padding-top: 5px; padding-bottom: 5px; } .gap { width: 10px; } #submitConvert::before { content: ""; display: inline-block; background-image: url('data:image/svg+xml;utf8, '); width: 36px; height: 36px; } #sizeDiv * { display: inline-block; } .sizeInputFields{ width: 50px; background-color: #222; border: 1px solid #333; padding-inline-start: 5px; margin-top: -5px; height: 24px; border-radius: 7px; font-family: Arial, sans-serif; font-size: 15px; color: #ddd; } a:link { color: rgba(221, 221, 221, 0.61); background-color: transparent; text-decoration: none; } a:visited { color: rgba(221, 221, 221, 0.61); background-color: transparent; text-decoration: none; } a:hover { color: #ddd; background-color: transparent; text-decoration: none; } a:active { color: rgba(221, 221, 221, 0.61); background-color: transparent; text-decoration: none; } ================================================ FILE: wled00/data/pixart/pixart.htm ================================================ WLED Pixel Art Converter

WLED Pixel Art Converter

Convert image to WLED JSON (pixel art on WLED matrix)

128
256
 Scale image

Drop image here
or
Click to select a file

Version 1.0.8
 -  Help/About

================================================ FILE: wled00/data/pixart/pixart.js ================================================ //Start up code //if (window.location.protocol == "file:") { // let locip = prompt("File Mode. Please enter WLED IP!"); // gId('curlUrl').value = locip; //} else // //Start up code let devMode = false; //Remove gurl.value = location.host; const urlParams = new URLSearchParams(window.location.search); if (gurl.value.length < 1){ gurl.value = "Missing_Host"; } function gen(){ //Generate image if enough info is in place //Is host non empty //Is image loaded //is scale > 0 if (((szX.value > 0 && szY.value > 0) || szDiv.style.display == 'none') && gurl.value.length > 0 && prw.style.display != 'none'){ //regenerate let base64Image = prw.src; if (isValidBase64Gif(base64Image)) { im.src = base64Image; getPixelRGBValues(base64Image); imcn.style.display = "block"; bcn.style.display = ""; } else { let imageInfo = '

WARNING! File does not appear to be a valid image

'; imin.innerHTML = imageInfo; imin.style.display = "block"; imcn.style.display = "none"; JLD.value = ''; if (devMode) console.log("The string '" + base64Image + "' is not a valid base64 image."); } } if(gurl.value.length > 0){ gId("sSg").setAttribute("fill", accentColor); } else{ gId("sSg").setAttribute("fill", accentTextColor); let ts = tSg; ts.style.display = "none"; ts.innerHTML = ""; sID.style.display = "flex"; } } // Code for copying the generated string to clipboard cjb.addEventListener('click', async () => { let JSONled = JLD; JSONled.select(); try { await navigator.clipboard.writeText(JSONled.value); } catch (err) { try { await d.execCommand("copy"); } catch (err) { console.error('Failed to copy text: ', err); } } }); // Event listeners ======================= lSS.addEventListener("change", gen); szY.addEventListener("change", gen); szX.addEventListener("change", gen); cFS.addEventListener("change", gen); aS.addEventListener("change", gen); brgh.addEventListener("change", gen); cLN.addEventListener("change", gen); haIDe.addEventListener("change", gen); haUe.addEventListener("change", gen); haNe.addEventListener("change", gen); gurl.addEventListener("change", gen); sID.addEventListener("change", gen); prw.addEventListener("load", gen); //gId("convertbutton").addEventListener("click", gen); tSg.addEventListener("change", () => { sop = tSg.options[tSg.selectedIndex]; szX.value = sop.dataset.x; szY.value = sop.dataset.y; gen(); }); gId("sendJSONledbutton").addEventListener('click', async () => { if (window.location.protocol === "https:") { alert('Will only be available when served over http (or WLED is run over https)'); } else { postPixels(); } }); brgh.oninput = () => { brgV.textContent = brgh.value; let perc = parseInt(brgh.value)*100/255; var val = `linear-gradient(90deg, #bbb ${perc}%, #333 ${perc}%)`; brgh.style.backgroundImage = val; } cLN.oninput = () => { let cln = cLN; cLV.textContent = cln.value; let perc = parseInt(cln.value)*100/512; var val = `linear-gradient(90deg, #bbb ${perc}%, #333 ${perc}%)`; cln.style.backgroundImage = val; } frm.addEventListener("change", () => { for (var i = 0; i < hideableRows.length; i++) { hideableRows[i].classList.toggle("hide", frm.value !== "ha"); gen(); } }); async function postPixels() { let ss = gId("sendSvgP"); ss.setAttribute("fill", prsCol); let er = false; for (let i of httpArray) { try { if (devMode) console.log(i); if (devMode) console.log(i.length); const response = await fetch('http://'+gId('curlUrl').value+'/json/state', { method: 'POST', headers: { 'Content-Type': 'application/json' //'Content-Type': 'text/html; charset=UTF-8' }, body: i }); const data = await response.json(); if (devMode) console.log(data); } catch (error) { console.error(error); er = true; } } if(er){ //Something went wrong ss.setAttribute("fill", redColor); setTimeout(function(){ ss.setAttribute("fill", accentTextColor); }, 1000); } else { // A, OK ss.setAttribute("fill", greenColor); setTimeout(function(){ ss.setAttribute("fill", accentColor); }, 1000); } } //File uploader code const dropZone = gId('drop-zone'); const filePicker = gId('file-picker'); const preview = prw; // Listen for dragenter, dragover, and drop events dropZone.addEventListener('dragenter', dragEnter); dropZone.addEventListener('dragover', dragOver); dropZone.addEventListener('drop', dropped); dropZone.addEventListener('click', zoneClicked); // Listen for change event on file picker filePicker.addEventListener('change', filePicked); // Handle zone click function zoneClicked(e) { e.preventDefault(); //this.classList.add('drag-over'); //alert('Hej'); filePicker.click(); } // Handle dragenter function dragEnter(e) { e.preventDefault(); this.classList.add('drag-over'); } // Handle dragover function dragOver(e) { e.preventDefault(); } // Handle drop function dropped(e) { e.preventDefault(); this.classList.remove('drag-over'); // Get the dropped file const file = e.dataTransfer.files[0]; updatePreview(file) } // Handle file picked function filePicked(e) { // Get the picked file const file = e.target.files[0]; updatePreview(file) } // Update the preview image function updatePreview(file) { // Use FileReader to read the file const reader = new FileReader(); reader.onload = () => { // Update the preview image preview.src = reader.result; //gId("submitConvertDiv").style.display = ""; prw.style.display = ""; }; reader.readAsDataURL(file); } function isValidBase64Gif(string) { // Use a regular expression to check that the string is a valid base64 string /* const base64gifPattern = /^data:image\/gif;base64,([A-Za-z0-9+/:]{4})*([A-Za-z0-9+/:]{3}=|[A-Za-z0-9+/:]{2}==)?$/; const base64pngPattern = /^data:image\/png;base64,([A-Za-z0-9+/:]{4})*([A-Za-z0-9+/:]{3}=|[A-Za-z0-9+/:]{2}==)?$/; const base64jpgPattern = /^data:image\/jpg;base64,([A-Za-z0-9+/:]{4})*([A-Za-z0-9+/:]{3}=|[A-Za-z0-9+/:]{2}==)?$/; const base64webpPattern = /^data:image\/webp;base64,([A-Za-z0-9+/:]{4})*([A-Za-z0-9+/:]{3}=|[A-Za-z0-9+/:]{2}==)?$/; */ //REMOVED, Any image appear to work as long as it can be drawn to the canvas. Leaving code in for future use, possibly if (1==1 || base64gifPattern.test(string) || base64pngPattern.test(string) || base64jpgPattern.test(string) || base64webpPattern.test(string)) { return true; } else { //Not OK return false; } } var hideableRows = d.querySelectorAll(".ha-hide"); for (var i = 0; i < hideableRows.length; i++) { hideableRows[i].classList.add("hide"); } frm.addEventListener("change", () => { for (var i = 0; i < hideableRows.length; i++) { hideableRows[i].classList.toggle("hide", frm.value !== "ha"); } }); function switchScale() { //let scalePath = gId("scaleDiv").children[1].children[0] let scaleTogglePath = scDiv.children[0].children[0] let color = scaleTogglePath.getAttribute("fill"); let d = ''; if (color === accentColor) { color = accentTextColor; d = scaleToggleOffd; szDiv.style.display = "none"; // Set values to actual XY of image, if possible } else { color = accentColor; d = scaleToggleOnd; szDiv.style.display = ""; } //scalePath.setAttribute("fill", color); scaleTogglePath.setAttribute("fill", color); scaleTogglePath.setAttribute("d", d); gen(); } function generateSegmentOptions(array) { //This function is prepared for a name property on each segment for easier selection //Currently the name is generated generically based on index tSg.innerHTML = ""; for (var i = 0; i < array.length; i++) { var option = cE("option"); option.value = array[i].value; option.text = array[i].text; option.dataset.x = array[i].x; option.dataset.y = array[i].y; tSg.appendChild(option); if(i === 0) { option.selected = true; szX.value = option.dataset.x; szY.value = option.dataset.y; } } } // Get segments from device async function getSegments() { cv = gurl.value; if (cv.length > 0 ){ try { var arr = []; const response = await fetch('http://'+cv+'/json/state'); const json = await response.json(); let ids = json.seg.map(sg => ({id: sg.id, n: sg.n, xs: sg.start, xe: sg.stop, ys: sg.startY, ye: sg.stopY})); for (var i = 0; i < ids.length; i++) { arr.push({ value: ids[i]["id"], text: ids[i]["n"] + ' (index: ' + ids[i]["id"] + ')', x: ids[i]["xe"] - ids[i]["xs"], y: ids[i]["ye"] - ids[i]["ys"] }); } generateSegmentOptions(arr); tSg.style.display = "flex"; sID.style.display = "none"; gId("sSg").setAttribute("fill", greenColor); setTimeout(function(){ gId("sSg").setAttribute("fill", accentColor); }, 1000); } catch (error) { console.error(error); gId("sSg").setAttribute("fill", redColor); setTimeout(function(){ gId("sSg").setAttribute("fill", accentColor); }, 1000); tSg.style.display = "none"; sID.style.display = "flex"; } } else{ gId("sSg").setAttribute("fill", redColor); setTimeout(function(){ gId("sSg").setAttribute("fill", accentTextColor); }, 1000); tSg.style.display = "none"; sID.style.display = "flex"; } } //Initial population of segment selection function generateSegmentArray(noOfSegments) { var arr = []; for (var i = 0; i < noOfSegments; i++) { arr.push({ value: i, text: "Segment index " + i }); } return arr; } var segmentData = generateSegmentArray(10); generateSegmentOptions(segmentData); seDiv.innerHTML = '' /*gId("convertbutton").innerHTML = '   Convert to WLED JSON '; */ cjb.innerHTML = '   Copy to clipboard'; gId("sendJSONledbutton").innerHTML = '   Send to device'; //After everything is loaded, check if we have a possible IP/host if(gurl.value.length > 0){ // Needs to be addressed directly here so the object actually exists gId("sSg").setAttribute("fill", accentColor); } ================================================ FILE: wled00/data/pixart/site.webmanifest ================================================ { "name": "WLED Pixel Art Convertor", "short_name": "ledconv", "icons": [ { "src": "/favicon-32x32.png", "sizes": "32x322", "type": "image/png" }, { "src": "/favicon-32x32.png", "sizes": "32x32", "type": "image/png" } ], "theme_color": "#ffffff", "background_color": "#ffffff", "display": "standalone" } ================================================ FILE: wled00/data/pixart/statics.js ================================================ //elements var gurl = gId('curlUrl'); var szX = gId("sizeX"); var szY = gId("sizeY"); var szDiv = gId("sizeDiv"); var prw = gId("preview"); var sID = gId('segID'); var JLD = gId('JSONled'); var tSg = gId('targetSegment'); var brgh = gId("brightnessNumber"); var seDiv = gId("getSegmentsDiv") var cjb = gId("copyJSONledbutton"); var frm = gId("formatSelector"); var cLN = gId("colorLimitNumber"); var haIDe = gId("haID"); var haUe = gId("haUID"); var haNe = gId("haName"); var aS = gId("addressingSelector"); var cFS = gId("colorFormatSelector"); var lSS = gId("ledSetupSelector"); var imin = gId('image-info'); var imcn = gId('image-container'); var bcn = gId("button-container"); var im = gId('image'); //var ss = gId("sendSvgP"); var scDiv = gId("scaleDiv"); var w = window; var canvas = gId('pixelCanvas'); var brgV = gId("brightnessValue"); var cLV = gId("colorLimitValue") //vars var httpArray = []; var fileJSON = ''; var hideableRows = d.querySelectorAll(".ha-hide"); for (var i = 0; i < hideableRows.length; i++) { hideableRows[i].classList.add("hide"); } var accentColor = '#eee'; var accentTextColor = '#777'; var prsCol = '#ccc'; var greenColor = '#056b0a'; var redColor = '#6b050c'; var scaleToggleOffd = "M17,7H7A5,5 0 0,0 2,12A5,5 0 0,0 7,17H17A5,5 0 0,0 22,12A5,5 0 0,0 17,7M7,15A3,3 0 0,1 4,12A3,3 0 0,1 7,9A3,3 0 0,1 10,12A3,3 0 0,1 7,15Z"; var scaleToggleOnd = "M17,7H7A5,5 0 0,0 2,12A5,5 0 0,0 7,17H17A5,5 0 0,0 22,12A5,5 0 0,0 17,7M17,15A3,3 0 0,1 14,12A3,3 0 0,1 17,9A3,3 0 0,1 20,12A3,3 0 0,1 17,15Z"; var sSg = gId("getSegmentsSVGpath"); ================================================ FILE: wled00/data/pixelforge/omggif.js ================================================ // (c) Dean McNamee , 2013. // // https://github.com/deanm/omggif // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to // deal in the Software without restriction, including without limitation the // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or // sell copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS // IN THE SOFTWARE. // // omggif is a JavaScript implementation of a GIF 89a encoder and decoder, // including animation and compression. It does not rely on any specific // underlying system, so should run in the browser, Node, or Plask. "use strict"; function GifWriter(buf, width, height, gopts) { var p = 0; var gopts = gopts === undefined ? { } : gopts; var loop_count = gopts.loop === undefined ? null : gopts.loop; var global_palette = gopts.palette === undefined ? null : gopts.palette; if (width <= 0 || height <= 0 || width > 65535 || height > 65535) throw new Error("Width/Height invalid."); function check_palette_and_num_colors(palette) { var num_colors = palette.length; if (num_colors < 2 || num_colors > 256 || num_colors & (num_colors-1)) { throw new Error( "Invalid code/color length, must be power of 2 and 2 .. 256."); } return num_colors; } // - Header. buf[p++] = 0x47; buf[p++] = 0x49; buf[p++] = 0x46; // GIF buf[p++] = 0x38; buf[p++] = 0x39; buf[p++] = 0x61; // 89a // Handling of Global Color Table (palette) and background index. var gp_num_colors_pow2 = 0; var background = 0; if (global_palette !== null) { var gp_num_colors = check_palette_and_num_colors(global_palette); while (gp_num_colors >>= 1) ++gp_num_colors_pow2; gp_num_colors = 1 << gp_num_colors_pow2; --gp_num_colors_pow2; if (gopts.background !== undefined) { background = gopts.background; if (background >= gp_num_colors) throw new Error("Background index out of range."); // The GIF spec states that a background index of 0 should be ignored, so // this is probably a mistake and you really want to set it to another // slot in the palette. But actually in the end most browsers, etc end // up ignoring this almost completely (including for dispose background). if (background === 0) throw new Error("Background index explicitly passed as 0."); } } // - Logical Screen Descriptor. // NOTE(deanm): w/h apparently ignored by implementations, but set anyway. buf[p++] = width & 0xff; buf[p++] = width >> 8 & 0xff; buf[p++] = height & 0xff; buf[p++] = height >> 8 & 0xff; // NOTE: Indicates 0-bpp original color resolution (unused?). buf[p++] = (global_palette !== null ? 0x80 : 0) | // Global Color Table Flag. gp_num_colors_pow2; // NOTE: No sort flag (unused?). buf[p++] = background; // Background Color Index. buf[p++] = 0; // Pixel aspect ratio (unused?). // - Global Color Table if (global_palette !== null) { for (var i = 0, il = global_palette.length; i < il; ++i) { var rgb = global_palette[i]; buf[p++] = rgb >> 16 & 0xff; buf[p++] = rgb >> 8 & 0xff; buf[p++] = rgb & 0xff; } } if (loop_count !== null) { // Netscape block for looping. if (loop_count < 0 || loop_count > 65535) throw new Error("Loop count invalid.") // Extension code, label, and length. buf[p++] = 0x21; buf[p++] = 0xff; buf[p++] = 0x0b; // NETSCAPE2.0 buf[p++] = 0x4e; buf[p++] = 0x45; buf[p++] = 0x54; buf[p++] = 0x53; buf[p++] = 0x43; buf[p++] = 0x41; buf[p++] = 0x50; buf[p++] = 0x45; buf[p++] = 0x32; buf[p++] = 0x2e; buf[p++] = 0x30; // Sub-block buf[p++] = 0x03; buf[p++] = 0x01; buf[p++] = loop_count & 0xff; buf[p++] = loop_count >> 8 & 0xff; buf[p++] = 0x00; // Terminator. } var ended = false; this.addFrame = function(x, y, w, h, indexed_pixels, opts) { if (ended === true) { --p; ended = false; } // Un-end. opts = opts === undefined ? { } : opts; // TODO(deanm): Bounds check x, y. Do they need to be within the virtual // canvas width/height, I imagine? if (x < 0 || y < 0 || x > 65535 || y > 65535) throw new Error("x/y invalid.") if (w <= 0 || h <= 0 || w > 65535 || h > 65535) throw new Error("Width/Height invalid.") if (indexed_pixels.length < w * h) throw new Error("Not enough pixels for the frame size."); var using_local_palette = true; var palette = opts.palette; if (palette === undefined || palette === null) { using_local_palette = false; palette = global_palette; } if (palette === undefined || palette === null) throw new Error("Must supply either a local or global palette."); var num_colors = check_palette_and_num_colors(palette); // Compute the min_code_size (power of 2), destroying num_colors. var min_code_size = 0; while (num_colors >>= 1) ++min_code_size; num_colors = 1 << min_code_size; // Now we can easily get it back. var delay = opts.delay === undefined ? 0 : opts.delay; // From the spec: // 0 - No disposal specified. The decoder is // not required to take any action. // 1 - Do not dispose. The graphic is to be left // in place. // 2 - Restore to background color. The area used by the // graphic must be restored to the background color. // 3 - Restore to previous. The decoder is required to // restore the area overwritten by the graphic with // what was there prior to rendering the graphic. // 4-7 - To be defined. // NOTE(deanm): Dispose background doesn't really work, apparently most // browsers ignore the background palette index and clear to transparency. var disposal = opts.disposal === undefined ? 0 : opts.disposal; if (disposal < 0 || disposal > 3) // 4-7 is reserved. throw new Error("Disposal out of range."); var use_transparency = false; var transparent_index = 0; if (opts.transparent !== undefined && opts.transparent !== null) { use_transparency = true; transparent_index = opts.transparent; if (transparent_index < 0 || transparent_index >= num_colors) throw new Error("Transparent color index."); } if (disposal !== 0 || use_transparency || delay !== 0) { // - Graphics Control Extension buf[p++] = 0x21; buf[p++] = 0xf9; // Extension / Label. buf[p++] = 4; // Byte size. buf[p++] = disposal << 2 | (use_transparency === true ? 1 : 0); buf[p++] = delay & 0xff; buf[p++] = delay >> 8 & 0xff; buf[p++] = transparent_index; // Transparent color index. buf[p++] = 0; // Block Terminator. } // - Image Descriptor buf[p++] = 0x2c; // Image Seperator. buf[p++] = x & 0xff; buf[p++] = x >> 8 & 0xff; // Left. buf[p++] = y & 0xff; buf[p++] = y >> 8 & 0xff; // Top. buf[p++] = w & 0xff; buf[p++] = w >> 8 & 0xff; buf[p++] = h & 0xff; buf[p++] = h >> 8 & 0xff; // NOTE: No sort flag (unused?). // TODO(deanm): Support interlace. buf[p++] = using_local_palette === true ? (0x80 | (min_code_size-1)) : 0; // - Local Color Table if (using_local_palette === true) { for (var i = 0, il = palette.length; i < il; ++i) { var rgb = palette[i]; buf[p++] = rgb >> 16 & 0xff; buf[p++] = rgb >> 8 & 0xff; buf[p++] = rgb & 0xff; } } p = GifWriterOutputLZWCodeStream( buf, p, min_code_size < 2 ? 2 : min_code_size, indexed_pixels); return p; }; this.end = function() { if (ended === false) { buf[p++] = 0x3b; // Trailer. ended = true; } return p; }; this.getOutputBuffer = function() { return buf; }; this.setOutputBuffer = function(v) { buf = v; }; this.getOutputBufferPosition = function() { return p; }; this.setOutputBufferPosition = function(v) { p = v; }; } // Main compression routine, palette indexes -> LZW code stream. // |index_stream| must have at least one entry. function GifWriterOutputLZWCodeStream(buf, p, min_code_size, index_stream) { buf[p++] = min_code_size; var cur_subblock = p++; // Pointing at the length field. var clear_code = 1 << min_code_size; var code_mask = clear_code - 1; var eoi_code = clear_code + 1; var next_code = eoi_code + 1; var cur_code_size = min_code_size + 1; // Number of bits per code. var cur_shift = 0; // We have at most 12-bit codes, so we should have to hold a max of 19 // bits here (and then we would write out). var cur = 0; function emit_bytes_to_buffer(bit_block_size) { while (cur_shift >= bit_block_size) { buf[p++] = cur & 0xff; cur >>= 8; cur_shift -= 8; if (p === cur_subblock + 256) { // Finished a subblock. buf[cur_subblock] = 255; cur_subblock = p++; } } } function emit_code(c) { cur |= c << cur_shift; cur_shift += cur_code_size; emit_bytes_to_buffer(8); } // I am not an expert on the topic, and I don't want to write a thesis. // However, it is good to outline here the basic algorithm and the few data // structures and optimizations here that make this implementation fast. // The basic idea behind LZW is to build a table of previously seen runs // addressed by a short id (herein called output code). All data is // referenced by a code, which represents one or more values from the // original input stream. All input bytes can be referenced as the same // value as an output code. So if you didn't want any compression, you // could more or less just output the original bytes as codes (there are // some details to this, but it is the idea). In order to achieve // compression, values greater then the input range (codes can be up to // 12-bit while input only 8-bit) represent a sequence of previously seen // inputs. The decompressor is able to build the same mapping while // decoding, so there is always a shared common knowledge between the // encoding and decoder, which is also important for "timing" aspects like // how to handle variable bit width code encoding. // // One obvious but very important consequence of the table system is there // is always a unique id (at most 12-bits) to map the runs. 'A' might be // 4, then 'AA' might be 10, 'AAA' 11, 'AAAA' 12, etc. This relationship // can be used for an effecient lookup strategy for the code mapping. We // need to know if a run has been seen before, and be able to map that run // to the output code. Since we start with known unique ids (input bytes), // and then from those build more unique ids (table entries), we can // continue this chain (almost like a linked list) to always have small // integer values that represent the current byte chains in the encoder. // This means instead of tracking the input bytes (AAAABCD) to know our // current state, we can track the table entry for AAAABC (it is guaranteed // to exist by the nature of the algorithm) and the next character D. // Therefor the tuple of (table_entry, byte) is guaranteed to also be // unique. This allows us to create a simple lookup key for mapping input // sequences to codes (table indices) without having to store or search // any of the code sequences. So if 'AAAA' has a table entry of 12, the // tuple of ('AAAA', K) for any input byte K will be unique, and can be our // key. This leads to a integer value at most 20-bits, which can always // fit in an SMI value and be used as a fast sparse array / object key. // Output code for the current contents of the index buffer. var ib_code = index_stream[0] & code_mask; // Load first input index. var code_table = { }; // Key'd on our 20-bit "tuple". emit_code(clear_code); // Spec says first code should be a clear code. // First index already loaded, process the rest of the stream. for (var i = 1, il = index_stream.length; i < il; ++i) { var k = index_stream[i] & code_mask; var cur_key = ib_code << 8 | k; // (prev, k) unique tuple. var cur_code = code_table[cur_key]; // buffer + k. // Check if we have to create a new code table entry. if (cur_code === undefined) { // We don't have buffer + k. // Emit index buffer (without k). // This is an inline version of emit_code, because this is the core // writing routine of the compressor (and V8 cannot inline emit_code // because it is a closure here in a different context). Additionally // we can call emit_byte_to_buffer less often, because we can have // 30-bits (from our 31-bit signed SMI), and we know our codes will only // be 12-bits, so can safely have 18-bits there without overflow. // emit_code(ib_code); cur |= ib_code << cur_shift; cur_shift += cur_code_size; while (cur_shift >= 8) { buf[p++] = cur & 0xff; cur >>= 8; cur_shift -= 8; if (p === cur_subblock + 256) { // Finished a subblock. buf[cur_subblock] = 255; cur_subblock = p++; } } if (next_code === 4096) { // Table full, need a clear. emit_code(clear_code); next_code = eoi_code + 1; cur_code_size = min_code_size + 1; code_table = { }; } else { // Table not full, insert a new entry. // Increase our variable bit code sizes if necessary. This is a bit // tricky as it is based on "timing" between the encoding and // decoder. From the encoders perspective this should happen after // we've already emitted the index buffer and are about to create the // first table entry that would overflow our current code bit size. if (next_code >= (1 << cur_code_size)) ++cur_code_size; code_table[cur_key] = next_code++; // Insert into code table. } ib_code = k; // Index buffer to single input k. } else { ib_code = cur_code; // Index buffer to sequence in code table. } } emit_code(ib_code); // There will still be something in the index buffer. emit_code(eoi_code); // End Of Information. // Flush / finalize the sub-blocks stream to the buffer. emit_bytes_to_buffer(1); // Finish the sub-blocks, writing out any unfinished lengths and // terminating with a sub-block of length 0. If we have already started // but not yet used a sub-block it can just become the terminator. if (cur_subblock + 1 === p) { // Started but unused. buf[cur_subblock] = 0; } else { // Started and used, write length and additional terminator block. buf[cur_subblock] = p - cur_subblock - 1; buf[p++] = 0; } return p; } function GifReader(buf) { var p = 0; // - Header (GIF87a or GIF89a). if (buf[p++] !== 0x47 || buf[p++] !== 0x49 || buf[p++] !== 0x46 || buf[p++] !== 0x38 || (buf[p++]+1 & 0xfd) !== 0x38 || buf[p++] !== 0x61) { throw new Error("Invalid GIF 87a/89a header."); } // - Logical Screen Descriptor. var width = buf[p++] | buf[p++] << 8; var height = buf[p++] | buf[p++] << 8; var pf0 = buf[p++]; // . var global_palette_flag = pf0 >> 7; var num_global_colors_pow2 = pf0 & 0x7; var num_global_colors = 1 << (num_global_colors_pow2 + 1); var background = buf[p++]; buf[p++]; // Pixel aspect ratio (unused?). var global_palette_offset = null; var global_palette_size = null; if (global_palette_flag) { global_palette_offset = p; global_palette_size = num_global_colors; p += num_global_colors * 3; // Seek past palette. } var no_eof = true; var frames = [ ]; var delay = 0; var transparent_index = null; var disposal = 0; // 0 - No disposal specified. var loop_count = null; this.width = width; this.height = height; while (no_eof && p < buf.length) { switch (buf[p++]) { case 0x21: // Graphics Control Extension Block switch (buf[p++]) { case 0xff: // Application specific block // Try if it's a Netscape block (with animation loop counter). if (buf[p ] !== 0x0b || // 21 FF already read, check block size. // NETSCAPE2.0 buf[p+1 ] == 0x4e && buf[p+2 ] == 0x45 && buf[p+3 ] == 0x54 && buf[p+4 ] == 0x53 && buf[p+5 ] == 0x43 && buf[p+6 ] == 0x41 && buf[p+7 ] == 0x50 && buf[p+8 ] == 0x45 && buf[p+9 ] == 0x32 && buf[p+10] == 0x2e && buf[p+11] == 0x30 && // Sub-block buf[p+12] == 0x03 && buf[p+13] == 0x01 && buf[p+16] == 0) { p += 14; loop_count = buf[p++] | buf[p++] << 8; p++; // Skip terminator. } else { // We don't know what it is, just try to get past it. p += 12; while (true) { // Seek through subblocks. var block_size = buf[p++]; // Bad block size (ex: undefined from an out of bounds read). if (!(block_size >= 0)) throw Error("Invalid block size"); if (block_size === 0) break; // 0 size is terminator p += block_size; } } break; case 0xf9: // Graphics Control Extension if (buf[p++] !== 0x4 || buf[p+4] !== 0) throw new Error("Invalid graphics extension block."); var pf1 = buf[p++]; delay = buf[p++] | buf[p++] << 8; transparent_index = buf[p++]; if ((pf1 & 1) === 0) transparent_index = null; disposal = pf1 >> 2 & 0x7; p++; // Skip terminator. break; case 0xfe: // Comment Extension. while (true) { // Seek through subblocks. var block_size = buf[p++]; // Bad block size (ex: undefined from an out of bounds read). if (!(block_size >= 0)) throw Error("Invalid block size"); if (block_size === 0) break; // 0 size is terminator // console.log(buf.slice(p, p+block_size).toString('ascii')); p += block_size; } break; default: throw new Error( "Unknown graphic control label: 0x" + buf[p-1].toString(16)); } break; case 0x2c: // Image Descriptor. var x = buf[p++] | buf[p++] << 8; var y = buf[p++] | buf[p++] << 8; var w = buf[p++] | buf[p++] << 8; var h = buf[p++] | buf[p++] << 8; var pf2 = buf[p++]; var local_palette_flag = pf2 >> 7; var interlace_flag = pf2 >> 6 & 1; var num_local_colors_pow2 = pf2 & 0x7; var num_local_colors = 1 << (num_local_colors_pow2 + 1); var palette_offset = global_palette_offset; var palette_size = global_palette_size; var has_local_palette = false; if (local_palette_flag) { var has_local_palette = true; palette_offset = p; // Override with local palette. palette_size = num_local_colors; p += num_local_colors * 3; // Seek past palette. } var data_offset = p; p++; // codesize while (true) { var block_size = buf[p++]; // Bad block size (ex: undefined from an out of bounds read). if (!(block_size >= 0)) throw Error("Invalid block size"); if (block_size === 0) break; // 0 size is terminator p += block_size; } frames.push({x: x, y: y, width: w, height: h, has_local_palette: has_local_palette, palette_offset: palette_offset, palette_size: palette_size, data_offset: data_offset, data_length: p - data_offset, transparent_index: transparent_index, interlaced: !!interlace_flag, delay: delay, disposal: disposal}); break; case 0x3b: // Trailer Marker (end of file). no_eof = false; break; default: throw new Error("Unknown gif block: 0x" + buf[p-1].toString(16)); break; } } this.numFrames = function() { return frames.length; }; this.loopCount = function() { return loop_count; }; this.frameInfo = function(frame_num) { if (frame_num < 0 || frame_num >= frames.length) throw new Error("Frame index out of range."); return frames[frame_num]; } this.decodeAndBlitFrameBGRA = function(frame_num, pixels) { var frame = this.frameInfo(frame_num); var num_pixels = frame.width * frame.height; var index_stream = new Uint8Array(num_pixels); // At most 8-bit indices. GifReaderLZWOutputIndexStream( buf, frame.data_offset, index_stream, num_pixels); var palette_offset = frame.palette_offset; // NOTE(deanm): It seems to be much faster to compare index to 256 than // to === null. Not sure why, but CompareStub_EQ_STRICT shows up high in // the profile, not sure if it's related to using a Uint8Array. var trans = frame.transparent_index; if (trans === null) trans = 256; // We are possibly just blitting to a portion of the entire frame. // That is a subrect within the framerect, so the additional pixels // must be skipped over after we finished a scanline. var framewidth = frame.width; var framestride = width - framewidth; var xleft = framewidth; // Number of subrect pixels left in scanline. // Output indicies of the top left and bottom right corners of the subrect. var opbeg = ((frame.y * width) + frame.x) * 4; var opend = ((frame.y + frame.height) * width + frame.x) * 4; var op = opbeg; var scanstride = framestride * 4; // Use scanstride to skip past the rows when interlacing. This is skipping // 7 rows for the first two passes, then 3 then 1. if (frame.interlaced === true) { scanstride += width * 4 * 7; // Pass 1. } var interlaceskip = 8; // Tracking the row interval in the current pass. for (var i = 0, il = index_stream.length; i < il; ++i) { var index = index_stream[i]; if (xleft === 0) { // Beginning of new scan line op += scanstride; xleft = framewidth; if (op >= opend) { // Catch the wrap to switch passes when interlacing. scanstride = framestride * 4 + width * 4 * (interlaceskip-1); // interlaceskip / 2 * 4 is interlaceskip << 1. op = opbeg + (framewidth + framestride) * (interlaceskip << 1); interlaceskip >>= 1; } } if (index === trans) { op += 4; } else { var r = buf[palette_offset + index * 3]; var g = buf[palette_offset + index * 3 + 1]; var b = buf[palette_offset + index * 3 + 2]; pixels[op++] = b; pixels[op++] = g; pixels[op++] = r; pixels[op++] = 255; } --xleft; } }; // I will go to copy and paste hell one day... this.decodeAndBlitFrameRGBA = function(frame_num, pixels) { var frame = this.frameInfo(frame_num); var num_pixels = frame.width * frame.height; var index_stream = new Uint8Array(num_pixels); // At most 8-bit indices. GifReaderLZWOutputIndexStream( buf, frame.data_offset, index_stream, num_pixels); var palette_offset = frame.palette_offset; // NOTE(deanm): It seems to be much faster to compare index to 256 than // to === null. Not sure why, but CompareStub_EQ_STRICT shows up high in // the profile, not sure if it's related to using a Uint8Array. var trans = frame.transparent_index; if (trans === null) trans = 256; // We are possibly just blitting to a portion of the entire frame. // That is a subrect within the framerect, so the additional pixels // must be skipped over after we finished a scanline. var framewidth = frame.width; var framestride = width - framewidth; var xleft = framewidth; // Number of subrect pixels left in scanline. // Output indicies of the top left and bottom right corners of the subrect. var opbeg = ((frame.y * width) + frame.x) * 4; var opend = ((frame.y + frame.height) * width + frame.x) * 4; var op = opbeg; var scanstride = framestride * 4; // Use scanstride to skip past the rows when interlacing. This is skipping // 7 rows for the first two passes, then 3 then 1. if (frame.interlaced === true) { scanstride += width * 4 * 7; // Pass 1. } var interlaceskip = 8; // Tracking the row interval in the current pass. for (var i = 0, il = index_stream.length; i < il; ++i) { var index = index_stream[i]; if (xleft === 0) { // Beginning of new scan line op += scanstride; xleft = framewidth; if (op >= opend) { // Catch the wrap to switch passes when interlacing. scanstride = framestride * 4 + width * 4 * (interlaceskip-1); // interlaceskip / 2 * 4 is interlaceskip << 1. op = opbeg + (framewidth + framestride) * (interlaceskip << 1); interlaceskip >>= 1; } } if (index === trans) { op += 4; } else { var r = buf[palette_offset + index * 3]; var g = buf[palette_offset + index * 3 + 1]; var b = buf[palette_offset + index * 3 + 2]; pixels[op++] = r; pixels[op++] = g; pixels[op++] = b; pixels[op++] = 255; } --xleft; } }; } function GifReaderLZWOutputIndexStream(code_stream, p, output, output_length) { var min_code_size = code_stream[p++]; var clear_code = 1 << min_code_size; var eoi_code = clear_code + 1; var next_code = eoi_code + 1; var cur_code_size = min_code_size + 1; // Number of bits per code. // NOTE: This shares the same name as the encoder, but has a different // meaning here. Here this masks each code coming from the code stream. var code_mask = (1 << cur_code_size) - 1; var cur_shift = 0; var cur = 0; var op = 0; // Output pointer. var subblock_size = code_stream[p++]; // TODO(deanm): Would using a TypedArray be any faster? At least it would // solve the fast mode / backing store uncertainty. // var code_table = Array(4096); var code_table = new Int32Array(4096); // Can be signed, we only use 20 bits. var prev_code = null; // Track code-1. while (true) { // Read up to two bytes, making sure we always 12-bits for max sized code. while (cur_shift < 16) { if (subblock_size === 0) break; // No more data to be read. cur |= code_stream[p++] << cur_shift; cur_shift += 8; if (subblock_size === 1) { // Never let it get to 0 to hold logic above. subblock_size = code_stream[p++]; // Next subblock. } else { --subblock_size; } } // TODO(deanm): We should never really get here, we should have received // and EOI. if (cur_shift < cur_code_size) break; var code = cur & code_mask; cur >>= cur_code_size; cur_shift -= cur_code_size; // TODO(deanm): Maybe should check that the first code was a clear code, // at least this is what you're supposed to do. But actually our encoder // now doesn't emit a clear code first anyway. if (code === clear_code) { // We don't actually have to clear the table. This could be a good idea // for greater error checking, but we don't really do any anyway. We // will just track it with next_code and overwrite old entries. next_code = eoi_code + 1; cur_code_size = min_code_size + 1; code_mask = (1 << cur_code_size) - 1; // Don't update prev_code ? prev_code = null; continue; } else if (code === eoi_code) { break; } // We have a similar situation as the decoder, where we want to store // variable length entries (code table entries), but we want to do in a // faster manner than an array of arrays. The code below stores sort of a // linked list within the code table, and then "chases" through it to // construct the dictionary entries. When a new entry is created, just the // last byte is stored, and the rest (prefix) of the entry is only // referenced by its table entry. Then the code chases through the // prefixes until it reaches a single byte code. We have to chase twice, // first to compute the length, and then to actually copy the data to the // output (backwards, since we know the length). The alternative would be // storing something in an intermediate stack, but that doesn't make any // more sense. I implemented an approach where it also stored the length // in the code table, although it's a bit tricky because you run out of // bits (12 + 12 + 8), but I didn't measure much improvements (the table // entries are generally not the long). Even when I created benchmarks for // very long table entries the complexity did not seem worth it. // The code table stores the prefix entry in 12 bits and then the suffix // byte in 8 bits, so each entry is 20 bits. var chase_code = code < next_code ? code : prev_code; // Chase what we will output, either {CODE} or {CODE-1}. var chase_length = 0; var chase = chase_code; while (chase > clear_code) { chase = code_table[chase] >> 8; ++chase_length; } var k = chase; var op_end = op + chase_length + (chase_code !== code ? 1 : 0); if (op_end > output_length) { console.log("Warning, gif stream longer than expected."); return; } // Already have the first byte from the chase, might as well write it fast. output[op++] = k; op += chase_length; var b = op; // Track pointer, writing backwards. if (chase_code !== code) // The case of emitting {CODE-1} + k. output[op++] = k; chase = chase_code; while (chase_length--) { chase = code_table[chase]; output[--b] = chase & 0xff; // Write backwards. chase >>= 8; // Pull down to the prefix code. } if (prev_code !== null && next_code < 4096) { code_table[next_code++] = prev_code << 8 | k; // TODO(deanm): Figure out this clearing vs code growth logic better. I // have an feeling that it should just happen somewhere else, for now it // is awkward between when we grow past the max and then hit a clear code. // For now just check if we hit the max 12-bits (then a clear code should // follow, also of course encoded in 12-bits). if (next_code >= code_mask+1 && cur_code_size < 12) { ++cur_code_size; code_mask = code_mask << 1 | 1; } } prev_code = code; } if (op !== output_length) { console.log("Warning, gif stream shorter than expected."); } return output; } // CommonJS. //try { exports.GifWriter = GifWriter; exports.GifReader = GifReader } catch(e) {} try { exports.GifWriter = GifWriter; } catch(e) {} ================================================ FILE: wled00/data/pixelforge/pixelforge.htm ================================================ WLED PixelForge
WLEDPixelForge

Target Segment

Images on Device

Upload New Image

Drop image or click to select

Crop & Adjust Image

Preview at target resolution
.gif will be added

Target Segment

Text to show

Settings

Speed
Y Offset
Trail
Font Size
Rotate

Available Tokens

#TIME - HH:MM AM/PM
#HHMM - HH:MM
#DATE - DD.MM.YYYY
#DDMM - Day.Month
#MMDD - Month/Day
#YYYY - Year
#YY - Year 2-digit
#HH - Hours
#MM - Minutes
#SS - Seconds
#MO - Month number
#DD - Day number
#MON - Month (Jan)
#MONL - Month (January)
#DAY - Weekday (Mon)
#DDDD - Weekday (Monday)
Tips:
• Mix text and tokens: "It's #HHMM O'Clock" or "#HH:#MM:#SS"
• Add '0' suffix for leading zeros: #TIME0, #HH0, etc.

Pixel Paint

Interactive painting tool

Video Lab

Stream video and generate animated GIFs (beta)

PIXEL MAGIC Tool

Legacy pixel art editor

================================================ FILE: wled00/data/pxmagic/pxmagic.htm ================================================ Pixel Magic Tool
PIXEL MAGIC TOOL It is a tool that converts any image into code in JSON WLED format for 2D Matrix panels

Drag and drop a file here or click to select a local file

================================================ FILE: wled00/data/rangetouch.js ================================================ // ========================================================================== // rangetouch.js v2.0.1 // Making work on touch devices // https://github.com/sampotts/rangetouch // License: The MIT License (MIT) // ========================================================================== !function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define("RangeTouch",t):(e=e||self).RangeTouch=t()}(this,(function(){"use strict";function e(e,t){for(var n=0;nt){var n=function(e){var t="".concat(e).match(/(?:\.(\d+))?(?:[eE]([+-]?\d+))?$/);return t?Math.max(0,(t[1]?t[1].length:0)-(t[2]?+t[2]:0)):0}(t);return parseFloat(e.toFixed(n))}return Math.round(e/t)*t}return function(){function t(e,n){(function(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")})(this,t),m(e)?this.element=e:d(e)&&(this.element=document.querySelector(e)),m(this.element)&&p(this.element.rangeTouch)&&(this.config=r({},i,{},n),this.init())}return n=t,c=[{key:"setup",value:function(e){var n=1(n=100/l.width*(i.clientX-l.left))?n=0:100n?n-=(100-2*n)*a:50 WLED Settings ================================================ FILE: wled00/data/settings_2D.htm ================================================ 2D Set-up

2D setup

Strip or panel:

================================================ FILE: wled00/data/settings_dmx.htm ================================================ DMX Settings

Imma firin ma lazer (if it has DMX support)

Proxy Universe from E1.31 to DMX (0=disabled)
This will disable the LED data output to DMX configurable below

Number of fixtures is taken from LED config page
Channels per fixture (15 max):
Start channel:
Spacing between start channels: [ info ]

DMX fixtures start LED:

Channel functions


================================================ FILE: wled00/data/settings_leds.htm ================================================ LED Settings

LED setup

Total LEDs: ?
Recommended power supply for brightest white:
?


Global brightness factor: %
Enable automatic brightness limiter:
Automatically limits brightness to stay close to the limit.
Keep at <1A if powering LEDs directly from the ESP 5V pin!
If using multiple outputs it is recommended to use per-output limiter.
Analog (PWM) and virtual LEDs cannot use automatic brightness limiter.
Maximum PSU Current: mA
Use per-output limiter:

LED outputs:



LED memory usage: 0 / ? B


Show Advanced Settings
Make a segment for each output:
Custom bus start indices:

Color Order Override:

Color & White

Use Gamma correction for color: (strongly recommended)
Use Gamma correction for brightness: (not recommended)
Use Gamma value:

White Balance correction:
Global override for Auto-calculate white:
Calculate CCT from RGB:
CCT IC used (Athom 15W):
CCT blending (±100%): %
Positive: additive blend, Negative: exclusive blend
Set to 0 when using 2-wire (reverse polarity) CCT strips

Hardware setup

Buttons


Disable internal pull-up/down:
Touch threshold:

IR Remote

IR GPIO:  ✕
Apply IR change to main segment only:
IR info

Relay

Relay GPIO:  ✕
Invert Open drain

General settings

Power up

Turn LEDs on after power up/reset:
with brightness: (1-255)
(disable if using boot preset to turn LEDs on)

Apply preset at boot (0 = none)

Transitions

Default transition time: ms

Random Palettes

Use harmonic colors in Random palettes:
Random Palette Cycle Time: s

Timed light

Default duration: min
Default target brightness:
Mode:

Advanced

Palette wrapping:
Target refresh rate: FPS

Config template:

================================================ FILE: wled00/data/settings_pin.htm ================================================ PIN required

Please enter settings PIN code


================================================ FILE: wled00/data/settings_pininfo.htm ================================================ Pin Info

Pin Info

Loading...
================================================ FILE: wled00/data/settings_sec.htm ================================================ Security & Update Setup

Security & Update Setup

Settings PIN:
⚠ Unencrypted transmission. Be prudent when selecting PIN, do NOT use your banking, door, SIM, etc. pin!

Lock wireless (OTA) software update:
Passphrase:
To enable OTA, for security reasons you need to also enter the correct password!
The password should be changed when OTA is enabled.
Disable OTA when not in use, otherwise an attacker can reflash device software!
Settings on this page are only changeable if OTA lock is disabled!
Deny access to WiFi settings if locked:

Factory reset:
All settings and presets will be erased.

⚠ Unencrypted transmission. An attacker on the same network can intercept form data!

Software Update


Enable ArduinoOTA:
Only allow update from same network/WiFi:
⚠ If you are using multiple VLANs (i.e. IoT or guest network) either set PIN or disable this option.
Disabling this option will make your device less secure.

Backup & Restore

⚠ Restoring presets/configuration will OVERWRITE your current presets/configuration.
Incorrect upload or configuration may require a factory reset or re-flashing of your ESP.
For security reasons, passwords are not backed up.
Backup presets
Restore presets


Backup configuration
Restore configuration

About

WLED version ##VERSION##

Contributors, dependencies and special thanks
A huge thank you to everyone who helped me create WLED!

(c) 2016-2024 Christian Schwinne
Licensed under the EUPL v1.2 license

Installed version: WLED ##VERSION##
================================================ FILE: wled00/data/settings_sync.htm ================================================ Sync Settings

Sync setup

WLED Broadcast

UDP Port:
2nd Port:

ESP-NOW

Disabled. Enable ESP-NOW in WiFi settings.
Use ESP-NOW sync:
(in AP mode or no WiFi)

Sync groups

1 2 3 4 5 6 7 8
Send:
Receive:

Receive

Brightness, Color, Effects, and Palette
Segment options, bounds

Send

Enable Sync on start:
Send notifications on direct change:
Send notifications on button press or IR:
Send Alexa notifications:
Send Philips Hue change notifications:
UDP packet retransmissions:

Reboot required to apply changes.

Instance List

Enable instance list:
Make this instance discoverable:

Realtime

Receive UDP realtime:
Use main segment only:
Respect LED Maps:

Network DMX input
Type:
Port:
Multicast:
Start universe:
Reboot required. Check out LedFx!
Skip out-of-sequence packets:
DMX start address:
DMX segment spacing:
E1.31 port priority:
DMX mode:
E1.31 info
Timeout: ms
Force max brightness:
Disable realtime gamma correction:
Realtime LED offset:

Wired DMX Input Pins

DMX RX: RO
DMX TX: DI
DMX Enable: RE+DE
DMX Port:

This firmware build does not include DMX Input support.

This firmware build does not include DMX output support.

Alexa Voice Assistant

This firmware build does not include Alexa support.

Emulate Alexa device:
Alexa invocation name:
Also emulate devices to call the first presets

MQTT and Hue sync connect to external hosts!
This may impact the responsiveness of WLED.

For best results, only use one of these services at a time.
(alternatively, connect a second ESP to them and use the UDP sync)

MQTT

This firmware build does not include MQTT support.
Enable MQTT:
Broker: Port:
The MQTT credentials are sent over an unsecured connection.
Never use the MQTT password for another service!

Username:
Password:
Client ID:
Device Topic:
Group Topic:
Publish on button press:
Retain brightness & color messages:
Reboot required to apply changes. MQTT info

Philips Hue

This firmware build does not include Philips Hue support.
You can find the bridge IP and the light number in the 'About' section of the hue app.
Poll Hue light every ms:
Then, receive On/Off, Brightness, and Color
Hue Bridge IP:
. . .
Press the pushlink button on the bridge, after that save this page!
(when first connecting)
Hue status: Disabled in this build

Serial

This firmware build does not support Serial interface.
Baud rate:
Keep at 115200 to use Improv. Some boards may not support high rates.

================================================ FILE: wled00/data/settings_time.htm ================================================ Time Settings

Time setup

Get time from NTP server:

Use 24h format:
Time zone:
UTC offset: seconds (max. 18 hours)
Current local time is unknown.
Latitude:
Longitude:
(opens new tab, only works in browser)

Clock

Analog Clock overlay:
First LED: Last LED:
12h LED:
Show 5min marks:
Seconds (as trail):
Show clock overlay only if all LEDs are solid black:
Countdown Mode:
Countdown Goal:
Date: 20--
Time: ::

Macro Presets

Presets can be used as macros for both JSON and HTTP API commands.
Enter the preset ID below.
Use 0 for the default action instead of a preset
JSON API
HTTP API

Timer & Alexa Presets

Countdown-Over Preset:
Timed-Light-Over Presets:
Alexa On/Off Preset:

Button Action Presets

push
switch
short
on->off
long
off->on
double
N/A
Analog Button setup

Time-Controlled Presets


================================================ FILE: wled00/data/settings_ui.htm ================================================ UI Settings


User Interface

Device Name:
Enable simplified UI:
The following UI customization settings are unique both to the WLED device and this browser.
You will need to set them again if using a different browser, device or WLED IP address.
Refresh the main UI to apply changes.

Loading settings...

UI Appearance

:
:
:
:
:
:
:
:
:
:
I hate dark mode:
:
:
Custom CSS:

UI Background

:
:
BG image:
:
:

Random BG image settings

:
:
:
Holidays:


================================================ FILE: wled00/data/settings_um.htm ================================================ Usermod Settings


Usermod Setup

Global I2C & SPI

I2C GPIOs (HW)
SDA: SCL:

SPI GPIOs (HW)
only changable on ESP32
MOSI: MISO: SCLK:

change requires reboot!
Reboot after save?
Loading settings...

================================================ FILE: wled00/data/settings_wifi.htm ================================================ WiFi Settings

WiFi & Network Settings

Wireless network




Ethernet Type



DNS & mDNS

DNS server address:
...

mDNS address (leave empty for no mDNS):
http:// .local
Client IP: Not connected

Configure Access Point

AP SSID (leave empty for no AP):

Hide AP name:
AP password (leave empty for open):

Access Point WiFi channel:
AP opens:
AP IP: Not active

WiFi Power

Force 802.11g mode (ESP8266 only):
Disable WiFi sleep:
Disabling WiFi sleep increases power consumption
but can help with connectivity issues and sync.


Max. TX power:
WARNING: Modifying TX power may render device unreachable.

ESP-NOW Wireless

This firmware build does not include ESP-NOW support.
Enable ESP-NOW:
Listen for events over ESP-NOW
Keep disabled if not using a remote or ESP-NOW sync, increases power consumption.
Last device seen: None
Linked MACs (10 max):

================================================ FILE: wled00/data/style.css ================================================ html { touch-action: manipulation; } body { font-family: Verdana, sans-serif; font-size: 1rem; text-align: center; background: #111; color: #fff; line-height: 200%; margin: 0; } hr { border-color: #666; } hr.sml { width: 260px; } h4 { margin: 0; } a, a:hover { color: #28f; text-decoration: none; } .sec { background: #222; border-radius: 20px; padding: 8px; margin: 12px auto; max-width: 520px; } button, .btn { background: #333; color: #fff; font-family: Verdana, sans-serif; border: 0.3ch solid #333; border-radius: 24px; display: inline-block; font-size: 20px; margin: 12px 8px 8px; padding: 8px 12px; min-width: 48px; cursor: pointer; text-decoration: none; transition: all 0.3s ease; } button.sml { padding: 8px; border-radius: 20px; font-size: 15px; min-width: 40px; margin: 0 0 0 10px; } button:hover, .btn:hover{ background:#555; border-color:#555; } #scan { margin-top: -10px; } .toprow { top: 0; position: sticky; background-color:#222; z-index:1; } .lnk { border: 0; } .helpB { text-align: left; position: absolute; width: 60px; } .hide { display: none; } .err { color: #f00; } .warn { color: #fa0; } input { background: #333; color: #fff; font-family: Verdana, sans-serif; border: 0.5ch solid #333; } input:disabled { color: #888; } input:invalid { color: #f00; } input[type="text"], input[type="number"], input[type="password"], select { font-size: medium; margin: 2px; } input[type="number"] { width: 4em; } input[type="number"].xxl { width: 100px; } input[type="number"].xl { width: 85px; } input[type="number"].l { width: 64px; } input[type="number"].m { width: 56px; } input[type="number"].s { width: 48px; } input[type="number"].xs { width: 40px; } input[type="checkbox"] { transform: scale(1.5); margin-right: 10px; } td input[type="checkbox"] { margin-right: revert; } input[type=file] { font-size: 16px } select { margin: 2px; background: #333; color: #fff; font-family: Verdana, sans-serif; border: 0.5ch solid #333; } select.pin { max-width: 120px; text-overflow: ellipsis; } tr { line-height: 100%; } td { padding: 2px; } .d5 { width: 4rem !important; } .cal { font-size:1.5rem; cursor:pointer } #TMT table { width: 100%; } #msg { display: none; } #toast { opacity: 0; background-color: #444; border-radius: 5px; bottom: 64px; color: #fff; font-size: 17px; padding: 16px; pointer-events: none; position: fixed; text-align: center; z-index: 5; transform: translateX(-50%); max-width: 90%; left: 50%; } #toast.show { opacity: 1; background-color: #264; animation: fadein 0.5s, fadein 0.5s 2.5s reverse; } #toast.error { opacity: 1; background-color: #b21; animation: fadein 0.5s; } @media screen and (max-width: 767px) { input[type="text"], input[type="file"], input[type="number"], input[type="email"], input[type="tel"], input[type="password"] { font-size: 16px; } } @media screen and (max-width: 480px) { input[type="number"].s { width: 40px; } input[type="number"].xs { width: 32px; } input[type="file"] { width: 224px; } #btns select { width: 144px; } } ================================================ FILE: wled00/data/update.htm ================================================ WLED Update

WLED Software Update

Installed version: Loading...
Release: Loading...
Latest binary: Checking...
Download the latest binary: badge






Bootloader Update

Warning: Only upload verified ESP32 bootloader files!


Updating...
Please do not close or refresh the page :)

================================================ FILE: wled00/data/usermod.htm ================================================ No usermod custom web page set. ================================================ FILE: wled00/data/welcome.htm ================================================ Welcome!

Welcome to WLED!

A versatile tool for controlling LEDs

Find out more at wled.me

Next steps:

Connect to your local WiFi here!

Just trying this out in AP mode?

================================================ FILE: wled00/dmx_input.cpp ================================================ #include "wled.h" #ifdef WLED_ENABLE_DMX_INPUT #ifdef ESP8266 #error DMX input is only supported on ESP32 #endif #include "dmx_input.h" #include void rdmPersonalityChangedCb(dmx_port_t dmxPort, const rdm_header_t *header, void *context) { DMXInput *dmx = static_cast(context); if (!dmx) { DEBUG_PRINTLN("DMX: Error: no context in rdmPersonalityChangedCb"); return; } if (header->cc == RDM_CC_SET_COMMAND_RESPONSE) { const uint8_t personality = dmx_get_current_personality(dmx->inputPortNum); DMXMode = std::min(DMX_MODE_PRESET, std::max(DMX_MODE_SINGLE_RGB, int(personality))); configNeedsWrite = true; DEBUG_PRINTF("DMX personality changed to to: %d\n", DMXMode); } } void rdmAddressChangedCb(dmx_port_t dmxPort, const rdm_header_t *header, void *context) { DMXInput *dmx = static_cast(context); if (!dmx) { DEBUG_PRINTLN("DMX: Error: no context in rdmAddressChangedCb"); return; } if (header->cc == RDM_CC_SET_COMMAND_RESPONSE) { const uint16_t addr = dmx_get_start_address(dmx->inputPortNum); DMXAddress = std::min(512, int(addr)); configNeedsWrite = true; DEBUG_PRINTF("DMX start addr changed to: %d\n", DMXAddress); } } static dmx_config_t createConfig() { dmx_config_t config; config.pd_size = 255; config.dmx_start_address = DMXAddress; config.model_id = 0; config.product_category = RDM_PRODUCT_CATEGORY_FIXTURE; config.software_version_id = VERSION; strcpy(config.device_label, "WLED_MM"); const std::string dmxWledVersionString = "WLED_V" + std::to_string(VERSION); strncpy(config.software_version_label, dmxWledVersionString.c_str(), 32); config.software_version_label[32] = '\0'; // zero termination in case versionString string was longer than 32 chars config.personalities[0].description = "SINGLE_RGB"; config.personalities[0].footprint = 3; config.personalities[1].description = "SINGLE_DRGB"; config.personalities[1].footprint = 4; config.personalities[2].description = "EFFECT"; config.personalities[2].footprint = 15; config.personalities[3].description = "MULTIPLE_RGB"; config.personalities[3].footprint = std::min(512, int(strip.getLengthTotal()) * 3); config.personalities[4].description = "MULTIPLE_DRGB"; config.personalities[4].footprint = std::min(512, int(strip.getLengthTotal()) * 3 + 1); config.personalities[5].description = "MULTIPLE_RGBW"; config.personalities[5].footprint = std::min(512, int(strip.getLengthTotal()) * 4); config.personalities[6].description = "EFFECT_W"; config.personalities[6].footprint = 18; config.personalities[7].description = "EFFECT_SEGMENT"; config.personalities[7].footprint = std::min(512, strip.getSegmentsNum() * 15); config.personalities[8].description = "EFFECT_SEGMENT_W"; config.personalities[8].footprint = std::min(512, strip.getSegmentsNum() * 18); config.personalities[9].description = "PRESET"; config.personalities[9].footprint = 1; config.personality_count = 10; // rdm personalities are numbered from 1, thus we can just set the DMXMode directly. config.current_personality = DMXMode; return config; } void dmxReceiverTask(void *context) { DMXInput *instance = static_cast(context); if (instance == nullptr) { return; } if (instance->installDriver()) { while (true) { instance->updateInternal(); } } } bool DMXInput::installDriver() { const auto config = createConfig(); DEBUG_PRINTF("DMX port: %u\n", inputPortNum); if (!dmx_driver_install(inputPortNum, &config, DMX_INTR_FLAGS_DEFAULT)) { DEBUG_PRINTF("Error: Failed to install dmx driver\n"); return false; } DEBUG_PRINTF("Listening for DMX on pin %u\n", rxPin); DEBUG_PRINTF("Sending DMX on pin %u\n", txPin); DEBUG_PRINTF("DMX enable pin is: %u\n", enPin); dmx_set_pin(inputPortNum, txPin, rxPin, enPin); rdm_register_dmx_start_address(inputPortNum, rdmAddressChangedCb, this); rdm_register_dmx_personality(inputPortNum, rdmPersonalityChangedCb, this); initialized = true; return true; } void DMXInput::init(uint8_t rxPin, uint8_t txPin, uint8_t enPin, uint8_t inputPortNum) { #ifdef WLED_ENABLE_DMX_OUTPUT //TODO add again once dmx output has been merged // if(inputPortNum == dmxOutputPort) // { // DEBUG_PRINTF("DMXInput: Error: Input port == output port"); // return; // } #endif if (inputPortNum <= (SOC_UART_NUM - 1) && inputPortNum > 0) { this->inputPortNum = inputPortNum; } else { DEBUG_PRINTF("DMXInput: Error: invalid inputPortNum: %d\n", inputPortNum); return; } if (rxPin > 0 && enPin > 0 && txPin > 0) { const managed_pin_type pins[] = { {(int8_t)txPin, false}, // these are not used as gpio pins, thus isOutput is always false. {(int8_t)rxPin, false}, {(int8_t)enPin, false}}; const bool pinsAllocated = PinManager::allocateMultiplePins(pins, 3, PinOwner::DMX_INPUT); if (!pinsAllocated) { DEBUG_PRINTF("DMXInput: Error: Failed to allocate pins for DMX_INPUT. Pins already in use:\n"); DEBUG_PRINTF("rx in use by: %s\n", PinManager::getPinOwner(rxPin)); DEBUG_PRINTF("tx in use by: %s\n", PinManager::getPinOwner(txPin)); DEBUG_PRINTF("en in use by: %s\n", PinManager::getPinOwner(enPin)); return; } this->rxPin = rxPin; this->txPin = txPin; this->enPin = enPin; // put dmx receiver into seperate task because it should not be blocked // pin to core 0 because wled is running on core 1 xTaskCreatePinnedToCore(dmxReceiverTask, "DMX_RCV_TASK", 10240, this, 2, &task, 0); if (!task) { DEBUG_PRINTF("Error: Failed to create dmx rcv task"); } } else { DEBUG_PRINTLN("DMX input disabled due to rxPin, enPin or txPin not set"); return; } } void DMXInput::updateInternal() { if (!initialized) { return; } checkAndUpdateConfig(); dmx_packet_t packet; unsigned long now = millis(); if (dmx_receive(inputPortNum, &packet, DMX_TIMEOUT_TICK)) { if (!packet.err) { if(!connected) { DEBUG_PRINTLN("DMX Input - connected"); } connected = true; identify = isIdentifyOn(); if (!packet.is_rdm) { const std::lock_guard lock(dmxDataLock); dmx_read(inputPortNum, dmxdata, packet.size); } } else { connected = false; } } else { if(connected) { DEBUG_PRINTLN("DMX Input - disconnected"); } connected = false; } } void DMXInput::update() { if (identify) { turnOnAllLeds(); } else if (connected) { const std::lock_guard lock(dmxDataLock); handleDMXData(1, 512, dmxdata, REALTIME_MODE_DMX, 0); } } void DMXInput::turnOnAllLeds() { // TODO not sure if this is the correct way? const uint16_t numPixels = strip.getLengthTotal(); for (uint16_t i = 0; i < numPixels; ++i) { strip.setPixelColor(i, 255, 255, 255, 255); } strip.setBrightness(255, true); strip.show(); } void DMXInput::disable() { if (initialized) { dmx_driver_disable(inputPortNum); } } void DMXInput::enable() { if (initialized) { dmx_driver_enable(inputPortNum); } } bool DMXInput::isIdentifyOn() const { uint8_t identify = 0; const bool gotIdentify = rdm_get_identify_device(inputPortNum, &identify); // gotIdentify should never be false because it is a default parameter in rdm // but just in case we check for it anyway return bool(identify) && gotIdentify; } void DMXInput::checkAndUpdateConfig() { /** * The global configuration variables are modified by the web interface. * If they differ from the driver configuration, we have to update the driver * configuration. */ const uint8_t currentPersonality = dmx_get_current_personality(inputPortNum); if (currentPersonality != DMXMode) { DEBUG_PRINTF("DMX personality has changed from %d to %d\n", currentPersonality, DMXMode); dmx_set_current_personality(inputPortNum, DMXMode); } const uint16_t currentAddr = dmx_get_start_address(inputPortNum); if (currentAddr != DMXAddress) { DEBUG_PRINTF("DMX address has changed from %d to %d\n", currentAddr, DMXAddress); dmx_set_start_address(inputPortNum, DMXAddress); } } #endif ================================================ FILE: wled00/dmx_input.h ================================================ #pragma once #include #include #include #include /* * Support for DMX/RDM input via serial (e.g. max485) on ESP32 * ESP32 Library from: * https://github.com/someweisguy/esp_dmx */ class DMXInput { public: void init(uint8_t rxPin, uint8_t txPin, uint8_t enPin, uint8_t inputPortNum); void update(); /**disable dmx receiver (do this before disabling the cache)*/ void disable(); void enable(); /// True if dmx is currently connected bool isConnected() const { return connected; } private: /// @return true if rdm identify is active bool isIdentifyOn() const; /** * Checks if the global dmx config has changed and updates the changes in rdm */ void checkAndUpdateConfig(); /// overrides everything and turns on all leds void turnOnAllLeds(); /// installs the dmx driver /// @return false on fail bool installDriver(); /// is called by the dmx receive task regularly to receive new dmx data void updateInternal(); // is invoked whenver the dmx start address is changed via rdm friend void rdmAddressChangedCb(dmx_port_t dmxPort, const rdm_header_t *header, void *context); // is invoked whenever the personality is changed via rdm friend void rdmPersonalityChangedCb(dmx_port_t dmxPort, const rdm_header_t *header, void *context); /// The internal dmx task. /// This is the main loop of the dmx receiver. It never returns. friend void dmxReceiverTask(void * context); uint8_t inputPortNum = 255; uint8_t rxPin = 255; uint8_t txPin = 255; uint8_t enPin = 255; /// is written to by the dmx receive task. byte dmxdata[DMX_PACKET_SIZE]; /// True once the dmx input has been initialized successfully bool initialized = false; // true once init finished successfully /// True if dmx is currently connected std::atomic connected{false}; std::atomic identify{false}; /// Timestamp of the last time a dmx frame was received unsigned long lastUpdate = 0; /// Taskhandle of the dmx task that is running in the background TaskHandle_t task; /// Guards access to dmxData std::mutex dmxDataLock; }; ================================================ FILE: wled00/dmx_output.cpp ================================================ #include "wled.h" /* * Support for DMX output via serial (e.g. MAX485). * Change the output pin in src/dependencies/ESPDMX.cpp, if needed (ESP8266) * Change the output pin in src/dependencies/SparkFunDMX.cpp, if needed (ESP32) * ESP8266 Library from: * https://github.com/Rickgg/ESP-Dmx * ESP32 Library from: * https://github.com/sparkfun/SparkFunDMX */ #ifdef WLED_ENABLE_DMX void handleDMXOutput() { // don't act, when in DMX Proxy mode if (e131ProxyUniverse != 0) return; uint8_t brightness = strip.getBrightness(); bool calc_brightness = true; // check if no shutter channel is set for (unsigned i = 0; i < DMXChannels; i++) { if (DMXFixtureMap[i] == 5) calc_brightness = false; } uint16_t len = strip.getLengthTotal(); for (int i = DMXStartLED; i < len; i++) { // uses the amount of LEDs as fixture count uint32_t in = strip.getPixelColor(i); // get the colors for the individual fixtures as suggested by Aircoookie in issue #462 byte w = W(in); byte r = R(in); byte g = G(in); byte b = B(in); int DMXFixtureStart = DMXStart + (DMXGap * (i - DMXStartLED)); for (int j = 0; j < DMXChannels; j++) { int DMXAddr = DMXFixtureStart + j; switch (DMXFixtureMap[j]) { case 0: // Set this channel to 0. Good way to tell strobe- and fade-functions to fuck right off. dmx.write(DMXAddr, 0); break; case 1: // Red dmx.write(DMXAddr, calc_brightness ? (r * brightness) / 255 : r); break; case 2: // Green dmx.write(DMXAddr, calc_brightness ? (g * brightness) / 255 : g); break; case 3: // Blue dmx.write(DMXAddr, calc_brightness ? (b * brightness) / 255 : b); break; case 4: // White dmx.write(DMXAddr, calc_brightness ? (w * brightness) / 255 : w); break; case 5: // Shutter channel. Controls the brightness. dmx.write(DMXAddr, brightness); break; case 6: // Sets this channel to 255. Like 0, but more wholesome. dmx.write(DMXAddr, 255); break; } } } dmx.update(); // update the DMX bus } void initDMXOutput() { #if defined(ESP8266) || defined(CONFIG_IDF_TARGET_ESP32C3) || defined(CONFIG_IDF_TARGET_ESP32S2) dmx.init(512); // initialize with bus length #else dmx.initWrite(512); // initialize with bus length #endif } #else void initDMXOutput(){} void handleDMXOutput() {} #endif ================================================ FILE: wled00/dynarray.h ================================================ /* dynarray.h Macros for generating a "dynamic array", a static array of objects declared in different translation units */ #pragma once // Declare the beginning and ending elements of a dynamic array of 'type'. // This must be used in only one translation unit in your program for any given array. #define DECLARE_DYNARRAY(type, array_name) \ static type const DYNARRAY_BEGIN(array_name)[0] __attribute__((__section__(DYNARRAY_SECTION "." #array_name ".0"), unused)) = {}; \ static type const DYNARRAY_END(array_name)[0] __attribute__((__section__(DYNARRAY_SECTION "." #array_name ".99999"), unused)) = {}; // Declare an object that is a member of a dynamic array. "member name" must be unique; "array_section" is an integer for ordering items. // It is legal to define multiple items with the same section name; the order of those items will be up to the linker. #define DYNARRAY_MEMBER(type, array_name, member_name, array_section) type const member_name __attribute__((__section__(DYNARRAY_SECTION "." #array_name "." #array_section), used)) #define DYNARRAY_BEGIN(array_name) array_name##_begin #define DYNARRAY_END(array_name) array_name##_end #define DYNARRAY_LENGTH(array_name) (&DYNARRAY_END(array_name)[0] - &DYNARRAY_BEGIN(array_name)[0]) #ifdef ESP8266 // ESP8266 linker script cannot be extended with a unique section for dynamic arrays. // We instead pack them in the ".dtors" section, as it's sorted and uploaded to the flash // (but will never be used in the embedded system) #define DYNARRAY_SECTION ".dtors" #else /* ESP8266 */ // Use a unique named section; the linker script must be extended to ensure it's correctly placed. #define DYNARRAY_SECTION ".dynarray" #endif ================================================ FILE: wled00/e131.cpp ================================================ #include "wled.h" #define MAX_3_CH_LEDS_PER_UNIVERSE 170 #define MAX_4_CH_LEDS_PER_UNIVERSE 128 #define MAX_CHANNELS_PER_UNIVERSE 512 // forward declarations static void handleDDPPacket(e131_packet_t* p); static void handleArtnetPollReply(IPAddress ipAddress); static void prepareArtnetPollReply(ArtPollReply *reply); static void sendArtnetPollReply(ArtPollReply *reply, IPAddress ipAddress, uint16_t portAddress); /* * E1.31 handler */ //DDP protocol support, called by handleE131Packet //handles RGB data only static void handleDDPPacket(e131_packet_t* p) { static bool ddpSeenPush = false; // have we seen a push yet? int lastPushSeq = e131LastSequenceNumber[0]; //reject late packets belonging to previous frame (assuming 4 packets max. before push) if (e131SkipOutOfSequence && lastPushSeq) { int sn = p->sequenceNum & 0xF; if (sn) { if (lastPushSeq > 5) { if (sn > (lastPushSeq -5) && sn < lastPushSeq) return; } else { if (sn > (10 + lastPushSeq) || sn < lastPushSeq) return; } } } unsigned ddpChannelsPerLed = ((p->dataType & 0b00111000)>>3 == 0b011) ? 4 : 3; // data type 0x1B (formerly 0x1A) is RGBW (type 3, 8 bit/channel) uint32_t start = htonl(p->channelOffset) / ddpChannelsPerLed; start += DMXAddress / ddpChannelsPerLed; uint16_t dataLen = htons(p->dataLen); unsigned stop = start + dataLen / ddpChannelsPerLed; uint8_t* data = p->data; unsigned c = 0; if (p->flags & DDP_TIMECODE_FLAG) c = 4; //packet has timecode flag, we do not support it, but data starts 4 bytes later unsigned numLeds = stop - start; // stop >= start is guaranteed unsigned maxDataIndex = c + numLeds * ddpChannelsPerLed; // validate bounds before accessing data array if (maxDataIndex > dataLen) { DEBUG_PRINTLN(F("DDP packet data bounds exceeded, rejecting.")); return; } if (realtimeMode != REALTIME_MODE_DDP) ddpSeenPush = false; // just starting, no push yet realtimeLock(realtimeTimeoutMs, REALTIME_MODE_DDP); if (!realtimeOverride) { for (unsigned i = start; i < stop; i++, c += ddpChannelsPerLed) { setRealtimePixel(i, data[c], data[c+1], data[c+2], ddpChannelsPerLed >3 ? data[c+3] : 0); } } bool push = p->flags & DDP_PUSH_FLAG; ddpSeenPush |= push; if (!ddpSeenPush || push) { // if we've never seen a push, or this is one, render display e131NewData = true; int sn = p->sequenceNum & 0xF; if (sn) e131LastSequenceNumber[0] = sn; } } //E1.31 and Art-Net protocol support void handleE131Packet(e131_packet_t* p, IPAddress clientIP, byte protocol){ int uni = 0, dmxChannels = 0; uint8_t* e131_data = nullptr; int seq = 0, mde = REALTIME_MODE_E131; if (protocol == P_ARTNET) { if (p->art_opcode == ARTNET_OPCODE_OPPOLL) { handleArtnetPollReply(clientIP); return; } uni = p->art_universe; dmxChannels = htons(p->art_length); e131_data = p->art_data; seq = p->art_sequence_number; mde = REALTIME_MODE_ARTNET; } else if (protocol == P_E131) { // Ignore PREVIEW data (E1.31: 6.2.6) if ((p->options & 0x80) != 0) return; dmxChannels = htons(p->property_value_count) - 1; // DMX level data is zero start code. Ignore everything else. (E1.11: 8.5) if (dmxChannels == 0 || p->property_values[0] != 0) return; uni = htons(p->universe); e131_data = p->property_values; seq = p->sequence_number; if (e131Priority != 0) { if (p->priority < e131Priority ) return; // track highest priority & skip all lower priorities if (p->priority >= highPriority.get()) highPriority.set(p->priority); if (p->priority < highPriority.get()) return; } } else { //DDP realtimeIP = clientIP; handleDDPPacket(p); return; } #ifdef WLED_ENABLE_DMX // does not act on out-of-order packets yet if (e131ProxyUniverse > 0 && uni == e131ProxyUniverse) { for (uint16_t i = 1; i <= dmxChannels; i++) dmx.write(i, e131_data[i]); dmx.update(); } #endif // only listen for universes we're handling & allocated memory if (uni < e131Universe || uni >= (e131Universe + E131_MAX_UNIVERSE_COUNT)) return; unsigned previousUniverses = uni - e131Universe; if (e131SkipOutOfSequence) if (seq < e131LastSequenceNumber[previousUniverses] && seq > 20 && e131LastSequenceNumber[previousUniverses] < 250){ DEBUG_PRINTF_P(PSTR("skipping E1.31 frame (last seq=%d, current seq=%d, universe=%d)\n"), e131LastSequenceNumber[previousUniverses], seq, uni); return; } e131LastSequenceNumber[previousUniverses] = seq; // update status info realtimeIP = clientIP; handleDMXData(uni, dmxChannels, e131_data, mde, previousUniverses); } void handleDMXData(uint16_t uni, uint16_t dmxChannels, uint8_t* e131_data, uint8_t mde, uint8_t previousUniverses) { byte wChannel = 0; unsigned totalLen = strip.getLengthTotal(); unsigned availDMXLen = 0; unsigned dataOffset = DMXAddress; // For legacy DMX start address 0 the available DMX length offset is 0 const unsigned dmxLenOffset = (DMXAddress == 0) ? 0 : 1; // Check if DMX start address fits in available channels if (dmxChannels >= DMXAddress) { availDMXLen = (dmxChannels - DMXAddress) + dmxLenOffset; } // DMX data in Art-Net packet starts at index 0, for E1.31 at index 1 if (mde == REALTIME_MODE_ARTNET && dataOffset > 0) { dataOffset--; } switch (DMXMode) { case DMX_MODE_DISABLED: return; // nothing to do break; case DMX_MODE_SINGLE_RGB: // 3 channel: [R,G,B] if (uni != e131Universe) return; if (availDMXLen < 3) return; realtimeLock(realtimeTimeoutMs, mde); if (realtimeOverride) return; wChannel = (availDMXLen > 3) ? e131_data[dataOffset+3] : 0; for (unsigned i = 0; i < totalLen; i++) setRealtimePixel(i, e131_data[dataOffset+0], e131_data[dataOffset+1], e131_data[dataOffset+2], wChannel); break; case DMX_MODE_SINGLE_DRGB: // 4 channel: [Dimmer,R,G,B] if (uni != e131Universe) return; if (availDMXLen < 4) return; realtimeLock(realtimeTimeoutMs, mde); if (realtimeOverride) return; wChannel = (availDMXLen > 4) ? e131_data[dataOffset+4] : 0; if (bri != e131_data[dataOffset+0]) { bri = e131_data[dataOffset+0]; strip.setBrightness(bri, true); } for (unsigned i = 0; i < totalLen; i++) setRealtimePixel(i, e131_data[dataOffset+1], e131_data[dataOffset+2], e131_data[dataOffset+3], wChannel); break; case DMX_MODE_PRESET: // 2 channel: [Dimmer,Preset] { if (uni != e131Universe || availDMXLen < 2) return; // limit max. selectable preset to 250, even though DMX max. val is 255 int dmxValPreset = (e131_data[dataOffset+1] > 250 ? 250 : e131_data[dataOffset+1]); // only apply preset if value changed if (dmxValPreset != 0 && dmxValPreset != currentPreset && // only apply preset if not in playlist, or playlist changed (currentPlaylist < 0 || dmxValPreset != currentPlaylist)) { presetCycCurr = dmxValPreset; applyPreset(dmxValPreset, CALL_MODE_NOTIFICATION); } // only change brightness if value changed if (bri != e131_data[dataOffset]) { bri = e131_data[dataOffset]; strip.setBrightness(bri, false); stateUpdated(CALL_MODE_WS_SEND); } return; break; } case DMX_MODE_EFFECT: // 15 channels [bri,effectCurrent,effectSpeed,effectIntensity,effectPalette,effectOption,R,G,B,R2,G2,B2,R3,G3,B3] case DMX_MODE_EFFECT_W: // 18 channels, same as above but with extra +3 white channels [..,W,W2,W3] case DMX_MODE_EFFECT_SEGMENT: // 15 channels per segment; case DMX_MODE_EFFECT_SEGMENT_W: // 18 Channels per segment; { if (uni != e131Universe) return; bool isSegmentMode = DMXMode == DMX_MODE_EFFECT_SEGMENT || DMXMode == DMX_MODE_EFFECT_SEGMENT_W; unsigned dmxEffectChannels = (DMXMode == DMX_MODE_EFFECT || DMXMode == DMX_MODE_EFFECT_SEGMENT) ? 15 : 18; for (unsigned id = 0; id < strip.getSegmentsNum(); id++) { Segment& seg = strip.getSegment(id); if (isSegmentMode) dataOffset = DMXAddress + id * (dmxEffectChannels + DMXSegmentSpacing); else dataOffset = DMXAddress; // Modify address for Art-Net data if (mde == REALTIME_MODE_ARTNET && dataOffset > 0) dataOffset--; // Skip out of universe addresses if (dataOffset > dmxChannels - dmxEffectChannels + 1) return; if (e131_data[dataOffset+1] < strip.getModeCount()) if (e131_data[dataOffset+1] != seg.mode) seg.setMode( e131_data[dataOffset+1]); if (e131_data[dataOffset+2] != seg.speed) seg.speed = e131_data[dataOffset+2]; if (e131_data[dataOffset+3] != seg.intensity) seg.intensity = e131_data[dataOffset+3]; if (e131_data[dataOffset+4] != seg.palette) seg.setPalette(e131_data[dataOffset+4]); if (bool(e131_data[dataOffset+5] & 0b00000010) != seg.reverse_y) { seg.reverse_y = bool(e131_data[dataOffset+5] & 0b00000010); } if (bool(e131_data[dataOffset+5] & 0b00000100) != seg.mirror_y) { seg.mirror_y = bool(e131_data[dataOffset+5] & 0b00000100); } if (bool(e131_data[dataOffset+5] & 0b00001000) != seg.transpose) { seg.transpose = bool(e131_data[dataOffset+5] & 0b00001000); } if ((e131_data[dataOffset+5] & 0b00110000) >> 4 != seg.map1D2D) { seg.map1D2D = (e131_data[dataOffset+5] & 0b00110000) >> 4; } // To maintain backwards compatibility with prior e1.31 values, reverse is fixed to mask 0x01000000 if ((e131_data[dataOffset+5] & 0b01000000) != seg.reverse) { seg.reverse = bool(e131_data[dataOffset+5] & 0b01000000); } // To maintain backwards compatibility with prior e1.31 values, mirror is fixed to mask 0x10000000 if ((e131_data[dataOffset+5] & 0b10000000) != seg.mirror) { seg.mirror = bool(e131_data[dataOffset+5] & 0b10000000); } uint32_t colors[3]; byte whites[3] = {0,0,0}; if (dmxEffectChannels == 18) { whites[0] = e131_data[dataOffset+15]; whites[1] = e131_data[dataOffset+16]; whites[2] = e131_data[dataOffset+17]; } colors[0] = RGBW32(e131_data[dataOffset+ 6], e131_data[dataOffset+ 7], e131_data[dataOffset+ 8], whites[0]); colors[1] = RGBW32(e131_data[dataOffset+ 9], e131_data[dataOffset+10], e131_data[dataOffset+11], whites[1]); colors[2] = RGBW32(e131_data[dataOffset+12], e131_data[dataOffset+13], e131_data[dataOffset+14], whites[2]); if (colors[0] != seg.colors[0]) seg.setColor(0, colors[0]); if (colors[1] != seg.colors[1]) seg.setColor(1, colors[1]); if (colors[2] != seg.colors[2]) seg.setColor(2, colors[2]); // Set segment opacity or global brightness if (isSegmentMode) { if (e131_data[dataOffset] != seg.opacity) seg.setOpacity(e131_data[dataOffset]); } else if ( id == strip.getSegmentsNum()-1U ) { if (bri != e131_data[dataOffset]) { bri = e131_data[dataOffset]; strip.setBrightness(bri, true); } } } return; break; } case DMX_MODE_MULTIPLE_DRGB: case DMX_MODE_MULTIPLE_RGB: case DMX_MODE_MULTIPLE_RGBW: { const bool is4Chan = (DMXMode == DMX_MODE_MULTIPLE_RGBW); const unsigned dmxChannelsPerLed = is4Chan ? 4 : 3; const unsigned ledsPerUniverse = is4Chan ? MAX_4_CH_LEDS_PER_UNIVERSE : MAX_3_CH_LEDS_PER_UNIVERSE; uint8_t stripBrightness = bri; unsigned previousLeds, dmxOffset, ledsTotal; if (previousUniverses == 0) { if (availDMXLen < 1) return; dmxOffset = dataOffset; previousLeds = 0; // First DMX address is dimmer in DMX_MODE_MULTIPLE_DRGB mode. if (DMXMode == DMX_MODE_MULTIPLE_DRGB) { stripBrightness = e131_data[dmxOffset++]; ledsTotal = (availDMXLen - 1) / dmxChannelsPerLed; } else { ledsTotal = availDMXLen / dmxChannelsPerLed; } } else { // All subsequent universes start at the first channel. dmxOffset = (mde == REALTIME_MODE_ARTNET) ? 0 : 1; const unsigned dimmerOffset = (DMXMode == DMX_MODE_MULTIPLE_DRGB) ? 1 : 0; unsigned ledsInFirstUniverse = (((MAX_CHANNELS_PER_UNIVERSE - DMXAddress) + dmxLenOffset) - dimmerOffset) / dmxChannelsPerLed; previousLeds = ledsInFirstUniverse + (previousUniverses - 1) * ledsPerUniverse; ledsTotal = previousLeds + (dmxChannels / dmxChannelsPerLed); } // All LEDs already have values if (previousLeds >= totalLen) { return; } realtimeLock(realtimeTimeoutMs, mde); if (realtimeOverride) return; if (ledsTotal > totalLen) { ledsTotal = totalLen; } if (DMXMode == DMX_MODE_MULTIPLE_DRGB && previousUniverses == 0) { if (bri != stripBrightness) { bri = stripBrightness; strip.setBrightness(bri, true); } } for (unsigned i = previousLeds; i < ledsTotal; i++) { setRealtimePixel(i, e131_data[dmxOffset], e131_data[dmxOffset+1], e131_data[dmxOffset+2], is4Chan ? e131_data[dmxOffset+3] : 0); dmxOffset += dmxChannelsPerLed; } break; } default: DEBUG_PRINTLN(F("unknown E1.31 DMX mode")); return; // nothing to do break; } e131NewData = true; } static void handleArtnetPollReply(IPAddress ipAddress) { ArtPollReply artnetPollReply; prepareArtnetPollReply(&artnetPollReply); unsigned startUniverse = e131Universe; unsigned endUniverse = e131Universe; switch (DMXMode) { case DMX_MODE_DISABLED: break; case DMX_MODE_SINGLE_RGB: case DMX_MODE_SINGLE_DRGB: case DMX_MODE_PRESET: case DMX_MODE_EFFECT: case DMX_MODE_EFFECT_W: case DMX_MODE_EFFECT_SEGMENT: case DMX_MODE_EFFECT_SEGMENT_W: break; // 1 universe is enough case DMX_MODE_MULTIPLE_DRGB: case DMX_MODE_MULTIPLE_RGB: case DMX_MODE_MULTIPLE_RGBW: { bool is4Chan = (DMXMode == DMX_MODE_MULTIPLE_RGBW); const unsigned dmxChannelsPerLed = is4Chan ? 4 : 3; const unsigned dimmerOffset = (DMXMode == DMX_MODE_MULTIPLE_DRGB) ? 1 : 0; const unsigned dmxLenOffset = (DMXAddress == 0) ? 0 : 1; // For legacy DMX start address 0 const unsigned ledsInFirstUniverse = (((MAX_CHANNELS_PER_UNIVERSE - DMXAddress) + dmxLenOffset) - dimmerOffset) / dmxChannelsPerLed; const unsigned totalLen = strip.getLengthTotal(); if (totalLen > ledsInFirstUniverse) { const unsigned ledsPerUniverse = is4Chan ? MAX_4_CH_LEDS_PER_UNIVERSE : MAX_3_CH_LEDS_PER_UNIVERSE; const unsigned remainLED = totalLen - ledsInFirstUniverse; endUniverse += (remainLED / ledsPerUniverse); if ((remainLED % ledsPerUniverse) > 0) { endUniverse++; } if ((endUniverse - startUniverse) > E131_MAX_UNIVERSE_COUNT) { endUniverse = startUniverse + E131_MAX_UNIVERSE_COUNT - 1; } } break; } default: DEBUG_PRINTLN(F("unknown E1.31 DMX mode")); return; // nothing to do break; } if (DMXMode != DMX_MODE_DISABLED) { for (unsigned i = startUniverse; i <= endUniverse; ++i) { sendArtnetPollReply(&artnetPollReply, ipAddress, i); } } #ifdef WLED_ENABLE_DMX if (e131ProxyUniverse > 0 && (DMXMode == DMX_MODE_DISABLED || (e131ProxyUniverse < startUniverse || e131ProxyUniverse > endUniverse))) { sendArtnetPollReply(&artnetPollReply, ipAddress, e131ProxyUniverse); } #endif } static void prepareArtnetPollReply(ArtPollReply *reply) { // Art-Net reply->reply_id[0] = 0x41; reply->reply_id[1] = 0x72; reply->reply_id[2] = 0x74; reply->reply_id[3] = 0x2d; reply->reply_id[4] = 0x4e; reply->reply_id[5] = 0x65; reply->reply_id[6] = 0x74; reply->reply_id[7] = 0x00; reply->reply_opcode = ARTNET_OPCODE_OPPOLLREPLY; IPAddress localIP = Network.localIP(); for (unsigned i = 0; i < 4; i++) { reply->reply_ip[i] = localIP[i]; } reply->reply_port = ARTNET_DEFAULT_PORT; char * numberEnd = (char*) versionString; // strtol promises not to try to edit this. reply->reply_version_h = (uint8_t)strtol(numberEnd, &numberEnd, 10); numberEnd++; reply->reply_version_l = (uint8_t)strtol(numberEnd, &numberEnd, 10); // Switch values depend on universe, set before sending reply->reply_net_sw = 0x00; reply->reply_sub_sw = 0x00; reply->reply_oem_h = 0x00; // TODO add assigned oem code reply->reply_oem_l = 0x00; reply->reply_ubea_ver = 0x00; // Indicators in Normal Mode // All or part of Port-Address programmed by network or Web browser reply->reply_status_1 = 0xE0; reply->reply_esta_man = 0x0000; strlcpy((char *)(reply->reply_short_name), serverDescription, 18); strlcpy((char *)(reply->reply_long_name), serverDescription, 64); reply->reply_node_report[0] = '\0'; reply->reply_num_ports_h = 0x00; reply->reply_num_ports_l = 0x01; // One output port reply->reply_port_types[0] = 0x80; // Output DMX data reply->reply_port_types[1] = 0x00; reply->reply_port_types[2] = 0x00; reply->reply_port_types[3] = 0x00; // No inputs reply->reply_good_input[0] = 0x00; reply->reply_good_input[1] = 0x00; reply->reply_good_input[2] = 0x00; reply->reply_good_input[3] = 0x00; // One output reply->reply_good_output_a[0] = 0x80; // Data is being transmitted reply->reply_good_output_a[1] = 0x00; reply->reply_good_output_a[2] = 0x00; reply->reply_good_output_a[3] = 0x00; // Values depend on universe, set before sending reply->reply_sw_in[0] = 0x00; reply->reply_sw_in[1] = 0x00; reply->reply_sw_in[2] = 0x00; reply->reply_sw_in[3] = 0x00; // Values depend on universe, set before sending reply->reply_sw_out[0] = 0x00; reply->reply_sw_out[1] = 0x00; reply->reply_sw_out[2] = 0x00; reply->reply_sw_out[3] = 0x00; reply->reply_sw_video = 0x00; reply->reply_sw_macro = 0x00; reply->reply_sw_remote = 0x00; reply->reply_spare[0] = 0x00; reply->reply_spare[1] = 0x00; reply->reply_spare[2] = 0x00; // A DMX to / from Art-Net device reply->reply_style = 0x00; Network.localMAC(reply->reply_mac); for (unsigned i = 0; i < 4; i++) { reply->reply_bind_ip[i] = localIP[i]; } reply->reply_bind_index = 1; // Product supports web browser configuration // Node’s IP is DHCP or manually configured // Node is DHCP capable // Node supports 15 bit Port-Address (Art-Net 3 or 4) // Node is able to switch between ArtNet and sACN reply->reply_status_2 = (multiWiFi[0].staticIP[0] == 0) ? 0x1F : 0x1D; // RDM is disabled // Output style is continuous reply->reply_good_output_b[0] = 0xC0; reply->reply_good_output_b[1] = 0xC0; reply->reply_good_output_b[2] = 0xC0; reply->reply_good_output_b[3] = 0xC0; // Fail-over state: Hold last state // Node does not support fail-over reply->reply_status_3 = 0x00; for (unsigned i = 0; i < 21; i++) { reply->reply_filler[i] = 0x00; } } static void sendArtnetPollReply(ArtPollReply *reply, IPAddress ipAddress, uint16_t portAddress) { reply->reply_net_sw = (uint8_t)((portAddress >> 8) & 0x007F); reply->reply_sub_sw = (uint8_t)((portAddress >> 4) & 0x000F); reply->reply_sw_out[0] = (uint8_t)(portAddress & 0x000F); snprintf_P((char *)reply->reply_node_report, sizeof(reply->reply_node_report)-1, PSTR("#0001 [%04u] OK - WLED v%s"), pollReplyCount, versionString); if (pollReplyCount < 9999) { pollReplyCount++; } else { pollReplyCount = 0; } notifierUdp.beginPacket(ipAddress, ARTNET_DEFAULT_PORT); notifierUdp.write(reply->raw, sizeof(ArtPollReply)); notifierUdp.endPacket(); reply->reply_bind_index++; } ================================================ FILE: wled00/fcn_declare.h ================================================ #pragma once #ifndef WLED_FCN_DECLARE_H #define WLED_FCN_DECLARE_H #include /* * All globally accessible functions are declared here */ //alexa.cpp #ifndef WLED_DISABLE_ALEXA void onAlexaChange(EspalexaDevice* dev); void alexaInit(); void handleAlexa(); void onAlexaChange(EspalexaDevice* dev); #endif //button.cpp void shortPressAction(uint8_t b=0); void longPressAction(uint8_t b=0); void doublePressAction(uint8_t b=0); bool isButtonPressed(uint8_t b=0); void handleButton(); void handleOnOff(bool forceOff = false); void handleIO(); void IRAM_ATTR touchButtonISR(); //cfg.cpp bool backupConfig(); bool restoreConfig(); bool verifyConfig(); bool configBackupExists(); void resetConfig(); bool deserializeConfig(JsonObject doc, bool fromFS = false); bool deserializeConfigFromFS(); bool deserializeConfigSec(); void serializeConfig(JsonObject doc); void serializeConfigToFS(); void serializeConfigSec(); template bool getJsonValue(const JsonVariant& element, DestType& destination) { if (element.isNull()) { return false; } destination = element.as(); return true; } template bool getJsonValue(const JsonVariant& element, DestType& destination, const DefaultType defaultValue) { if(!getJsonValue(element, destination)) { destination = defaultValue; return false; } return true; } typedef struct WiFiConfig { char clientSSID[33]; char clientPass[65]; uint8_t bssid[6]; IPAddress staticIP; IPAddress staticGW; IPAddress staticSN; #ifdef WLED_ENABLE_WPA_ENTERPRISE byte encryptionType; char enterpriseAnonIdentity[65]; char enterpriseIdentity[65]; WiFiConfig(const char *ssid="", const char *pass="", uint32_t ip=0, uint32_t gw=0, uint32_t subnet=0x00FFFFFF // little endian , byte enc_type=WIFI_ENCRYPTION_TYPE_PSK, const char *ent_anon="", const char *ent_iden="") #else WiFiConfig(const char *ssid="", const char *pass="", uint32_t ip=0, uint32_t gw=0, uint32_t subnet=0x00FFFFFF) // little endian #endif : staticIP(ip) , staticGW(gw) , staticSN(subnet) { strncpy(clientSSID, ssid, 32); clientSSID[32] = 0; strncpy(clientPass, pass, 64); clientPass[64] = 0; #ifdef WLED_ENABLE_WPA_ENTERPRISE encryptionType = enc_type; strncpy(enterpriseAnonIdentity, ent_anon, 64); enterpriseAnonIdentity[64] = 0; strncpy(enterpriseIdentity, ent_iden, 64); enterpriseIdentity[64] = 0; #endif memset(bssid, 0, sizeof(bssid)); } } wifi_config; //dmx_output.cpp void initDMXOutput(); void handleDMXOutput(); //dmx_input.cpp void initDMXInput(); void handleDMXInput(); //e131.cpp void handleE131Packet(e131_packet_t* p, IPAddress clientIP, byte protocol); void handleDMXData(uint16_t uni, uint16_t dmxChannels, uint8_t* e131_data, uint8_t mde, uint8_t previousUniverses); // void handleArtnetPollReply(IPAddress ipAddress); // local function, only used in e131.cpp // void prepareArtnetPollReply(ArtPollReply* reply); // local function, only used in e131.cpp // void sendArtnetPollReply(ArtPollReply* reply, IPAddress ipAddress, uint16_t portAddress); // local function, only used in e131.cpp //file.cpp bool handleFileRead(AsyncWebServerRequest*, String path); bool writeObjectToFileUsingId(const char* file, uint16_t id, const JsonDocument* content); bool writeObjectToFile(const char* file, const char* key, const JsonDocument* content); bool readObjectFromFileUsingId(const char* file, uint16_t id, JsonDocument* dest, const JsonDocument* filter = nullptr); bool readObjectFromFile(const char* file, const char* key, JsonDocument* dest, const JsonDocument* filter = nullptr); void updateFSInfo(); void closeFile(); inline bool writeObjectToFileUsingId(const String &file, uint16_t id, const JsonDocument* content) { return writeObjectToFileUsingId(file.c_str(), id, content); }; inline bool writeObjectToFile(const String &file, const char* key, const JsonDocument* content) { return writeObjectToFile(file.c_str(), key, content); }; inline bool readObjectFromFileUsingId(const String &file, uint16_t id, JsonDocument* dest, const JsonDocument* filter = nullptr) { return readObjectFromFileUsingId(file.c_str(), id, dest); }; inline bool readObjectFromFile(const String &file, const char* key, JsonDocument* dest, const JsonDocument* filter = nullptr) { return readObjectFromFile(file.c_str(), key, dest); }; bool copyFile(const char* src_path, const char* dst_path); bool backupFile(const char* filename); bool restoreFile(const char* filename); bool checkBackupExists(const char* filename); bool validateJsonFile(const char* filename); void dumpFilesToSerial(); //hue.cpp void handleHue(); void reconnectHue(); void onHueError(void* arg, AsyncClient* client, int8_t error); void onHueConnect(void* arg, AsyncClient* client); void sendHuePoll(); void onHueData(void* arg, AsyncClient* client, void *data, size_t len); //image_loader.cpp class Segment; #ifdef WLED_ENABLE_GIF bool fileSeekCallback(unsigned long position); unsigned long filePositionCallback(void); int fileReadCallback(void); int fileReadBlockCallback(void * buffer, int numberOfBytes); int fileSizeCallback(void); byte renderImageToSegment(Segment &seg); void endImagePlayback(Segment* seg); #endif //improv.cpp enum ImprovRPCType { Command_Wifi = 0x01, Request_State = 0x02, Request_Info = 0x03, Request_Scan = 0x04 }; void handleImprovPacket(); void sendImprovRPCResult(ImprovRPCType type, uint8_t n_strings = 0, const char **strings = nullptr); void sendImprovStateResponse(uint8_t state, bool error = false); void sendImprovInfoResponse(); void startImprovWifiScan(); void handleImprovWifiScan(); void sendImprovIPRPCResult(ImprovRPCType type); //ir.cpp void initIR(); void deInitIR(); void handleIR(); //json.cpp #include "ESPAsyncWebServer.h" #include "src/dependencies/json/ArduinoJson-v6.h" #include "src/dependencies/json/AsyncJson-v6.h" bool deserializeState(JsonObject root, byte callMode = CALL_MODE_DIRECT_CHANGE, byte presetId = 0); void serializeSegment(const JsonObject& root, const Segment& seg, byte id, bool forPreset = false, bool segmentBounds = true); void serializeState(JsonObject root, bool forPreset = false, bool includeBri = true, bool segmentBounds = true, bool selectedSegmentsOnly = false); void serializeInfo(JsonObject root); void serializeModeNames(JsonArray arr); void serializeModeData(JsonArray fxdata); void serializePins(JsonObject root); void serveJson(AsyncWebServerRequest* request); #ifdef WLED_ENABLE_JSONLIVE bool serveLiveLeds(AsyncWebServerRequest* request, uint32_t wsClient = 0); #endif //led.cpp void setValuesFromSegment(uint8_t s); #define setValuesFromMainSeg() setValuesFromSegment(strip.getMainSegmentId()) #define setValuesFromFirstSelectedSeg() setValuesFromSegment(strip.getFirstSelectedSegId()) void toggleOnOff(); void applyBri(); void applyFinalBri(); void applyValuesToSelectedSegs(); void colorUpdated(byte callMode); void stateUpdated(byte callMode); void updateInterfaces(uint8_t callMode); void handleTransitions(); void handleNightlight(); byte scaledBri(byte in); #ifdef WLED_ENABLE_LOXONE //lx_parser.cpp bool parseLx(int lxValue, byte* rgbw); void parseLxJson(int lxValue, byte segId, bool secondary); #endif //mqtt.cpp bool initMqtt(); void publishMqtt(); //ntp.cpp void handleTime(); void handleNetworkTime(); // void sendNTPPacket(); // local function, only used in ntp.cpp // bool checkNTPResponse(); // local function, only used in ntp.cpp void updateLocalTime(); void getTimeString(char* out); bool checkCountdown(); void setCountdown(); byte weekdayMondayFirst(); bool isTodayInDateRange(byte monthStart, byte dayStart, byte monthEnd, byte dayEnd); void checkTimers(); void calculateSunriseAndSunset(); void setTimeFromAPI(uint32_t timein); //overlay.cpp void handleOverlayDraw(); // void _overlayAnalogCountdown(); // local function, only used in overlay.cpp // void _overlayAnalogClock(); // local function, only used in overlay.cpp //playlist.cpp void shufflePlaylist(); void unloadPlaylist(); int16_t loadPlaylist(JsonObject playlistObject, byte presetId = 0); void handlePlaylist(); void serializePlaylist(JsonObject obj); //presets.cpp const char *getPresetsFileName(bool persistent = true); bool presetNeedsSaving(); void initPresetsFile(); void handlePresets(); bool applyPreset(byte index, byte callMode = CALL_MODE_DIRECT_CHANGE); bool applyPresetFromPlaylist(byte index); void applyPresetWithFallback(uint8_t presetID, uint8_t callMode, uint8_t effectID = 0, uint8_t paletteID = 0); inline bool applyTemporaryPreset() {return applyPreset(255);}; void savePreset(byte index, const char* pname = nullptr, JsonObject saveobj = JsonObject()); inline void saveTemporaryPreset() {savePreset(255);}; void deletePreset(byte index); bool getPresetName(byte index, String& name); //remote.cpp void handleWiZdata(uint8_t *incomingData, size_t len); void handleRemote(); //set.cpp bool isAsterisksOnly(const char* str, byte maxLen); void handleSettingsSet(AsyncWebServerRequest *request, byte subPage); bool handleSet(AsyncWebServerRequest *request, const String& req, bool apply=true); //udp.cpp void notify(byte callMode, bool followUp=false); uint8_t realtimeBroadcast(uint8_t type, IPAddress client, uint16_t length, const uint8_t* buffer, uint8_t bri=255, bool isRGBW=false); void realtimeLock(uint32_t timeoutMs, byte md = REALTIME_MODE_GENERIC); void exitRealtime(); void handleNotifications(); void setRealtimePixel(uint16_t i, byte r, byte g, byte b, byte w); void refreshNodeList(); void sendSysInfoUDP(); #ifndef WLED_DISABLE_ESPNOW void espNowSentCB(uint8_t* address, uint8_t status); void espNowReceiveCB(uint8_t* address, uint8_t* data, uint8_t len, signed int rssi, bool broadcast); #endif //network.cpp bool initEthernet(); // result is informational int getSignalQuality(int rssi); void fillMAC2Str(char *str, const uint8_t *mac); void fillStr2MAC(uint8_t *mac, const char *str); int findWiFi(bool doScan = false); bool isWiFiConfigured(); void WiFiEvent(WiFiEvent_t event); //um_manager.cpp typedef enum UM_Data_Types { UMT_BYTE = 0, UMT_UINT16, UMT_INT16, UMT_UINT32, UMT_INT32, UMT_FLOAT, UMT_DOUBLE, UMT_BYTE_ARR, UMT_UINT16_ARR, UMT_INT16_ARR, UMT_UINT32_ARR, UMT_INT32_ARR, UMT_FLOAT_ARR, UMT_DOUBLE_ARR } um_types_t; typedef struct UM_Exchange_Data { // should just use: size_t arr_size, void **arr_ptr, byte *ptr_type size_t u_size; // size of u_data array um_types_t *u_type; // array of data types void **u_data; // array of pointers to data UM_Exchange_Data() { u_size = 0; u_type = nullptr; u_data = nullptr; } ~UM_Exchange_Data() { if (u_type) delete[] u_type; if (u_data) delete[] u_data; } } um_data_t; const unsigned int um_data_size = sizeof(um_data_t); // 12 bytes class Usermod { protected: um_data_t *um_data; // um_data should be allocated using new in (derived) Usermod's setup() or constructor public: Usermod() : um_data(nullptr) {}; virtual ~Usermod() { if (um_data) delete um_data; } virtual void setup() = 0; // pure virtual, has to be overriden virtual void loop() = 0; // pure virtual, has to be overriden virtual void handleOverlayDraw() {} // called after all effects have been processed, just before strip.show() virtual bool handleButton(uint8_t b) { return false; } // button overrides are possible here virtual bool getUMData(um_data_t **data) { if (data) *data = nullptr; return false; }; // usermod data exchange [see examples for audio effects] virtual void connected() {} // called when WiFi is (re)connected virtual void appendConfigData(Print& settingsScript); // helper function called from usermod settings page to add metadata for entry fields virtual void addToJsonState(JsonObject& obj) {} // add JSON objects for WLED state virtual void addToJsonInfo(JsonObject& obj) {} // add JSON objects for UI Info page virtual void readFromJsonState(JsonObject& obj) {} // process JSON messages received from web server virtual void addToConfig(JsonObject& obj) {} // add JSON entries that go to cfg.json virtual bool readFromConfig(JsonObject& obj) { return true; } // Note as of 2021-06 readFromConfig() now needs to return a bool, see usermod_v2_example.h virtual void onMqttConnect(bool sessionPresent) {} // fired when MQTT connection is established (so usermod can subscribe) virtual bool onMqttMessage(char* topic, char* payload) { return false; } // fired upon MQTT message received (wled topic) virtual bool onEspNowMessage(uint8_t* sender, uint8_t* payload, uint8_t len) { return false; } // fired upon ESP-NOW message received virtual bool onUdpPacket(uint8_t* payload, size_t len) { return false; } //fired upon UDP packet received virtual void onUpdateBegin(bool) {} // fired prior to and after unsuccessful firmware update virtual void onStateChange(uint8_t mode) {} // fired upon WLED state change virtual uint16_t getId() {return USERMOD_ID_UNSPECIFIED;} // API shims private: static Print* oappend_shim; // old form of appendConfigData; called by default appendConfigData(Print&) with oappend_shim set up // private so it is not accidentally invoked except via Usermod::appendConfigData(Print&) virtual void appendConfigData() {} protected: // Shim for oappend(), which used to exist in utils.cpp template static inline void oappend(const T& t) { oappend_shim->print(t); }; #ifdef ESP8266 // Handle print(PSTR()) without crashing by detecting PROGMEM strings static void oappend(const char* c) { if ((intptr_t) c >= 0x40000000) oappend_shim->print(FPSTR(c)); else oappend_shim->print(c); }; #endif }; namespace UsermodManager { void loop(); void handleOverlayDraw(); bool handleButton(uint8_t b); bool getUMData(um_data_t **um_data, uint8_t mod_id = USERMOD_ID_RESERVED); // USERMOD_ID_RESERVED will poll all usermods void setup(); void connected(); void appendConfigData(Print&); void addToJsonState(JsonObject& obj); void addToJsonInfo(JsonObject& obj); void readFromJsonState(JsonObject& obj); void addToConfig(JsonObject& obj); bool readFromConfig(JsonObject& obj); #ifndef WLED_DISABLE_MQTT void onMqttConnect(bool sessionPresent); bool onMqttMessage(char* topic, char* payload); #endif #ifndef WLED_DISABLE_ESPNOW bool onEspNowMessage(uint8_t* sender, uint8_t* payload, uint8_t len); #endif bool onUdpPacket(uint8_t* payload, size_t len); void onUpdateBegin(bool); void onStateChange(uint8_t); Usermod* lookup(uint16_t mod_id); size_t getModCount(); }; // Register usermods by building a static list via a linker section #define REGISTER_USERMOD(x) DYNARRAY_MEMBER(Usermod*, usermods, um_##x, 1) = &x //usermod.cpp void userSetup(); void userConnected(); void userLoop(); //util.cpp #ifdef ESP8266 #define HW_RND_REGISTER RANDOM_REG32 #else // ESP32 family #include "soc/wdev_reg.h" #define HW_RND_REGISTER REG_READ(WDEV_RND_REG) #endif #define inoise8 perlin8 // fastled legacy alias #define inoise16 perlin16 // fastled legacy alias #define hex2int(a) (((a)>='0' && (a)<='9') ? (a)-'0' : ((a)>='A' && (a)<='F') ? (a)-'A'+10 : ((a)>='a' && (a)<='f') ? (a)-'a'+10 : 0) [[gnu::pure]] int getNumVal(const String &req, uint16_t pos); void parseNumber(const char* str, byte &val, byte minv=0, byte maxv=255); bool getVal(JsonVariant elem, byte &val, byte vmin=0, byte vmax=255); // getVal supports inc/decrementing and random ("X~Y(r|[w]~[-][Z])" form) [[gnu::pure]] bool getBoolVal(const JsonVariant &elem, bool dflt); bool updateVal(const char* req, const char* key, byte &val, byte minv=0, byte maxv=255); size_t printSetFormCheckbox(Print& settingsScript, const char* key, int val); size_t printSetFormValue(Print& settingsScript, const char* key, int val); size_t printSetFormValue(Print& settingsScript, const char* key, const char* val); size_t printSetFormIndex(Print& settingsScript, const char* key, int index); size_t printSetClassElementHTML(Print& settingsScript, const char* key, const int index, const char* val); void prepareHostname(char* hostname); [[gnu::pure]] bool isAsterisksOnly(const char* str, byte maxLen); bool requestJSONBufferLock(uint8_t moduleID=JSON_LOCK_UNKNOWN); void releaseJSONBufferLock(); uint8_t extractModeName(uint8_t mode, const char *src, char *dest, uint8_t maxLen); uint8_t extractModeSlider(uint8_t mode, uint8_t slider, char *dest, uint8_t maxLen, uint8_t *var = nullptr); int16_t extractModeDefaults(uint8_t mode, const char *segVar); void checkSettingsPIN(const char *pin); uint16_t crc16(const unsigned char* data_p, size_t length); String computeSHA1(const String& input); String getDeviceId(); uint16_t beatsin88_t(accum88 beats_per_minute_88, uint16_t lowest = 0, uint16_t highest = 65535, uint32_t timebase = 0, uint16_t phase_offset = 0); uint16_t beatsin16_t(accum88 beats_per_minute, uint16_t lowest = 0, uint16_t highest = 65535, uint32_t timebase = 0, uint16_t phase_offset = 0); uint8_t beatsin8_t(accum88 beats_per_minute, uint8_t lowest = 0, uint8_t highest = 255, uint32_t timebase = 0, uint8_t phase_offset = 0); um_data_t* simulateSound(uint8_t simulationId); void enumerateLedmaps(); [[gnu::hot]] uint8_t get_random_wheel_index(uint8_t pos); [[gnu::hot, gnu::pure]] float mapf(float x, float in_min, float in_max, float out_min, float out_max); uint32_t hashInt(uint32_t s); int32_t perlin1D_raw(uint32_t x, bool is16bit = false); int32_t perlin2D_raw(uint32_t x, uint32_t y, bool is16bit = false); int32_t perlin3D_raw(uint32_t x, uint32_t y, uint32_t z, bool is16bit = false); uint16_t perlin16(uint32_t x); uint16_t perlin16(uint32_t x, uint32_t y); uint16_t perlin16(uint32_t x, uint32_t y, uint32_t z); uint8_t perlin8(uint16_t x); uint8_t perlin8(uint16_t x, uint16_t y); uint8_t perlin8(uint16_t x, uint16_t y, uint16_t z); // fast (true) random numbers using hardware RNG, all functions return values in the range lowerlimit to upperlimit-1 // note: for true random numbers with high entropy, do not call faster than every 200ns (5MHz) // tests show it is still highly random reading it quickly in a loop (better than fastled PRNG) // for 8bit and 16bit random functions: no limit check is done for best speed // 32bit inputs are used for speed and code size, limits don't work if inverted or out of range // inlining does save code size except for random(a,b) and 32bit random with limits #define random hw_random // replace arduino random() inline uint32_t hw_random() { return HW_RND_REGISTER; }; uint32_t hw_random(uint32_t upperlimit); // not inlined for code size int32_t hw_random(int32_t lowerlimit, int32_t upperlimit); inline uint16_t hw_random16() { return HW_RND_REGISTER; }; inline uint16_t hw_random16(uint32_t upperlimit) { return (hw_random16() * upperlimit) >> 16; }; // input range 0-65535 (uint16_t) inline int16_t hw_random16(int32_t lowerlimit, int32_t upperlimit) { int32_t range = upperlimit - lowerlimit; return lowerlimit + hw_random16(range); }; // signed limits, use int16_t ranges inline uint8_t hw_random8() { return HW_RND_REGISTER; }; inline uint8_t hw_random8(uint32_t upperlimit) { return (hw_random8() * upperlimit) >> 8; }; // input range 0-255 inline uint8_t hw_random8(uint32_t lowerlimit, uint32_t upperlimit) { uint32_t range = upperlimit - lowerlimit; return lowerlimit + hw_random8(range); }; // input range 0-255 // memory allocation wrappers (util.cpp) extern "C" { // prefer DRAM in d_xalloc functions, PSRAM as fallback void *d_malloc(size_t); void *d_calloc(size_t, size_t); void *d_realloc_malloc(void *ptr, size_t size); #ifndef ESP8266 inline void d_free(void *ptr) { heap_caps_free(ptr); } #else inline void d_free(void *ptr) { free(ptr); } #endif #if defined(BOARD_HAS_PSRAM) // prefer PSRAM in p_xalloc functions, DRAM as fallback void *p_malloc(size_t); void *p_calloc(size_t, size_t); void *p_realloc_malloc(void *ptr, size_t size); inline void p_free(void *ptr) { heap_caps_free(ptr); } #else #define p_malloc d_malloc #define p_calloc d_calloc #define p_realloc_malloc d_realloc_malloc #define p_free d_free #endif } #ifndef ESP8266 inline size_t getFreeHeapSize() { return heap_caps_get_free_size(MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT); } // returns free heap (ESP.getFreeHeap() can include other memory types) inline size_t getContiguousFreeHeap() { return heap_caps_get_largest_free_block(MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT); } // returns largest contiguous free block #else inline size_t getFreeHeapSize() { return ESP.getFreeHeap(); } // returns free heap inline size_t getContiguousFreeHeap() { return ESP.getMaxFreeBlockSize(); } // returns largest contiguous free block #endif #define BFRALLOC_NOBYTEACCESS (1 << 0) // ESP32 has 32bit accessible DRAM (usually ~50kB free) that must not be byte-accessed #define BFRALLOC_PREFER_DRAM (1 << 1) // prefer DRAM over PSRAM #define BFRALLOC_ENFORCE_DRAM (1 << 2) // use DRAM only, no PSRAM #define BFRALLOC_PREFER_PSRAM (1 << 3) // prefer PSRAM over DRAM #define BFRALLOC_ENFORCE_PSRAM (1 << 4) // use PSRAM if available, otherwise uses DRAM #define BFRALLOC_CLEAR (1 << 5) // clear allocated buffer after allocation void *allocate_buffer(size_t size, uint32_t type); void handleBootLoop(); // detect and handle bootloops #ifndef ESP8266 void bootloopCheckOTA(); // swap boot image if bootloop is detected instead of restoring config #endif // RAII guard class for the JSON Buffer lock // Modeled after std::lock_guard class JSONBufferGuard { bool holding_lock; public: inline JSONBufferGuard(uint8_t module=JSON_LOCK_UNKNOWN) : holding_lock(requestJSONBufferLock(module)) {}; inline ~JSONBufferGuard() { if (holding_lock) releaseJSONBufferLock(); }; inline JSONBufferGuard(const JSONBufferGuard&) = delete; // Noncopyable inline JSONBufferGuard& operator=(const JSONBufferGuard&) = delete; inline JSONBufferGuard(JSONBufferGuard&& r) : holding_lock(r.holding_lock) { r.holding_lock = false; }; // but movable inline JSONBufferGuard& operator=(JSONBufferGuard&& r) { holding_lock |= r.holding_lock; r.holding_lock = false; return *this; }; inline bool owns_lock() const { return holding_lock; } explicit inline operator bool() const { return owns_lock(); }; inline void release() { if (holding_lock) releaseJSONBufferLock(); holding_lock = false; } }; //wled_math.cpp //float cos_t(float phi); // use float math //float sin_t(float phi); //float tan_t(float x); int16_t sin16_t(uint16_t theta); int16_t cos16_t(uint16_t theta); uint8_t sin8_t(uint8_t theta); uint8_t cos8_t(uint8_t theta); float sin_approx(float theta); // uses integer math (converted to float), accuracy +/-0.0015 (compared to sinf()) float cos_approx(float theta); float tan_approx(float x); float atan2_t(float y, float x); float acos_t(float x); float asin_t(float x); template T atan_t(T x); float floor_t(float x); float fmod_t(float num, float denom); uint32_t sqrt32_bw(uint32_t x); #define sin_t sin_approx #define cos_t cos_approx #define tan_t tan_approx /* #include // standard math functions. use a lot of flash #define sin_t sinf #define cos_t cosf #define tan_t tanf #define asin_t asinf #define acos_t acosf #define atan_t atanf #define fmod_t fmodf #define floor_t floorf */ //wled_serial.cpp void handleSerial(); void updateBaudRate(uint32_t rate); //wled_server.cpp void initServer(); void serveMessage(AsyncWebServerRequest* request, uint16_t code, const String& headl, const String& subl="", byte optionT=255); void serveJsonError(AsyncWebServerRequest* request, uint16_t code, uint16_t error); void serveSettings(AsyncWebServerRequest* request, bool post = false); void serveSettingsJS(AsyncWebServerRequest* request); //ws.cpp void handleWs(); void wsEvent(AsyncWebSocket * server, AsyncWebSocketClient * client, AwsEventType type, void * arg, uint8_t *data, size_t len); void sendDataWs(AsyncWebSocketClient * client = nullptr); //xml.cpp void XML_response(Print& dest); void getSettingsJS(byte subPage, Print& dest); #endif ================================================ FILE: wled00/file.cpp ================================================ #include "wled.h" /* * Utility for SPIFFS filesystem */ #ifdef ARDUINO_ARCH_ESP32 //FS info bare IDF function until FS wrapper is available for ESP32 #if WLED_FS != LITTLEFS && ESP_IDF_VERSION_MAJOR < 4 #include "esp_spiffs.h" #endif #endif #define FS_BUFSIZE 256 /* * Structural requirements for files managed by writeObjectToFile() and readObjectFromFile() utilities: * 1. File must be a string representation of a valid JSON object * 2. File must have '{' as first character * 3. There must not be any additional characters between a root-level key and its value object (e.g. space, tab, newline) * 4. There must not be any characters between an root object-separating ',' and the next object key string * 5. There may be any number of spaces, tabs, and/or newlines before such object-separating ',' * 6. There must not be more than 5 consecutive spaces at any point except for those permitted in condition 5 * 7. If it is desired to delete the first usable object (e.g. preset file), a dummy object '"0":{}' is inserted at the beginning. * It shall be disregarded by receiving software. * The reason for it is that deleting the first preset would require special code to handle commas between it and the 2nd preset */ // There are no consecutive spaces longer than this in the file, so if more space is required, findSpace() can return false immediately // Actual space may be lower constexpr size_t MAX_SPACE = UINT16_MAX * 2U; // smallest supported config has 128Kb flash size static volatile size_t knownLargestSpace = MAX_SPACE; static File f; // don't export to other cpp files //wrapper to find out how long closing takes void closeFile() { #ifdef WLED_DEBUG_FS DEBUGFS_PRINT(F("Close -> ")); uint32_t s = millis(); #endif f.close(); // "if (f)" check is aleady done inside f.close(), and f cannot be nullptr -> no need for double checking before closing the file handle. DEBUGFS_PRINTF("took %lu ms\n", millis() - s); doCloseFile = false; } //find() that reads and buffers data from file stream in 256-byte blocks. //Significantly faster, f.find(key) can take SECONDS for multi-kB files static bool bufferedFind(const char *target, bool fromStart = true) { #ifdef WLED_DEBUG_FS DEBUGFS_PRINT("Find "); DEBUGFS_PRINTLN(target); uint32_t s = millis(); #endif if (!f || !f.size()) return false; size_t targetLen = strlen(target); size_t index = 0; byte buf[FS_BUFSIZE]; if (fromStart) f.seek(0); while (f.position() < f.size() -1) { size_t bufsize = f.read(buf, FS_BUFSIZE); // better to use size_t instead if uint16_t size_t count = 0; while (count < bufsize) { if(buf[count] != target[index]) index = 0; // reset index if any char does not match if(buf[count] == target[index]) { if(++index >= targetLen) { // return true if all chars in the target match f.seek((f.position() - bufsize) + count +1); DEBUGFS_PRINTF("Found at pos %d, took %lu ms", f.position(), millis() - s); return true; } } count++; } } DEBUGFS_PRINTF("No match, took %lu ms\n", millis() - s); return false; } //find empty spots in file stream in 256-byte blocks. static bool bufferedFindSpace(size_t targetLen, bool fromStart = true) { #ifdef WLED_DEBUG_FS DEBUGFS_PRINTF("Find %d spaces\n", targetLen); uint32_t s = millis(); #endif if (knownLargestSpace < targetLen) { DEBUGFS_PRINT(F("No match, KLS ")); DEBUGFS_PRINTLN(knownLargestSpace); return false; } if (!f || !f.size()) return false; size_t index = 0; // better to use size_t instead if uint16_t byte buf[FS_BUFSIZE]; if (fromStart) f.seek(0); while (f.position() < f.size() -1) { size_t bufsize = f.read(buf, FS_BUFSIZE); size_t count = 0; while (count < bufsize) { if(buf[count] == ' ') { if(++index >= targetLen) { // return true if space long enough if (fromStart) { f.seek((f.position() - bufsize) + count +1 - targetLen); knownLargestSpace = MAX_SPACE; //there may be larger spaces after, so we don't know } DEBUGFS_PRINTF("Found at pos %d, took %lu ms", f.position(), millis() - s); return true; } } else { if (!fromStart) return false; if (index) { if (knownLargestSpace < index || (knownLargestSpace == MAX_SPACE)) knownLargestSpace = index; index = 0; // reset index if not space } } count++; } } DEBUGFS_PRINTF("No match, took %lu ms\n", millis() - s); return false; } //find the closing bracket corresponding to the opening bracket at the file pos when calling this function static bool bufferedFindObjectEnd() { #ifdef WLED_DEBUG_FS DEBUGFS_PRINTLN(F("Find obj end")); uint32_t s = millis(); #endif if (!f || !f.size()) return false; unsigned objDepth = 0; //num of '{' minus num of '}'. return once 0 //size_t start = f.position(); byte buf[FS_BUFSIZE]; while (f.position() < f.size() -1) { size_t bufsize = f.read(buf, FS_BUFSIZE); // better to use size_t instead of uint16_t size_t count = 0; while (count < bufsize) { if (buf[count] == '{') objDepth++; if (buf[count] == '}') objDepth--; if (objDepth == 0) { f.seek((f.position() - bufsize) + count +1); DEBUGFS_PRINTF("} at pos %d, took %lu ms", f.position(), millis() - s); return true; } count++; } } DEBUGFS_PRINTF("No match, took %lu ms\n", millis() - s); return false; } //fills n bytes from current file pos with ' ' characters static void writeSpace(size_t l) { byte buf[FS_BUFSIZE]; memset(buf, ' ', FS_BUFSIZE); while (l > 0) { size_t block = (l>FS_BUFSIZE) ? FS_BUFSIZE : l; f.write(buf, block); l -= block; } if (knownLargestSpace < l) knownLargestSpace = l; } static bool appendObjectToFile(const char* key, const JsonDocument* content, uint32_t s, uint32_t contentLen = 0) { #ifdef WLED_DEBUG_FS DEBUGFS_PRINTLN(F("Append")); uint32_t s1 = millis(); #endif uint32_t pos = 0; if (!f) return false; if (f.size() < 3) { char init[10]; strcpy_P(init, PSTR("{\"0\":{}}")); f.print(init); } if (content->isNull()) { doCloseFile = true; return true; //nothing to append } //if there is enough empty space in file, insert there instead of appending if (!contentLen) contentLen = measureJson(*content); DEBUGFS_PRINTF("CLen %d\n", contentLen); if (bufferedFindSpace(contentLen + strlen(key) + 1)) { if (f.position() > 2) f.write(','); //add comma if not first object f.print(key); serializeJson(*content, f); DEBUGFS_PRINTF("Inserted, took %lu ms (total %lu)", millis() - s1, millis() - s); doCloseFile = true; return true; } //not enough space, append at end //permitted space for presets exceeded updateFSInfo(); if (f.size() + 9000 > (fsBytesTotal - fsBytesUsed)) { //make sure there is enough space to at least copy the file once errorFlag = ERR_FS_QUOTA; doCloseFile = true; return false; } //check if last character in file is '}' (typical) uint32_t eof = f.size() -1; f.seek(eof, SeekSet); if (f.read() == '}') pos = eof; if (pos == 0) //not found { DEBUGFS_PRINTLN(F("not }")); f.seek(0); while (bufferedFind("}",false)) //find last closing bracket in JSON if not last char { pos = f.position(); } if (pos > 0) pos--; } DEBUGFS_PRINT("pos "); DEBUGFS_PRINTLN(pos); if (pos > 2) { f.seek(pos, SeekSet); f.write(','); } else { //file content is not valid JSON object f.seek(0, SeekSet); f.print('{'); //start JSON } f.print(key); //Append object serializeJson(*content, f); f.write('}'); doCloseFile = true; DEBUGFS_PRINTF("Appended, took %lu ms (total %lu)", millis() - s1, millis() - s); return true; } bool writeObjectToFileUsingId(const char* file, uint16_t id, const JsonDocument* content) { char objKey[10]; sprintf(objKey, "\"%d\":", id); return writeObjectToFile(file, objKey, content); } bool writeObjectToFile(const char* file, const char* key, const JsonDocument* content) { uint32_t s = 0; //timing #ifdef WLED_DEBUG_FS DEBUGFS_PRINTF("Write to %s with key %s >>>\n", file, (key==nullptr)?"nullptr":key); serializeJson(*content, Serial); DEBUGFS_PRINTLN(); s = millis(); #endif if (doCloseFile) closeFile(); // This prevents the loss of file data that is still cached in the File object. size_t pos = 0; char fileName[129]; strncpy_P(fileName, file, 128); fileName[128] = 0; //use PROGMEM safe copy as FS.open() does not f = WLED_FS.open(fileName, WLED_FS.exists(fileName) ? "r+" : "w+"); if (!f) { DEBUGFS_PRINTLN(F("Failed to open!")); return false; } if (!bufferedFind(key)) //key does not exist in file { return appendObjectToFile(key, content, s); } //an object with this key already exists, replace or delete it pos = f.position(); //measure out end of old object bufferedFindObjectEnd(); size_t pos2 = f.position(); uint32_t oldLen = pos2 - pos; DEBUGFS_PRINTF("Old obj len %d\n", oldLen); //Three cases: //1. The new content is null, overwrite old obj with spaces //2. The new content is smaller than the old, overwrite and fill diff with spaces //3. The new content is larger than the old, but smaller than old + trailing spaces, overwrite with new //4. The new content is larger than old + trailing spaces, delete old and append size_t contentLen = 0; if (!content->isNull()) contentLen = measureJson(*content); if (contentLen && contentLen <= oldLen) { //replace and fill diff with spaces DEBUGFS_PRINTLN(F("replace")); f.seek(pos); serializeJson(*content, f); writeSpace(pos2 - f.position()); } else if (contentLen && bufferedFindSpace(contentLen - oldLen, false)) { //enough leading spaces to replace DEBUGFS_PRINTLN(F("replace (trailing)")); f.seek(pos); serializeJson(*content, f); } else { DEBUGFS_PRINTLN(F("delete")); pos -= strlen(key); if (pos > 3) pos--; //also delete leading comma if not first object f.seek(pos); writeSpace(pos2 - pos); if (contentLen) return appendObjectToFile(key, content, s, contentLen); } doCloseFile = true; DEBUGFS_PRINTF("Replaced/deleted, took %lu ms\n", millis() - s); return true; } bool readObjectFromFileUsingId(const char* file, uint16_t id, JsonDocument* dest, const JsonDocument* filter) { char objKey[10]; sprintf(objKey, "\"%d\":", id); return readObjectFromFile(file, objKey, dest, filter); } //if the key is a nullptr, deserialize entire object bool readObjectFromFile(const char* file, const char* key, JsonDocument* dest, const JsonDocument* filter) { if (doCloseFile) closeFile(); #ifdef WLED_DEBUG_FS DEBUGFS_PRINTF("Read from %s with key %s >>>\n", file, (key==nullptr)?"nullptr":key); uint32_t s = millis(); #endif char fileName[129]; strncpy_P(fileName, file, 128); fileName[128] = 0; //use PROGMEM safe copy as FS.open() does not f = WLED_FS.open(fileName, "r"); if (!f) return false; if (key != nullptr && !bufferedFind(key)) //key does not exist in file { f.close(); dest->clear(); DEBUGFS_PRINTLN(F("Obj not found.")); return false; } if (filter) deserializeJson(*dest, f, DeserializationOption::Filter(*filter)); else deserializeJson(*dest, f); f.close(); DEBUGFS_PRINTF("Read, took %lu ms\n", millis() - s); return true; } void updateFSInfo() { #ifdef ARDUINO_ARCH_ESP32 #if WLED_FS == LITTLEFS || ESP_IDF_VERSION_MAJOR >= 4 fsBytesTotal = WLED_FS.totalBytes(); fsBytesUsed = WLED_FS.usedBytes(); #else esp_spiffs_info(nullptr, &fsBytesTotal, &fsBytesUsed); #endif #else FSInfo fsi; WLED_FS.info(fsi); fsBytesUsed = fsi.usedBytes; fsBytesTotal = fsi.totalBytes; #endif } #ifdef ARDUINO_ARCH_ESP32 // caching presets in PSRAM may prevent occasional flashes seen when HomeAssitant polls WLED // original idea by @akaricchi (https://github.com/Akaricchi) // returns a pointer to the PSRAM buffer, updates size parameter static const uint8_t *getPresetCache(size_t &size) { static unsigned long presetsCachedTime = 0; static uint8_t *presetsCached = nullptr; static size_t presetsCachedSize = 0; static byte presetsCachedValidate = 0; //if (presetsModifiedTime != presetsCachedTime) DEBUG_PRINTLN(F("getPresetCache(): presetsModifiedTime changed.")); //if (presetsCachedValidate != cacheInvalidate) DEBUG_PRINTLN(F("getPresetCache(): cacheInvalidate changed.")); if ((presetsModifiedTime != presetsCachedTime) || (presetsCachedValidate != cacheInvalidate)) { if (presetsCached) { p_free(presetsCached); presetsCached = nullptr; presetsCachedSize = 0; } } if (!presetsCached) { File file = WLED_FS.open(FPSTR(getPresetsFileName()), "r"); if (file) { presetsCachedTime = presetsModifiedTime; presetsCachedValidate = cacheInvalidate; presetsCachedSize = 0; presetsCached = (uint8_t*)p_malloc(file.size() + 1); if (presetsCached) { presetsCachedSize = file.size(); file.read(presetsCached, presetsCachedSize); presetsCached[presetsCachedSize] = 0; file.close(); } } } size = presetsCachedSize; return presetsCached; } #endif bool handleFileRead(AsyncWebServerRequest* request, String path){ DEBUGFS_PRINT(F("WS FileRead: ")); DEBUGFS_PRINTLN(path); if(path.endsWith("/")) path += "index.htm"; if(path.indexOf(F("sec")) > -1) return false; #ifdef BOARD_HAS_PSRAM if (path.endsWith(FPSTR(getPresetsFileName()))) { size_t psize; const uint8_t *presets = getPresetCache(psize); if (presets) { AsyncWebServerResponse *response = request->beginResponse_P(200, FPSTR(CONTENT_TYPE_JSON), presets, psize); request->send(response); return true; } } #endif if(WLED_FS.exists(path) || WLED_FS.exists(path + ".gz")) { request->send(request->beginResponse(WLED_FS, path, {}, request->hasArg(F("download")), {})); return true; } return false; } // copy a file, delete destination file if incomplete to prevent corrupted files bool copyFile(const char* src_path, const char* dst_path) { DEBUG_PRINTF("copyFile from %s to %s\n", src_path, dst_path); if(!WLED_FS.exists(src_path)) { DEBUG_PRINTLN(F("file not found")); return false; } bool success = true; // is set to false on error File src = WLED_FS.open(src_path, "r"); File dst = WLED_FS.open(dst_path, "w"); if (src && dst) { uint8_t buf[128]; // copy file in 128-byte blocks while (src.available() > 0) { size_t bytesRead = src.read(buf, sizeof(buf)); if (bytesRead == 0) { success = false; break; // error, no data read } size_t bytesWritten = dst.write(buf, bytesRead); if (bytesWritten != bytesRead) { success = false; break; // error, not all data written } } } else { success = false; // error, could not open files } if(src) src.close(); if(dst) dst.close(); if (!success) { DEBUG_PRINTLN(F("copy failed")); WLED_FS.remove(dst_path); // delete incomplete file } return success; } // compare two files, return true if identical bool compareFiles(const char* path1, const char* path2) { DEBUG_PRINTF("compareFile %s and %s\n", path1, path2); if (!WLED_FS.exists(path1) || !WLED_FS.exists(path2)) { DEBUG_PRINTLN(F("file not found")); return false; } bool identical = true; // set to false on mismatch File f1 = WLED_FS.open(path1, "r"); File f2 = WLED_FS.open(path2, "r"); if (f1 && f2) { uint8_t buf1[128], buf2[128]; while (f1.available() > 0 || f2.available() > 0) { size_t len1 = f1.read(buf1, sizeof(buf1)); size_t len2 = f2.read(buf2, sizeof(buf2)); if (len1 != len2) { identical = false; break; // files differ in size or read failed } if (memcmp(buf1, buf2, len1) != 0) { identical = false; break; // files differ in content } } } else { identical = false; // error opening files } if (f1) f1.close(); if (f2) f2.close(); return identical; } static const char s_backup_fmt[] PROGMEM = "/bkp.%s"; bool backupFile(const char* filename) { DEBUG_PRINTF("backup %s \n", filename); if (!validateJsonFile(filename)) { DEBUG_PRINTLN(F("broken file")); return false; } char backupname[32]; snprintf_P(backupname, sizeof(backupname), s_backup_fmt, filename + 1); // skip leading '/' in filename if (copyFile(filename, backupname)) { DEBUG_PRINTLN(F("backup ok")); return true; } DEBUG_PRINTLN(F("backup failed")); return false; } bool restoreFile(const char* filename) { DEBUG_PRINTF("restore %s \n", filename); char backupname[32]; snprintf_P(backupname, sizeof(backupname), s_backup_fmt, filename + 1); // skip leading '/' in filename if (!WLED_FS.exists(backupname)) { DEBUG_PRINTLN(F("no backup found")); return false; } if (!validateJsonFile(backupname)) { DEBUG_PRINTLN(F("broken backup")); return false; } if (copyFile(backupname, filename)) { DEBUG_PRINTLN(F("restore ok")); return true; } DEBUG_PRINTLN(F("restore failed")); return false; } bool checkBackupExists(const char* filename) { char backupname[32]; snprintf_P(backupname, sizeof(backupname), s_backup_fmt, filename + 1); // skip leading '/' in filename return WLED_FS.exists(backupname); } bool validateJsonFile(const char* filename) { if (!WLED_FS.exists(filename)) return false; File file = WLED_FS.open(filename, "r"); if (!file) return false; StaticJsonDocument<0> doc, filter; // https://arduinojson.org/v6/how-to/validate-json/ bool result = deserializeJson(doc, file, DeserializationOption::Filter(filter)) == DeserializationError::Ok; file.close(); if (!result) { DEBUG_PRINTF_P(PSTR("Invalid JSON file %s\n"), filename); } else { DEBUG_PRINTF_P(PSTR("Valid JSON file %s\n"), filename); } return result; } // print contents of all files in root dir to Serial except wsec files void dumpFilesToSerial() { File rootdir = WLED_FS.open("/", "r"); File rootfile = rootdir.openNextFile(); while (rootfile) { size_t len = strlen(rootfile.name()); // skip files starting with "wsec" and dont end in .json if (strncmp(rootfile.name(), "wsec", 4) != 0 && len >= 6 && strcmp(rootfile.name() + len - 5, ".json") == 0) { Serial.println(rootfile.name()); while (rootfile.available()) { Serial.write(rootfile.read()); } Serial.println(); Serial.println(); } rootfile.close(); rootfile = rootdir.openNextFile(); } } ================================================ FILE: wled00/hue.cpp ================================================ #include "wled.h" /* * Sync to Philips hue lights */ #ifndef WLED_DISABLE_HUESYNC void handleHue() { if (hueReceived) { colorUpdated(CALL_MODE_HUE); hueReceived = false; if (hueStoreAllowed && hueNewKey) { serializeConfigSec(); //save api key hueStoreAllowed = false; hueNewKey = false; } } if (!WLED_CONNECTED || hueClient == nullptr || millis() - hueLastRequestSent < huePollIntervalMs) return; hueLastRequestSent = millis(); if (huePollingEnabled) { reconnectHue(); } else { hueClient->close(); if (hueError == HUE_ERROR_ACTIVE) hueError = HUE_ERROR_INACTIVE; } } void reconnectHue() { if (!WLED_CONNECTED || !huePollingEnabled) return; DEBUG_PRINTLN(F("Hue reconnect")); if (hueClient == nullptr) { hueClient = new AsyncClient(); hueClient->onConnect(&onHueConnect, hueClient); hueClient->onData(&onHueData, hueClient); hueClient->onError(&onHueError, hueClient); hueAuthRequired = (strlen(hueApiKey)<20); } hueClient->connect(hueIP, 80); } void onHueError(void* arg, AsyncClient* client, int8_t error) { DEBUG_PRINTLN(F("Hue err")); hueError = HUE_ERROR_TIMEOUT; } void onHueConnect(void* arg, AsyncClient* client) { DEBUG_PRINTLN(F("Hue connect")); sendHuePoll(); } void sendHuePoll() { if (hueClient == nullptr || !hueClient->connected()) return; String req = ""; if (hueAuthRequired) { req += F("POST /api HTTP/1.1\r\nHost: "); req += hueIP.toString(); req += F("\r\nContent-Length: 25\r\n\r\n{\"devicetype\":\"wled#esp\"}"); } else { req += F("GET /api/"); req += hueApiKey; req += F("/lights/"); req += String(huePollLightId); req += F(" HTTP/1.1\r\nHost: "); req += hueIP.toString(); req += "\r\n\r\n"; } hueClient->add(req.c_str(), req.length()); hueClient->send(); hueLastRequestSent = millis(); } void onHueData(void* arg, AsyncClient* client, void *data, size_t len) { if (!len) return; char* str = (char*)data; DEBUG_PRINTLN(hueApiKey); DEBUG_PRINTLN(str); //only get response body str = strstr(str,"\r\n\r\n"); if (str == nullptr) return; str += 4; StaticJsonDocument<1024> root; if (str[0] == '[') //is JSON array { auto error = deserializeJson(root, str); if (error) { hueError = HUE_ERROR_JSON_PARSING; return; } int hueErrorCode = root[0][F("error")]["type"]; if (hueErrorCode)//hue bridge returned error { hueError = hueErrorCode; switch (hueErrorCode) { case 1: hueAuthRequired = true; break; //Unauthorized user case 3: huePollingEnabled = false; break; //Invalid light ID case 101: hueAuthRequired = true; break; //link button not presset } return; } if (hueAuthRequired) { const char* apikey = root[0][F("success")][F("username")]; if (apikey != nullptr && strlen(apikey) < sizeof(hueApiKey)) { strlcpy(hueApiKey, apikey, sizeof(hueApiKey)); hueAuthRequired = false; hueNewKey = true; } } return; } //else, assume it is JSON object, look for state and only parse that str = strstr(str,"state"); if (str == nullptr) return; str = strstr(str,"{"); auto error = deserializeJson(root, str); if (error) { hueError = HUE_ERROR_JSON_PARSING; return; } float hueX=0, hueY=0; uint16_t hueHue=0, hueCt=0; byte hueBri=0, hueSat=0, hueColormode=0; if (root["on"]) { if (root.containsKey("bri")) //Dimmable device { hueBri = root["bri"]; hueBri++; const char* cm =root[F("colormode")]; if (cm != nullptr) //Color device { if (strstr(cm,("ct")) != nullptr) //ct mode { hueCt = root["ct"]; hueColormode = 3; } else if (strstr(cm,"xy") != nullptr) //xy mode { hueX = root["xy"][0]; // 0.5051 hueY = root["xy"][1]; // 0.4151 hueColormode = 1; } else //hs mode { hueHue = root["hue"]; hueSat = root[F("sat")]; hueColormode = 2; } } } else //On/Off device { hueBri = briLast; } } else { hueBri = 0; } hueError = HUE_ERROR_ACTIVE; //apply vals if (hueBri != hueBriLast) { if (hueApplyOnOff) { if (hueBri==0) {bri = 0;} else if (bri==0 && hueBri>0) bri = briLast; } if (hueApplyBri) { if (hueBri>0) bri = hueBri; } hueBriLast = hueBri; } if (hueApplyColor) { switch(hueColormode) { case 1: if (hueX != hueXLast || hueY != hueYLast) colorXYtoRGB(hueX,hueY,colPri); hueXLast = hueX; hueYLast = hueY; break; case 2: if (hueHue != hueHueLast || hueSat != hueSatLast) colorHStoRGB(hueHue,hueSat,colPri); hueHueLast = hueHue; hueSatLast = hueSat; break; case 3: if (hueCt != hueCtLast) colorCTtoRGB(hueCt,colPri); hueCtLast = hueCt; break; } } hueReceived = true; } #else void handleHue(){} void reconnectHue(){} #endif ================================================ FILE: wled00/image_loader.cpp ================================================ #include "wled.h" #ifdef WLED_ENABLE_GIF #include "GifDecoder.h" /* * Functions to render images from filesystem to segments, used by the "Image" effect */ static File file; static char lastFilename[WLED_MAX_SEGNAME_LEN+2] = "/"; // enough space for "/" + seg.name + '\0' static GifDecoder<320,320,12,true> decoder; // this creates the basic object; parameter lzwMaxBits is not used; decoder.alloc() always allocated "everything else" = 24Kb static bool gifDecodeFailed = false; static unsigned long lastFrameDisplayTime = 0, currentFrameDelay = 0; bool fileSeekCallback(unsigned long position) { return file.seek(position); } unsigned long filePositionCallback(void) { return file.position(); } int fileReadCallback(void) { return file.read(); } int fileReadBlockCallback(void * buffer, int numberOfBytes) { return file.read((uint8_t*)buffer, numberOfBytes); } int fileSizeCallback(void) { return file.size(); } bool openGif(const char *filename) { // side-effect: updates "file" file = WLED_FS.open(filename, "r"); DEBUG_PRINTF_P(PSTR("opening GIF file %s\n"), filename); if (!file) return false; return true; } static Segment* activeSeg; static uint16_t gifWidth, gifHeight; static int lastCoordinate; // last coordinate (x+y) that was set, used to reduce redundant pixel writes static uint16_t perPixelX, perPixelY; // scaling factors when upscaling void screenClearCallback(void) { activeSeg->fill(0); } // this callback runs when the decoder has finished painting all pixels void updateScreenCallback(void) { // perfect time for adding blur if (activeSeg->intensity > 1) { uint8_t blurAmount = activeSeg->intensity; if ((blurAmount < 24) && (activeSeg->is2D())) activeSeg->blurRows(activeSeg->intensity); // some blur - fast else activeSeg->blur(blurAmount); // more blur - slower } lastCoordinate = -1; // invalidate last position } // note: GifDecoder drawing is done top right to bottom left, line by line // callbacks to draw a pixel at (x,y) without scaling: used if GIF size matches (virtual)segment size (faster) works for 1D and 2D segments void drawPixelCallbackNoScale(int16_t x, int16_t y, uint8_t red, uint8_t green, uint8_t blue) { activeSeg->setPixelColor(y * gifWidth + x, red, green, blue); } void drawPixelCallback1D(int16_t x, int16_t y, uint8_t red, uint8_t green, uint8_t blue) { // 1D strip: load pixel-by-pixel left to right, top to bottom (0/0 = top-left in gifs) int totalImgPix = (int)gifWidth * gifHeight; int start = ((int)y * gifWidth + (int)x) * activeSeg->vLength() / totalImgPix; // simple nearest-neighbor scaling if (start == lastCoordinate) return; // skip setting same coordinate again lastCoordinate = start; for (int i = 0; i < perPixelX; i++) { activeSeg->setPixelColor(start + i, red, green, blue); } } void drawPixelCallback2D(int16_t x, int16_t y, uint8_t red, uint8_t green, uint8_t blue) { // simple nearest-neighbor scaling int outY = (int)y * activeSeg->vHeight() / gifHeight; int outX = (int)x * activeSeg->vWidth() / gifWidth; // Pack coordinates uniquely: outY into upper 16 bits, outX into lower 16 bits if (((outY << 16) | outX) == lastCoordinate) return; // skip setting same coordinate again lastCoordinate = (outY << 16) | outX; // since input is a "scanline" this is sufficient to identify a "unique" coordinate // set multiple pixels if upscaling for (int i = 0; i < perPixelX; i++) { for (int j = 0; j < perPixelY; j++) { activeSeg->setPixelColorXY(outX + i, outY + j, red, green, blue); } } } #define IMAGE_ERROR_NONE 0 #define IMAGE_ERROR_NO_NAME 1 #define IMAGE_ERROR_SEG_LIMIT 2 #define IMAGE_ERROR_UNSUPPORTED_FORMAT 3 #define IMAGE_ERROR_FILE_MISSING 4 #define IMAGE_ERROR_DECODER_ALLOC 5 #define IMAGE_ERROR_GIF_DECODE 6 #define IMAGE_ERROR_FRAME_DECODE 7 #define IMAGE_ERROR_WAITING 254 #define IMAGE_ERROR_PREV 255 // renders an image (.gif only; .bmp and .fseq to be added soon) from FS to a segment byte renderImageToSegment(Segment &seg) { if (!seg.name) return IMAGE_ERROR_NO_NAME; // disable during effect transition, causes flickering, multiple allocations and depending on image, part of old FX remaining //if (seg.mode != seg.currentMode()) return IMAGE_ERROR_WAITING; if (activeSeg && activeSeg != &seg) { // only one segment at a time if (!seg.isActive()) return IMAGE_ERROR_SEG_LIMIT; // sanity check: calling segment must be active if (gifDecodeFailed || !activeSeg->isActive()) // decoder failed, or last segment became inactive endImagePlayback(activeSeg); // => allow takeover but clean up first else return IMAGE_ERROR_SEG_LIMIT; } activeSeg = &seg; if (strncmp(lastFilename +1, seg.name, WLED_MAX_SEGNAME_LEN) != 0) { // segment name changed, load new image strcpy(lastFilename, "/"); // filename always starts with '/' strncpy(lastFilename +1, seg.name, WLED_MAX_SEGNAME_LEN); lastFilename[WLED_MAX_SEGNAME_LEN+1] ='\0'; // ensure proper string termination when segment name was truncated gifDecodeFailed = false; size_t fnameLen = strlen(lastFilename); if ((fnameLen < 4) || strcmp(lastFilename + fnameLen - 4, ".gif") != 0) { // empty segment name, name too short, or name not ending in .gif gifDecodeFailed = true; DEBUG_PRINTF_P(PSTR("GIF decoder unsupported file: %s\n"), lastFilename); return IMAGE_ERROR_UNSUPPORTED_FORMAT; } if (file) file.close(); if (!openGif(lastFilename)) { gifDecodeFailed = true; DEBUG_PRINTF_P(PSTR("GIF file not found: %s\n"), lastFilename); return IMAGE_ERROR_FILE_MISSING; } lastCoordinate = -1; decoder.setScreenClearCallback(screenClearCallback); decoder.setUpdateScreenCallback(updateScreenCallback); decoder.setDrawPixelCallback(drawPixelCallbackNoScale); // default: use "fast path" callback without scaling decoder.setFileSeekCallback(fileSeekCallback); decoder.setFilePositionCallback(filePositionCallback); decoder.setFileReadCallback(fileReadCallback); decoder.setFileReadBlockCallback(fileReadBlockCallback); decoder.setFileSizeCallback(fileSizeCallback); #if __cpp_exceptions // use exception handler if we can (some targets don't support exceptions) try { #endif decoder.alloc(); // this function may throw out-of memory and cause a crash #if __cpp_exceptions } catch (...) { // if we arrive here, the decoder has thrown an OOM exception gifDecodeFailed = true; errorFlag = ERR_NORAM_PX; DEBUG_PRINTLN(F("\nGIF decoder out of memory. Please try a smaller image file.\n")); return IMAGE_ERROR_DECODER_ALLOC; // decoder cleanup (hi @coderabbitai): No additonal cleanup necessary - decoder.alloc() ultimately uses "new AnimatedGIF". // If new throws, no pointer is assigned, previous decoder state (if any) has already been deleted inside alloc(), so calling decoder.dealloc() here is unnecessary. } #endif DEBUG_PRINTLN(F("Starting decoding")); int decoderError = decoder.startDecoding(); if(decoderError < 0) { DEBUG_PRINTF_P(PSTR("GIF Decoding error %d in startDecoding().\n"), decoderError); errorFlag = ERR_NORAM_PX; gifDecodeFailed = true; return IMAGE_ERROR_GIF_DECODE; } DEBUG_PRINTLN(F("Decoding started")); // after startDecoding, we can get GIF size, update static variables and callbacks decoder.getSize(&gifWidth, &gifHeight); if (gifWidth == 0 || gifHeight == 0) { // bad gif size: prevent division by zero gifDecodeFailed = true; DEBUG_PRINTF_P(PSTR("Invalid GIF dimensions: %dx%d\n"), gifWidth, gifHeight); return IMAGE_ERROR_GIF_DECODE; } if (activeSeg->is2D()) { perPixelX = (activeSeg->vWidth() + gifWidth -1) / gifWidth; perPixelY = (activeSeg->vHeight() + gifHeight-1) / gifHeight; if (activeSeg->vWidth() != gifWidth || activeSeg->vHeight() != gifHeight) { decoder.setDrawPixelCallback(drawPixelCallback2D); // use 2D callback with scaling //DEBUG_PRINTLN(F("scaling image")); } } else { int totalImgPix = (int)gifWidth * gifHeight; if (totalImgPix - activeSeg->vLength() == 1) totalImgPix--; // handle off-by-one: skip last pixel instead of first (gifs constructed from 1D input pad last pixel if length is odd) perPixelX = (activeSeg->vLength() + totalImgPix-1) / totalImgPix; if (totalImgPix != activeSeg->vLength()) { decoder.setDrawPixelCallback(drawPixelCallback1D); // use 1D callback with scaling //DEBUG_PRINTLN(F("scaling image")); } } } if (gifDecodeFailed) return IMAGE_ERROR_PREV; if (!file) { gifDecodeFailed = true; return IMAGE_ERROR_FILE_MISSING; } //if (!decoder) { gifDecodeFailed = true; return IMAGE_ERROR_DECODER_ALLOC; } // speed 0 = half speed, 128 = normal, 255 = full FX FPS // TODO: 0 = 4x slow, 64 = 2x slow, 128 = normal, 192 = 2x fast, 255 = 4x fast uint32_t wait = currentFrameDelay * 2 - seg.speed * currentFrameDelay / 128; // TODO consider handling this on FX level with a different frametime, but that would cause slow gifs to speed up during transitions if (millis() - lastFrameDisplayTime < wait) return IMAGE_ERROR_WAITING; int result = decoder.decodeFrame(false); if (result < 0) { DEBUG_PRINTF_P(PSTR("GIF Decoding error %d in decodeFrame().\n"), result); gifDecodeFailed = true; return IMAGE_ERROR_FRAME_DECODE; } currentFrameDelay = decoder.getFrameDelay_ms(); unsigned long tooSlowBy = (millis() - lastFrameDisplayTime) - wait; // if last frame was longer than intended, compensate currentFrameDelay = tooSlowBy > currentFrameDelay ? 0 : currentFrameDelay - tooSlowBy; lastFrameDisplayTime = millis(); return IMAGE_ERROR_NONE; } void endImagePlayback(Segment *seg) { DEBUG_PRINTLN(F("Image playback end called")); if (!activeSeg || activeSeg != seg) return; if (file) file.close(); decoder.dealloc(); gifDecodeFailed = false; activeSeg = nullptr; strcpy(lastFilename, "/"); // reset filename gifWidth = gifHeight = 0; // reset dimensions DEBUG_PRINTLN(F("Image playback ended")); } #endif ================================================ FILE: wled00/improv.cpp ================================================ #include "wled.h" #ifdef WLED_DEBUG_IMPROV #define DIMPROV_PRINT(x) Serial.print(x) #define DIMPROV_PRINTLN(x) Serial.println(x) #define DIMPROV_PRINTF(x...) Serial.printf(x) #else #define DIMPROV_PRINT(x) #define DIMPROV_PRINTLN(x) #define DIMPROV_PRINTF(x...) #endif #if defined(CONFIG_IDF_TARGET_ESP32S2) || defined(CONFIG_IDF_TARGET_ESP32C3) || defined(CONFIG_IDF_TARGET_ESP32S3) #undef WLED_DISABLE_IMPROV_WIFISCAN #define WLED_DISABLE_IMPROV_WIFISCAN #endif #define IMPROV_VERSION 1 // forward declarations static void parseWiFiCommand(char* rpcData); enum ImprovPacketType { Current_State = 0x01, Error_State = 0x02, RPC_Command = 0x03, RPC_Response = 0x04 }; enum ImprovPacketByte { Version = 6, PacketType = 7, Length = 8, RPC_CommandType = 9 }; #ifndef WLED_DISABLE_IMPROV_WIFISCAN static bool improvWifiScanRunning = false; #endif //blocking function to parse an Improv Serial packet void handleImprovPacket() { uint8_t header[6] = {'I','M','P','R','O','V'}; bool timeout = false; unsigned waitTime = 25; unsigned packetByte = 0; unsigned packetLen = 9; unsigned checksum = 0; unsigned rpcCommandType = 0; char rpcData[128]; rpcData[0] = 0; while (!timeout) { if (Serial.available() < 1) { delay(1); waitTime--; if (!waitTime) timeout = true; continue; } byte next = Serial.read(); DIMPROV_PRINT("Received improv byte: "); DIMPROV_PRINTF("%x\r\n",next); switch (packetByte) { case ImprovPacketByte::Version: { if (next != IMPROV_VERSION) { DIMPROV_PRINTLN(F("Invalid version")); return; } break; } case ImprovPacketByte::PacketType: { if (next != ImprovPacketType::RPC_Command) { DIMPROV_PRINTF("Non RPC-command improv packet type %i\n",next); return; } if (!improvActive) improvActive = 1; break; } case ImprovPacketByte::Length: packetLen = 9 + next; break; case ImprovPacketByte::RPC_CommandType: rpcCommandType = next; break; default: { if (packetByte >= packetLen) { //end of packet, check checksum match if (checksum != next) { DIMPROV_PRINTF("Got RPC checksum %i, expected %i",next,checksum); sendImprovStateResponse(0x01, true); return; } switch (rpcCommandType) { case ImprovRPCType::Command_Wifi: parseWiFiCommand(rpcData); break; case ImprovRPCType::Request_State: { unsigned improvState = 0x02; //authorized if (WLED_WIFI_CONFIGURED) improvState = 0x03; //provisioning if (Network.isConnected()) improvState = 0x04; //provisioned sendImprovStateResponse(improvState, false); if (improvState == 0x04) sendImprovIPRPCResult(ImprovRPCType::Request_State); break; } case ImprovRPCType::Request_Info: sendImprovInfoResponse(); break; #ifndef WLED_DISABLE_IMPROV_WIFISCAN case ImprovRPCType::Request_Scan: startImprovWifiScan(); break; #endif default: { DIMPROV_PRINTF("Unknown RPC command %i\n",next); sendImprovStateResponse(0x02, true); } } return; } if (packetByte < 6) { //check header if (next != header[packetByte]) { DIMPROV_PRINTLN(F("Invalid improv header")); return; } } else if (packetByte > 9) { //RPC data rpcData[packetByte - 10] = next; if (packetByte > 137) return; //prevent buffer overflow } } } checksum += next; checksum &= 0xFF; packetByte++; } } void sendImprovStateResponse(uint8_t state, bool error) { if (!error && improvError > 0 && improvError < 3) sendImprovStateResponse(0x00, true); if (error) improvError = state; char out[11] = {'I','M','P','R','O','V'}; out[6] = IMPROV_VERSION; out[7] = error? ImprovPacketType::Error_State : ImprovPacketType::Current_State; out[8] = 1; out[9] = state; unsigned checksum = 0; for (unsigned i = 0; i < 10; i++) checksum += out[i]; out[10] = checksum; Serial.write((uint8_t*)out, 11); Serial.write('\n'); } // used by sendImprovIPRPCResult(), sendImprovInfoResponse(), and handleImprovWifiScan() void sendImprovRPCResult(ImprovRPCType type, uint8_t n_strings, const char **strings) { if (improvError > 0 && improvError < 3) sendImprovStateResponse(0x00, true); unsigned packetLen = 12; char out[256] = {'I','M','P','R','O','V'}; out[6] = IMPROV_VERSION; out[7] = ImprovPacketType::RPC_Response; //out[8] = 2; //Length (set below) out[9] = type; //out[10] = 0; //Data len (set below) unsigned pos = 11; for (unsigned s = 0; s < n_strings; s++) { size_t len = strlen(strings[s]); if (pos + len > 254) continue; // simple buffer overflow guard out[pos++] = len; strcpy(out + pos, strings[s]); pos += len; } packetLen = pos +1; out[8] = pos -9; // Length of packet (excluding first 9 header bytes and final checksum byte) out[10] = pos -11; // Data len unsigned checksum = 0; for (unsigned i = 0; i < packetLen -1; i++) checksum += out[i]; out[packetLen -1] = checksum; Serial.write((uint8_t*)out, packetLen); Serial.write('\n'); DIMPROV_PRINT("RPC result checksum"); DIMPROV_PRINTLN(checksum); } void sendImprovIPRPCResult(ImprovRPCType type) { if (Network.isConnected()) { char urlStr[64]; IPAddress localIP = Network.localIP(); unsigned len = sprintf(urlStr, "http://%d.%d.%d.%d", localIP[0], localIP[1], localIP[2], localIP[3]); if (len > 24) return; //sprintf fail? const char *str[1] = {urlStr}; sendImprovRPCResult(type, 1, str); } else { sendImprovRPCResult(type, 0); } improvActive = 1; //no longer provisioning } void sendImprovInfoResponse() { char bString[32]; #ifdef ESP8266 strcpy(bString, "esp8266"); #else // ESP32 strncpy(bString, ESP.getChipModel(), 31); #if CONFIG_IDF_TARGET_ESP32 bString[5] = '\0'; // disregard chip revision for classic ESP32 #else bString[31] = '\0'; // just in case #endif strlwr(bString); #endif //Use serverDescription if it has been changed from the default "WLED", else mDNS name bool useMdnsName = (strcmp(serverDescription, "WLED") == 0 && strlen(cmDNS) > 0); char vString[32]; sprintf_P(vString, PSTR("%s/%i"), versionString, VERSION); const char *str[4] = {"WLED", vString, bString, useMdnsName ? cmDNS : serverDescription}; sendImprovRPCResult(ImprovRPCType::Request_Info, 4, str); } #ifndef WLED_DISABLE_IMPROV_WIFISCAN void startImprovWifiScan() { if (improvWifiScanRunning) return; WiFi.scanNetworks(true); improvWifiScanRunning = true; } void handleImprovWifiScan() { if (!improvWifiScanRunning) return; int16_t status = WiFi.scanComplete(); if (status == WIFI_SCAN_RUNNING) return; // here scan completed or failed (-2) improvWifiScanRunning = false; for (int i = 0; i < status; i++) { char rssiStr[8]; sprintf(rssiStr, "%d", WiFi.RSSI(i)); #ifdef ESP8266 bool isOpen = WiFi.encryptionType(i) == ENC_TYPE_NONE; #else bool isOpen = WiFi.encryptionType(i) == WIFI_AUTH_OPEN; #endif char ssidStr[33]; strcpy(ssidStr, WiFi.SSID(i).c_str()); const char *str[3] = {ssidStr, rssiStr, isOpen ? "NO":"YES"}; sendImprovRPCResult(ImprovRPCType::Request_Scan, 3, str); } sendImprovRPCResult(ImprovRPCType::Request_Scan, 0); WiFi.scanDelete(); } #else void startImprovWifiScan() {} void handleImprovWifiScan() {} #endif static void parseWiFiCommand(char* rpcData) { unsigned len = rpcData[0]; if (!len || len > 126) return; unsigned ssidLen = rpcData[1]; if (ssidLen > len -1 || ssidLen > 32) return; memset(multiWiFi[0].clientSSID, 0, 32); memcpy(multiWiFi[0].clientSSID, rpcData+2, ssidLen); memset(multiWiFi[0].clientPass, 0, 64); if (len > ssidLen +1) { unsigned passLen = rpcData[2+ssidLen]; memset(multiWiFi[0].clientPass, 0, 64); memcpy(multiWiFi[0].clientPass, rpcData+3+ssidLen, passLen); } sendImprovStateResponse(0x03); //provisioning improvActive = 2; forceReconnect = true; serializeConfigToFS(); } ================================================ FILE: wled00/ir.cpp ================================================ #include "wled.h" #ifndef WLED_DISABLE_INFRARED #include "ir_codes.h" /* * Infrared sensor support for several generic RGB remotes and custom JSON remote */ static IRrecv* irrecv; static decode_results results; static unsigned long irCheckedTime = 0; static uint32_t lastValidCode = 0; static byte lastRepeatableAction = ACTION_NONE; static uint8_t lastRepeatableValue = 0; static uint16_t irTimesRepeated = 0; static uint8_t lastIR6ColourIdx = 0; // brightnessSteps: a static array of brightness levels following a geometric // progression. Can be generated from the following Python, adjusting the // arbitrary 4.5 value to taste: // // def values(level): // while level >= 5: // yield int(level) // level -= level / 4.5 // result = [v for v in reversed(list(values(255)))] // print("%d values: %s" % (len(result), result)) // // It would be hard to maintain repeatable steps if calculating this on the fly. const uint8_t brightnessSteps[] = { 5, 7, 9, 12, 16, 20, 26, 34, 43, 56, 72, 93, 119, 154, 198, 255 }; const size_t numBrightnessSteps = sizeof(brightnessSteps) / sizeof(uint8_t); // increment `bri` to the next `brightnessSteps` value static void incBrightness() { // dumb incremental search is efficient enough for so few items for (unsigned index = 0; index < numBrightnessSteps; ++index) { if (brightnessSteps[index] > bri) { bri = brightnessSteps[index]; lastRepeatableAction = ACTION_BRIGHT_UP; break; } } } // decrement `bri` to the next `brightnessSteps` value static void decBrightness() { // dumb incremental search is efficient enough for so few items for (int index = numBrightnessSteps - 1; index >= 0; --index) { if (brightnessSteps[index] < bri) { bri = brightnessSteps[index]; lastRepeatableAction = ACTION_BRIGHT_DOWN; break; } } } static void presetFallback(uint8_t presetID, uint8_t effectID, uint8_t paletteID) { applyPresetWithFallback(presetID, CALL_MODE_BUTTON_PRESET, effectID, paletteID); } static byte relativeChange(byte property, int8_t amount, byte lowerBoundary = 0, byte higherBoundary = 0xFF) { int16_t new_val = (int16_t) property + amount; if (lowerBoundary >= higherBoundary) return property; if (new_val > higherBoundary) new_val = higherBoundary; if (new_val < lowerBoundary) new_val = lowerBoundary; return (byte)constrain(new_val, 0, 255); } static void changeEffect(uint8_t fx) { if (irApplyToAllSelected) { for (unsigned i = 0; i < strip.getSegmentsNum(); i++) { Segment& seg = strip.getSegment(i); if (!seg.isActive() || !seg.isSelected()) continue; seg.setMode(fx); } setValuesFromFirstSelectedSeg(); } else { strip.getSegment(strip.getMainSegmentId()).setMode(fx); setValuesFromMainSeg(); } stateChanged = true; } static void changePalette(uint8_t pal) { if (irApplyToAllSelected) { for (unsigned i = 0; i < strip.getSegmentsNum(); i++) { Segment& seg = strip.getSegment(i); if (!seg.isActive() || !seg.isSelected()) continue; seg.setPalette(pal); } setValuesFromFirstSelectedSeg(); } else { strip.getMainSegment().palette = pal; setValuesFromMainSeg(); } stateChanged = true; } static void changeEffectSpeed(int8_t amount) { if (effectCurrent != 0) { int16_t new_val = (int16_t) effectSpeed + amount; effectSpeed = (byte)constrain(new_val,0,255); if (irApplyToAllSelected) { for (unsigned i = 0; i < strip.getSegmentsNum(); i++) { Segment& seg = strip.getSegment(i); if (!seg.isActive() || !seg.isSelected()) continue; seg.speed = effectSpeed; } setValuesFromFirstSelectedSeg(); } else { strip.getMainSegment().speed = effectSpeed; setValuesFromMainSeg(); } } else { // if Effect == "solid Color", change the hue of the primary color Segment& sseg = irApplyToAllSelected ? strip.getFirstSelectedSeg() : strip.getMainSegment(); CRGB fastled_col = CRGB(sseg.colors[0]); CHSV prim_hsv = rgb2hsv(fastled_col); int16_t new_val = (int16_t)prim_hsv.h + amount; if (new_val > 255) new_val -= 255; // roll-over if bigger than 255 if (new_val < 0) new_val += 255; // roll-over if smaller than 0 prim_hsv.h = (byte)new_val; hsv2rgb_rainbow(prim_hsv, fastled_col); if (irApplyToAllSelected) { for (unsigned i = 0; i < strip.getSegmentsNum(); i++) { Segment& seg = strip.getSegment(i); if (!seg.isActive() || !seg.isSelected()) continue; seg.colors[0] = RGBW32(fastled_col.red, fastled_col.green, fastled_col.blue, W(sseg.colors[0])); } setValuesFromFirstSelectedSeg(); } else { strip.getMainSegment().colors[0] = RGBW32(fastled_col.red, fastled_col.green, fastled_col.blue, W(sseg.colors[0])); setValuesFromMainSeg(); } } stateChanged = true; if(amount > 0) lastRepeatableAction = ACTION_SPEED_UP; if(amount < 0) lastRepeatableAction = ACTION_SPEED_DOWN; lastRepeatableValue = amount; } static void changeEffectIntensity(int8_t amount) { if (effectCurrent != 0) { int16_t new_val = (int16_t) effectIntensity + amount; effectIntensity = (byte)constrain(new_val,0,255); if (irApplyToAllSelected) { for (unsigned i = 0; i < strip.getSegmentsNum(); i++) { Segment& seg = strip.getSegment(i); if (!seg.isActive() || !seg.isSelected()) continue; seg.intensity = effectIntensity; } setValuesFromFirstSelectedSeg(); } else { strip.getMainSegment().speed = effectIntensity; setValuesFromMainSeg(); } } else { // if Effect == "solid Color", change the saturation of the primary color Segment& sseg = irApplyToAllSelected ? strip.getFirstSelectedSeg() : strip.getMainSegment(); CRGB fastled_col = CRGB(sseg.colors[0]); CHSV prim_hsv = rgb2hsv(fastled_col); int16_t new_val = (int16_t) prim_hsv.s + amount; prim_hsv.s = (byte)constrain(new_val,0,255); // constrain to 0-255 hsv2rgb_rainbow(prim_hsv, fastled_col); if (irApplyToAllSelected) { for (unsigned i = 0; i < strip.getSegmentsNum(); i++) { Segment& seg = strip.getSegment(i); if (!seg.isActive() || !seg.isSelected()) continue; seg.colors[0] = RGBW32(fastled_col.red, fastled_col.green, fastled_col.blue, W(sseg.colors[0])); } setValuesFromFirstSelectedSeg(); } else { strip.getMainSegment().colors[0] = RGBW32(fastled_col.red, fastled_col.green, fastled_col.blue, W(sseg.colors[0])); setValuesFromMainSeg(); } } stateChanged = true; if(amount > 0) lastRepeatableAction = ACTION_INTENSITY_UP; if(amount < 0) lastRepeatableAction = ACTION_INTENSITY_DOWN; lastRepeatableValue = amount; } static void changeColor(uint32_t c, int16_t cct=-1) { if (irApplyToAllSelected) { // main segment may not be selected! for (unsigned i = 0; i < strip.getSegmentsNum(); i++) { Segment& seg = strip.getSegment(i); if (!seg.isActive() || !seg.isSelected()) continue; byte capabilities = seg.getLightCapabilities(); uint32_t mask = 0; bool isRGB = GET_BIT(capabilities, 0); // is segment RGB capable bool hasW = GET_BIT(capabilities, 1); // do we have white/CCT channel bool isCCT = GET_BIT(capabilities, 2); // is segment CCT capable bool wSlider = GET_BIT(capabilities, 3); // is white auto calculated (white slider NOT shown in UI) if (isRGB) mask |= 0x00FFFFFF; // RGB if (hasW) mask |= 0xFF000000; // white if (hasW && !wSlider && (c & 0xFF000000)) { // segment has white channel & white channel is auto calculated & white specified seg.setColor(0, c | 0xFFFFFF); // for accurate/brighter mode we fake white (since button may not set white color to 0xFFFFFF) } else if (c & mask) seg.setColor(0, c & mask); // only apply if not black if (isCCT && cct >= 0) seg.setCCT(cct); } setValuesFromFirstSelectedSeg(); } else { byte i = strip.getMainSegmentId(); Segment& seg = strip.getSegment(i); byte capabilities = seg.getLightCapabilities(); uint32_t mask = 0; bool isRGB = GET_BIT(capabilities, 0); // is segment RGB capable bool hasW = GET_BIT(capabilities, 1); // do we have white/CCT channel bool isCCT = GET_BIT(capabilities, 2); // is segment CCT capable bool wSlider = GET_BIT(capabilities, 3); // is white auto calculated (white slider NOT shown in UI) if (isRGB) mask |= 0x00FFFFFF; // RGB if (hasW) mask |= 0xFF000000; // white if (hasW && !wSlider && (c & 0xFF000000)) { // segment has white channel & white channel is auto calculated & white specified seg.setColor(0, c | 0xFFFFFF); // for accurate/brighter mode we fake white (since button may not set white color to 0xFFFFFF) } else if (c & mask) seg.setColor(0, c & mask); // only apply if not black if (isCCT && cct >= 0) seg.setCCT(cct); setValuesFromMainSeg(); } stateChanged = true; } static void changeWhite(int8_t amount, int16_t cct=-1) { Segment& seg = irApplyToAllSelected ? strip.getFirstSelectedSeg() : strip.getMainSegment(); byte r = R(seg.colors[0]); byte g = G(seg.colors[0]); byte b = B(seg.colors[0]); byte w = relativeChange(W(seg.colors[0]), amount, 5); changeColor(RGBW32(r, g, b, w), cct); } static void decodeIR24(uint32_t code) { switch (code) { case IR24_BRIGHTER : incBrightness(); break; case IR24_DARKER : decBrightness(); break; case IR24_OFF : if (bri > 0) briLast = bri; bri = 0; break; case IR24_ON : bri = briLast; break; case IR24_RED : changeColor(COLOR_RED); break; case IR24_REDDISH : changeColor(COLOR_REDDISH); break; case IR24_ORANGE : changeColor(COLOR_ORANGE); break; case IR24_YELLOWISH : changeColor(COLOR_YELLOWISH); break; case IR24_YELLOW : changeColor(COLOR_YELLOW); break; case IR24_GREEN : changeColor(COLOR_GREEN); break; case IR24_GREENISH : changeColor(COLOR_GREENISH); break; case IR24_TURQUOISE : changeColor(COLOR_TURQUOISE); break; case IR24_CYAN : changeColor(COLOR_CYAN); break; case IR24_AQUA : changeColor(COLOR_AQUA); break; case IR24_BLUE : changeColor(COLOR_BLUE); break; case IR24_DEEPBLUE : changeColor(COLOR_DEEPBLUE); break; case IR24_PURPLE : changeColor(COLOR_PURPLE); break; case IR24_MAGENTA : changeColor(COLOR_MAGENTA); break; case IR24_PINK : changeColor(COLOR_PINK); break; case IR24_WHITE : changeColor(COLOR_WHITE); changeEffect(FX_MODE_STATIC); break; case IR24_FLASH : presetFallback(1, FX_MODE_COLORTWINKLE, effectPalette); break; case IR24_STROBE : presetFallback(2, FX_MODE_RAINBOW_CYCLE, effectPalette); break; case IR24_FADE : presetFallback(3, FX_MODE_BREATH, effectPalette); break; case IR24_SMOOTH : presetFallback(4, FX_MODE_RAINBOW, effectPalette); break; default: return; } lastValidCode = code; } static void decodeIR24OLD(uint32_t code) { switch (code) { case IR24_OLD_BRIGHTER : incBrightness(); break; case IR24_OLD_DARKER : decBrightness(); break; case IR24_OLD_OFF : if (bri > 0) briLast = bri; bri = 0; break; case IR24_OLD_ON : bri = briLast; break; case IR24_OLD_RED : changeColor(COLOR_RED); break; case IR24_OLD_REDDISH : changeColor(COLOR_REDDISH); break; case IR24_OLD_ORANGE : changeColor(COLOR_ORANGE); break; case IR24_OLD_YELLOWISH : changeColor(COLOR_YELLOWISH); break; case IR24_OLD_YELLOW : changeColor(COLOR_YELLOW); break; case IR24_OLD_GREEN : changeColor(COLOR_GREEN); break; case IR24_OLD_GREENISH : changeColor(COLOR_GREENISH); break; case IR24_OLD_TURQUOISE : changeColor(COLOR_TURQUOISE); break; case IR24_OLD_CYAN : changeColor(COLOR_CYAN); break; case IR24_OLD_AQUA : changeColor(COLOR_AQUA); break; case IR24_OLD_BLUE : changeColor(COLOR_BLUE); break; case IR24_OLD_DEEPBLUE : changeColor(COLOR_DEEPBLUE); break; case IR24_OLD_PURPLE : changeColor(COLOR_PURPLE); break; case IR24_OLD_MAGENTA : changeColor(COLOR_MAGENTA); break; case IR24_OLD_PINK : changeColor(COLOR_PINK); break; case IR24_OLD_WHITE : changeColor(COLOR_WHITE); changeEffect(FX_MODE_STATIC); break; case IR24_OLD_FLASH : presetFallback(1, FX_MODE_COLORTWINKLE, 0); break; case IR24_OLD_STROBE : presetFallback(2, FX_MODE_RAINBOW_CYCLE, 0); break; case IR24_OLD_FADE : presetFallback(3, FX_MODE_BREATH, 0); break; case IR24_OLD_SMOOTH : presetFallback(4, FX_MODE_RAINBOW, 0); break; default: return; } lastValidCode = code; } static void decodeIR24CT(uint32_t code) { switch (code) { case IR24_CT_BRIGHTER : incBrightness(); break; case IR24_CT_DARKER : decBrightness(); break; case IR24_CT_OFF : if (bri > 0) briLast = bri; bri = 0; break; case IR24_CT_ON : bri = briLast; break; case IR24_CT_RED : changeColor(COLOR_RED); break; case IR24_CT_REDDISH : changeColor(COLOR_REDDISH); break; case IR24_CT_ORANGE : changeColor(COLOR_ORANGE); break; case IR24_CT_YELLOWISH : changeColor(COLOR_YELLOWISH); break; case IR24_CT_YELLOW : changeColor(COLOR_YELLOW); break; case IR24_CT_GREEN : changeColor(COLOR_GREEN); break; case IR24_CT_GREENISH : changeColor(COLOR_GREENISH); break; case IR24_CT_TURQUOISE : changeColor(COLOR_TURQUOISE); break; case IR24_CT_CYAN : changeColor(COLOR_CYAN); break; case IR24_CT_AQUA : changeColor(COLOR_AQUA); break; case IR24_CT_BLUE : changeColor(COLOR_BLUE); break; case IR24_CT_DEEPBLUE : changeColor(COLOR_DEEPBLUE); break; case IR24_CT_PURPLE : changeColor(COLOR_PURPLE); break; case IR24_CT_MAGENTA : changeColor(COLOR_MAGENTA); break; case IR24_CT_PINK : changeColor(COLOR_PINK); break; case IR24_CT_COLDWHITE : changeColor(COLOR_COLDWHITE2, 255); changeEffect(FX_MODE_STATIC); break; case IR24_CT_WARMWHITE : changeColor(COLOR_WARMWHITE2, 0); changeEffect(FX_MODE_STATIC); break; case IR24_CT_CTPLUS : changeColor(COLOR_COLDWHITE, strip.getSegment(strip.getMainSegmentId()).cct+1); changeEffect(FX_MODE_STATIC); break; case IR24_CT_CTMINUS : changeColor(COLOR_WARMWHITE, strip.getSegment(strip.getMainSegmentId()).cct-1); changeEffect(FX_MODE_STATIC); break; case IR24_CT_MEMORY : changeColor(COLOR_NEUTRALWHITE, 127); changeEffect(FX_MODE_STATIC); break; default: return; } lastValidCode = code; } static void decodeIR40(uint32_t code) { Segment& seg = irApplyToAllSelected ? strip.getFirstSelectedSeg() : strip.getMainSegment(); byte r = R(seg.colors[0]); byte g = G(seg.colors[0]); byte b = B(seg.colors[0]); byte w = W(seg.colors[0]); switch (code) { case IR40_BPLUS : incBrightness(); break; case IR40_BMINUS : decBrightness(); break; case IR40_OFF : if (bri > 0) briLast = bri; bri = 0; break; case IR40_ON : bri = briLast; break; case IR40_RED : changeColor(COLOR_RED); break; case IR40_REDDISH : changeColor(COLOR_REDDISH); break; case IR40_ORANGE : changeColor(COLOR_ORANGE); break; case IR40_YELLOWISH : changeColor(COLOR_YELLOWISH); break; case IR40_YELLOW : changeColor(COLOR_YELLOW); break; case IR40_GREEN : changeColor(COLOR_GREEN); break; case IR40_GREENISH : changeColor(COLOR_GREENISH); break; case IR40_TURQUOISE : changeColor(COLOR_TURQUOISE); break; case IR40_CYAN : changeColor(COLOR_CYAN); break; case IR40_AQUA : changeColor(COLOR_AQUA); break; case IR40_BLUE : changeColor(COLOR_BLUE); break; case IR40_DEEPBLUE : changeColor(COLOR_DEEPBLUE); break; case IR40_PURPLE : changeColor(COLOR_PURPLE); break; case IR40_MAGENTA : changeColor(COLOR_MAGENTA); break; case IR40_PINK : changeColor(COLOR_PINK); break; case IR40_WARMWHITE2 : changeColor(COLOR_WARMWHITE2, 0); changeEffect(FX_MODE_STATIC); break; case IR40_WARMWHITE : changeColor(COLOR_WARMWHITE, 63); changeEffect(FX_MODE_STATIC); break; case IR40_WHITE : changeColor(COLOR_NEUTRALWHITE, 127); changeEffect(FX_MODE_STATIC); break; case IR40_COLDWHITE : changeColor(COLOR_COLDWHITE, 191); changeEffect(FX_MODE_STATIC); break; case IR40_COLDWHITE2 : changeColor(COLOR_COLDWHITE2, 255); changeEffect(FX_MODE_STATIC); break; case IR40_WPLUS : changeWhite(10); break; case IR40_WMINUS : changeWhite(-10); break; case IR40_WOFF : if (w) whiteLast = w; changeColor(RGBW32(r, g, b, 0)); break; case IR40_WON : changeColor(RGBW32(r, g, b, whiteLast)); break; case IR40_W25 : bri = 63; break; case IR40_W50 : bri = 127; break; case IR40_W75 : bri = 191; break; case IR40_W100 : bri = 255; break; case IR40_QUICK : changeEffectSpeed( 16); break; case IR40_SLOW : changeEffectSpeed(-16); break; case IR40_JUMP7 : changeEffectIntensity( 16); break; case IR40_AUTO : changeEffectIntensity(-16); break; case IR40_JUMP3 : presetFallback(1, FX_MODE_STATIC, 0); break; case IR40_FADE3 : presetFallback(2, FX_MODE_BREATH, 0); break; case IR40_FADE7 : presetFallback(3, FX_MODE_FIRE_FLICKER, 0); break; case IR40_FLASH : presetFallback(4, FX_MODE_RAINBOW, 0); break; default: return; } lastValidCode = code; } static void decodeIR44(uint32_t code) { switch (code) { case IR44_BPLUS : incBrightness(); break; case IR44_BMINUS : decBrightness(); break; case IR44_OFF : if (bri > 0) briLast = bri; bri = 0; break; case IR44_ON : bri = briLast; break; case IR44_RED : changeColor(COLOR_RED); break; case IR44_REDDISH : changeColor(COLOR_REDDISH); break; case IR44_ORANGE : changeColor(COLOR_ORANGE); break; case IR44_YELLOWISH : changeColor(COLOR_YELLOWISH); break; case IR44_YELLOW : changeColor(COLOR_YELLOW); break; case IR44_GREEN : changeColor(COLOR_GREEN); break; case IR44_GREENISH : changeColor(COLOR_GREENISH); break; case IR44_TURQUOISE : changeColor(COLOR_TURQUOISE); break; case IR44_CYAN : changeColor(COLOR_CYAN); break; case IR44_AQUA : changeColor(COLOR_AQUA); break; case IR44_BLUE : changeColor(COLOR_BLUE); break; case IR44_DEEPBLUE : changeColor(COLOR_DEEPBLUE); break; case IR44_PURPLE : changeColor(COLOR_PURPLE); break; case IR44_MAGENTA : changeColor(COLOR_MAGENTA); break; case IR44_PINK : changeColor(COLOR_PINK); break; case IR44_WHITE : changeColor(COLOR_NEUTRALWHITE, 127); changeEffect(FX_MODE_STATIC); break; case IR44_WARMWHITE2 : changeColor(COLOR_WARMWHITE2, 0); changeEffect(FX_MODE_STATIC); break; case IR44_WARMWHITE : changeColor(COLOR_WARMWHITE, 63); changeEffect(FX_MODE_STATIC); break; case IR44_COLDWHITE : changeColor(COLOR_COLDWHITE, 191); changeEffect(FX_MODE_STATIC); break; case IR44_COLDWHITE2 : changeColor(COLOR_COLDWHITE2, 255); changeEffect(FX_MODE_STATIC); break; case IR44_REDPLUS : changeEffect(relativeChange(effectCurrent, 1, 0, strip.getModeCount() -1)); break; case IR44_REDMINUS : changeEffect(relativeChange(effectCurrent, -1, 0, strip.getModeCount() -1)); break; case IR44_GREENPLUS : changePalette(relativeChange(effectPalette, 1, 0, getPaletteCount() -1)); break; case IR44_GREENMINUS : changePalette(relativeChange(effectPalette, -1, 0, getPaletteCount() -1)); break; case IR44_BLUEPLUS : changeEffectIntensity( 16); break; case IR44_BLUEMINUS : changeEffectIntensity(-16); break; case IR44_QUICK : changeEffectSpeed( 16); break; case IR44_SLOW : changeEffectSpeed(-16); break; case IR44_DIY1 : presetFallback(1, FX_MODE_STATIC, 0); break; case IR44_DIY2 : presetFallback(2, FX_MODE_BREATH, 0); break; case IR44_DIY3 : presetFallback(3, FX_MODE_FIRE_FLICKER, 0); break; case IR44_DIY4 : presetFallback(4, FX_MODE_RAINBOW, 0); break; case IR44_DIY5 : presetFallback(5, FX_MODE_METEOR, 0); break; case IR44_DIY6 : presetFallback(6, FX_MODE_RAIN, 0); break; case IR44_AUTO : changeEffect(FX_MODE_STATIC); break; case IR44_FLASH : changeEffect(FX_MODE_PALETTE); break; case IR44_JUMP3 : bri = 63; break; case IR44_JUMP7 : bri = 127; break; case IR44_FADE3 : bri = 191; break; case IR44_FADE7 : bri = 255; break; default: return; } lastValidCode = code; } static void decodeIR21(uint32_t code) { switch (code) { case IR21_BRIGHTER: incBrightness(); break; case IR21_DARKER: decBrightness(); break; case IR21_OFF: if (bri > 0) briLast = bri; bri = 0; break; case IR21_ON: bri = briLast; break; case IR21_RED: changeColor(COLOR_RED); break; case IR21_REDDISH: changeColor(COLOR_REDDISH); break; case IR21_ORANGE: changeColor(COLOR_ORANGE); break; case IR21_YELLOWISH: changeColor(COLOR_YELLOWISH); break; case IR21_GREEN: changeColor(COLOR_GREEN); break; case IR21_GREENISH: changeColor(COLOR_GREENISH); break; case IR21_TURQUOISE: changeColor(COLOR_TURQUOISE); break; case IR21_CYAN: changeColor(COLOR_CYAN); break; case IR21_BLUE: changeColor(COLOR_BLUE); break; case IR21_DEEPBLUE: changeColor(COLOR_DEEPBLUE); break; case IR21_PURPLE: changeColor(COLOR_PURPLE); break; case IR21_PINK: changeColor(COLOR_PINK); break; case IR21_WHITE: changeColor(COLOR_WHITE); changeEffect(FX_MODE_STATIC); break; case IR21_FLASH: presetFallback(1, FX_MODE_COLORTWINKLE, 0); break; case IR21_STROBE: presetFallback(2, FX_MODE_RAINBOW_CYCLE, 0); break; case IR21_FADE: presetFallback(3, FX_MODE_BREATH, 0); break; case IR21_SMOOTH: presetFallback(4, FX_MODE_RAINBOW, 0); break; default: return; } lastValidCode = code; } static void decodeIR6(uint32_t code) { switch (code) { case IR6_POWER: toggleOnOff(); break; case IR6_CHANNEL_UP: incBrightness(); break; case IR6_CHANNEL_DOWN: decBrightness(); break; case IR6_VOLUME_UP: changeEffect(relativeChange(effectCurrent, 1, 0, strip.getModeCount() -1)); break; case IR6_VOLUME_DOWN: changePalette(relativeChange(effectPalette, 1, 0, getPaletteCount() -1)); switch(lastIR6ColourIdx) { case 0: changeColor(COLOR_RED); break; case 1: changeColor(COLOR_REDDISH); break; case 2: changeColor(COLOR_ORANGE); break; case 3: changeColor(COLOR_YELLOWISH); break; case 4: changeColor(COLOR_GREEN); break; case 5: changeColor(COLOR_GREENISH); break; case 6: changeColor(COLOR_TURQUOISE); break; case 7: changeColor(COLOR_CYAN); break; case 8: changeColor(COLOR_BLUE); break; case 9: changeColor(COLOR_DEEPBLUE); break; case 10:changeColor(COLOR_PURPLE); break; case 11:changeColor(COLOR_PINK); break; case 12:changeColor(COLOR_WHITE); break; default: break; } lastIR6ColourIdx++; if(lastIR6ColourIdx > 12) lastIR6ColourIdx = 0; break; case IR6_MUTE: changeEffect(FX_MODE_STATIC); changePalette(0); changeColor(COLOR_WHITE); bri=255; break; default: return; } lastValidCode = code; } static void decodeIR9(uint32_t code) { switch (code) { case IR9_POWER : toggleOnOff(); break; case IR9_A : presetFallback(1, FX_MODE_COLORTWINKLE, effectPalette); break; case IR9_B : presetFallback(2, FX_MODE_RAINBOW_CYCLE, effectPalette); break; case IR9_C : presetFallback(3, FX_MODE_BREATH, effectPalette); break; case IR9_UP : incBrightness(); break; case IR9_DOWN : decBrightness(); break; case IR9_LEFT : changeEffectSpeed(-16); break; case IR9_RIGHT : changeEffectSpeed(16); break; case IR9_SELECT : changeEffect(relativeChange(effectCurrent, 1, 0, strip.getModeCount() -1)); break; default: return; } lastValidCode = code; } /* This allows users to customize IR actions without the need to edit C code and compile. From the https://github.com/wled/WLED/wiki/Infrared-Control page, download the starter ir.json file that corresponds to the number of buttons on your remote. Many of the remotes with the same number of buttons emit the same codes, but will have different labels or colors. Once you edit the ir.json file, upload it to your controller using the /edit page. Each key should be the hex encoded IR code. The "cmd" property should be the HTTP API or JSON API command to execute on button press. If the command contains a relative change (SI=~16), it will register as a repeatable command. If the command doesn't contain a "~" but is repeatable, add "rpt" property set to true. Other properties are ignored but having labels and positions can assist with editing the json file. Sample: { "0xFF629D": {"cmd": "T=2", "rpt": true, "label": "Toggle on/off"}, // HTTP command "0xFF9867": {"cmd": "A=~16", "label": "Inc brightness"}, // HTTP command with incrementing "0xFF38C7": {"cmd": {"bri": 10}, "label": "Dim to 10"}, // JSON command "0xFF22DD": {"cmd": "!presetFallback", "PL": 1, "FX": 16, "FP": 6, // Custom command "label": "Preset 1, fallback to Saw - Party if not found"}, } */ static void decodeIRJson(uint32_t code) { char objKey[10]; char fileName[16]; String cmdStr; JsonObject fdo; JsonObject jsonCmdObj; if (!requestJSONBufferLock(JSON_LOCK_IR)) return; sprintf_P(objKey, PSTR("\"0x%lX\":"), (unsigned long)code); strcpy_P(fileName, PSTR("/ir.json")); // for FS.exists() // attempt to read command from ir.json // this may fail for two reasons: ir.json does not exist or IR code not found // if the IR code is not found readObjectFromFile() will clean() doc JSON document // so we can differentiate between the two readObjectFromFile(fileName, objKey, pDoc); fdo = pDoc->as(); lastValidCode = 0; if (fdo.isNull()) { //the received code does not exist if (!WLED_FS.exists(fileName)) errorFlag = ERR_FS_IRLOAD; //warn if IR file itself doesn't exist releaseJSONBufferLock(); return; } cmdStr = fdo["cmd"].as(); jsonCmdObj = fdo["cmd"]; //object if (jsonCmdObj.isNull()) // we could also use: fdo["cmd"].is() { if (cmdStr.startsWith("!")) { // call limited set of C functions if (cmdStr.startsWith(F("!incBri"))) { lastValidCode = code; incBrightness(); } else if (cmdStr.startsWith(F("!decBri"))) { lastValidCode = code; decBrightness(); } else if (cmdStr.startsWith(F("!presetF"))) { //!presetFallback uint8_t p1 = fdo["PL"] | 1; uint8_t p2 = fdo["FX"] | hw_random8(strip.getModeCount() -1); uint8_t p3 = fdo["FP"] | 0; presetFallback(p1, p2, p3); } } else { // HTTP API command String apireq = "win"; apireq += '&'; // reduce flash string usage if (cmdStr.indexOf("~") > 0 || fdo["rpt"]) lastValidCode = code; // repeatable action if (!cmdStr.startsWith(apireq)) cmdStr = apireq + cmdStr; // if no "win&" prefix if (!irApplyToAllSelected && cmdStr.indexOf(F("SS="))<0) { char tmp[10]; sprintf_P(tmp, PSTR("&SS=%d"), strip.getMainSegmentId()); cmdStr += tmp; } fdo.clear(); // clear JSON buffer (it is no longer needed) handleSet(nullptr, cmdStr, false); // no stateUpdated() call here } } else { // command is JSON object if (jsonCmdObj[F("psave")].isNull()) { if (irApplyToAllSelected && jsonCmdObj["seg"].is()) { JsonObject seg = jsonCmdObj["seg"][0]; // take 1st segment from array and use it to apply to all selected segments seg.remove("id"); // remove segment ID if it exists jsonCmdObj["seg"] = seg; // replace array with object } deserializeState(jsonCmdObj, CALL_MODE_BUTTON_PRESET); // **will call stateUpdated() with correct CALL_MODE** } else { uint8_t psave = jsonCmdObj[F("psave")].as(); char pname[33]; sprintf_P(pname, PSTR("IR Preset %d"), psave); fdo.clear(); if (psave > 0 && psave < 251) savePreset(psave, pname, fdo); } } releaseJSONBufferLock(); } static void applyRepeatActions() { if (irEnabled == 8) { decodeIRJson(lastValidCode); stateUpdated(CALL_MODE_BUTTON_PRESET); return; } else switch (lastRepeatableAction) { case ACTION_BRIGHT_UP : incBrightness(); stateUpdated(CALL_MODE_BUTTON); return; case ACTION_BRIGHT_DOWN : decBrightness(); stateUpdated(CALL_MODE_BUTTON); return; case ACTION_SPEED_UP : changeEffectSpeed(lastRepeatableValue); stateUpdated(CALL_MODE_BUTTON); return; case ACTION_SPEED_DOWN : changeEffectSpeed(lastRepeatableValue); stateUpdated(CALL_MODE_BUTTON); return; case ACTION_INTENSITY_UP : changeEffectIntensity(lastRepeatableValue); stateUpdated(CALL_MODE_BUTTON); return; case ACTION_INTENSITY_DOWN : changeEffectIntensity(lastRepeatableValue); stateUpdated(CALL_MODE_BUTTON); return; default: break; } if (lastValidCode == IR40_WPLUS) { changeWhite(10); stateUpdated(CALL_MODE_BUTTON); } else if (lastValidCode == IR40_WMINUS) { changeWhite(-10); stateUpdated(CALL_MODE_BUTTON); } else if ((lastValidCode == IR24_ON || lastValidCode == IR40_ON) && irTimesRepeated > 7 ) { nightlightActive = true; nightlightStartTime = millis(); stateUpdated(CALL_MODE_BUTTON); } } static void decodeIR(uint32_t code) { if (code == 0xFFFFFFFF) { //repeated code, continue brightness up/down irTimesRepeated++; applyRepeatActions(); return; } lastValidCode = 0; irTimesRepeated = 0; lastRepeatableAction = ACTION_NONE; if (irEnabled == 8) { // any remote configurable with ir.json file decodeIRJson(code); stateUpdated(CALL_MODE_BUTTON_PRESET); return; } if (code > 0xFFFFFF) return; //invalid code switch (irEnabled) { case 1: if (code > 0xF80000) decodeIR24OLD(code); // white 24-key remote (old) - it sends 0xFF0000 values else decodeIR24(code); // 24-key remote - 0xF70000 to 0xF80000 break; case 2: decodeIR24CT(code); break; // white 24-key remote with CW, WW, CT+ and CT- keys case 3: decodeIR40(code); break; // blue 40-key remote with 25%, 50%, 75% and 100% keys case 4: decodeIR44(code); break; // white 44-key remote with color-up/down keys and DIY1 to 6 keys case 5: decodeIR21(code); break; // white 21-key remote case 6: decodeIR6(code); break; // black 6-key learning remote defaults: "CH" controls brightness, // "VOL +" controls effect, "VOL -" controls colour/palette, "MUTE" // sets bright plain white case 7: decodeIR9(code); break; //case 8: return; // ir.json file, handled above switch statement } if (nightlightActive && bri == 0) nightlightActive = false; stateUpdated(CALL_MODE_BUTTON); //for notifier, IR is considered a button input } void initIR() { if (irEnabled > 0) { irrecv = new IRrecv(irPin); if (irrecv) irrecv->enableIRIn(); } else irrecv = nullptr; } void deInitIR() { if (irrecv) { irrecv->disableIRIn(); delete irrecv; } irrecv = nullptr; } void handleIR() { unsigned long currentTime = millis(); unsigned timeDiff = currentTime - irCheckedTime; if (timeDiff > 120 && irEnabled > 0 && irrecv) { if (strip.isUpdating() && timeDiff < 240) return; // be nice, but not too nice irCheckedTime = currentTime; if (irrecv->decode(&results)) { if (results.value != 0 && serialCanTX) { // only print results if anything is received ( != 0 ) Serial.printf_P(PSTR("IR recv: 0x%lX\n"), (unsigned long)results.value); } decodeIR(results.value); irrecv->resume(); } } } #endif ================================================ FILE: wled00/ir_codes.h ================================================ //Infrared codes //Add your custom codes here #define IRCUSTOM_ONOFF 0xA55AEA15 //Pioneer RC-975R "+FAV" button (example) #define IRCUSTOM_MACRO1 0xFFFFFFFF //placeholder, will never be checked for // Default IR codes for 6-key learning remote https://www.aliexpress.com/item/4000307837886.html // This cheap remote has the advantage of being more powerful (longer range) than cheap credit-card remotes #define IR6_POWER 0xFF0FF0 #define IR6_CHANNEL_UP 0xFF8F70 #define IR6_CHANNEL_DOWN 0xFF4FB0 #define IR6_VOLUME_UP 0xFFCF30 #define IR6_VOLUME_DOWN 0xFF2FD0 #define IR6_MUTE 0xFFAF50 #define IR9_POWER 0xFF629D #define IR9_A 0xFF22DD #define IR9_B 0xFF02FD #define IR9_C 0xFFC23D #define IR9_LEFT 0xFF30CF #define IR9_RIGHT 0xFF7A85 #define IR9_UP 0xFF9867 #define IR9_DOWN 0xFF38C7 #define IR9_SELECT 0xFF18E7 //Infrared codes for 24-key remote from http://woodsgood.ca/projects/2015/02/13/rgb-led-strip-controllers-ir-codes/ #define IR24_BRIGHTER 0xF700FF #define IR24_DARKER 0xF7807F #define IR24_OFF 0xF740BF #define IR24_ON 0xF7C03F #define IR24_RED 0xF720DF #define IR24_REDDISH 0xF710EF #define IR24_ORANGE 0xF730CF #define IR24_YELLOWISH 0xF708F7 #define IR24_YELLOW 0xF728D7 #define IR24_GREEN 0xF7A05F #define IR24_GREENISH 0xF7906F #define IR24_TURQUOISE 0xF7B04F #define IR24_CYAN 0xF78877 #define IR24_AQUA 0xF7A857 #define IR24_BLUE 0xF7609F #define IR24_DEEPBLUE 0xF750AF #define IR24_PURPLE 0xF7708F #define IR24_MAGENTA 0xF748B7 #define IR24_PINK 0xF76897 #define IR24_WHITE 0xF7E01F #define IR24_FLASH 0xF7D02F #define IR24_STROBE 0xF7F00F #define IR24_FADE 0xF7C837 #define IR24_SMOOTH 0xF7E817 // 24-key defs for white remote control with CW / WW / CT+ and CT- keys (from ALDI LED pillar lamp) #define IR24_CT_BRIGHTER 0xF700FF // BRI + #define IR24_CT_DARKER 0xF7807F // BRI - #define IR24_CT_OFF 0xF740BF // OFF #define IR24_CT_ON 0xF7C03F // ON #define IR24_CT_RED 0xF720DF // RED #define IR24_CT_REDDISH 0xF710EF // REDDISH #define IR24_CT_ORANGE 0xF730CF // ORANGE #define IR24_CT_YELLOWISH 0xF708F7 // YELLOWISH #define IR24_CT_YELLOW 0xF728D7 // YELLOW #define IR24_CT_GREEN 0xF7A05F // GREEN #define IR24_CT_GREENISH 0xF7906F // GREENISH #define IR24_CT_TURQUOISE 0xF7B04F // TURQUOISE #define IR24_CT_CYAN 0xF78877 // CYAN #define IR24_CT_AQUA 0xF7A857 // AQUA #define IR24_CT_BLUE 0xF7609F // BLUE #define IR24_CT_DEEPBLUE 0xF750AF // DEEPBLUE #define IR24_CT_PURPLE 0xF7708F // PURPLE #define IR24_CT_MAGENTA 0xF748B7 // MAGENTA #define IR24_CT_PINK 0xF76897 // PINK #define IR24_CT_COLDWHITE 0xF7E01F // CW #define IR24_CT_WARMWHITE 0xF7D02F // WW #define IR24_CT_CTPLUS 0xF7F00F // CT+ #define IR24_CT_CTMINUS 0xF7C837 // CT- #define IR24_CT_MEMORY 0xF7E817 // MEMORY // 24-key defs for old remote control #define IR24_OLD_BRIGHTER 0xFF906F // Brightness Up #define IR24_OLD_DARKER 0xFFB847 // Brightness Down #define IR24_OLD_OFF 0xFFF807 // Power OFF #define IR24_OLD_ON 0xFFB04F // Power On #define IR24_OLD_RED 0xFF9867 // RED #define IR24_OLD_REDDISH 0xFFE817 // Light RED #define IR24_OLD_ORANGE 0xFF02FD // Orange #define IR24_OLD_YELLOWISH 0xFF50AF // Light Orange #define IR24_OLD_YELLOW 0xFF38C7 // YELLOW #define IR24_OLD_GREEN 0xFFD827 // GREEN #define IR24_OLD_GREENISH 0xFF48B7 // Light GREEN #define IR24_OLD_TURQUOISE 0xFF32CD // TURQUOISE #define IR24_OLD_CYAN 0xFF7887 // CYAN #define IR24_OLD_AQUA 0xFF28D7 // AQUA #define IR24_OLD_BLUE 0xFF8877 // BLUE #define IR24_OLD_DEEPBLUE 0xFF6897 // Dark BLUE #define IR24_OLD_PURPLE 0xFF20DF // PURPLE #define IR24_OLD_MAGENTA 0xFF708F // MAGENTA #define IR24_OLD_PINK 0xFFF00F // PINK #define IR24_OLD_WHITE 0xFFA857 // WHITE #define IR24_OLD_FLASH 0xFFB24D // FLASH Mode #define IR24_OLD_STROBE 0xFF00FF // STROBE Mode #define IR24_OLD_FADE 0xFF58A7 // FADE Mode #define IR24_OLD_SMOOTH 0xFF30CF // SMOOTH Mode // 40-key defs for blue remote control #define IR40_BPLUS 0xFF3AC5 // #define IR40_BMINUS 0xFFBA45 // #define IR40_OFF 0xFF827D // #define IR40_ON 0xFF02FD // #define IR40_RED 0xFF1AE5 // #define IR40_GREEN 0xFF9A65 // #define IR40_BLUE 0xFFA25D // #define IR40_WHITE 0xFF22DD // natural white #define IR40_REDDISH 0xFF2AD5 // #define IR40_GREENISH 0xFFAA55 // #define IR40_DEEPBLUE 0xFF926D // #define IR40_WARMWHITE2 0xFF12ED // warmest white #define IR40_ORANGE 0xFF0AF5 // #define IR40_TURQUOISE 0xFF8A75 // #define IR40_PURPLE 0xFFB24D // #define IR40_WARMWHITE 0xFF32CD // warm white #define IR40_YELLOWISH 0xFF38C7 // #define IR40_CYAN 0xFFB847 // #define IR40_MAGENTA 0xFF7887 // #define IR40_COLDWHITE 0xFFF807 // cold white #define IR40_YELLOW 0xFF18E7 // #define IR40_AQUA 0xFF9867 // #define IR40_PINK 0xFF58A7 // #define IR40_COLDWHITE2 0xFFD827 // coldest white #define IR40_WPLUS 0xFF28D7 // white chanel bright plus #define IR40_WMINUS 0xFFA857 // white chanel bright minus #define IR40_WOFF 0xFF6897 // white chanel on #define IR40_WON 0xFFE817 // white chanel off #define IR40_W25 0xFF08F7 // white chanel 25% #define IR40_W50 0xFF8877 // white chanel 50% #define IR40_W75 0xFF48B7 // white chanel 75% #define IR40_W100 0xFFC837 // white chanel 100% #define IR40_JUMP3 0xFF30CF // JUMP3 #define IR40_FADE3 0xFFB04F // FADE3 #define IR40_JUMP7 0xFF708F // JUMP7 #define IR40_QUICK 0xFFF00F // QUICK #define IR40_FADE7 0xFF10EF // FADE7 #define IR40_FLASH 0xFF906F // FLASH #define IR40_AUTO 0xFF50AF // AUTO #define IR40_SLOW 0xFFD02F // SLOW // 44-key defs #define IR44_BPLUS 0xFF3AC5 // #define IR44_BMINUS 0xFFBA45 // #define IR44_OFF 0xFF827D // #define IR44_ON 0xFF02FD // #define IR44_RED 0xFF1AE5 // #define IR44_GREEN 0xFF9A65 // #define IR44_BLUE 0xFFA25D // #define IR44_WHITE 0xFF22DD // natural white #define IR44_REDDISH 0xFF2AD5 // #define IR44_GREENISH 0xFFAA55 // #define IR44_DEEPBLUE 0xFF926D // #define IR44_WARMWHITE2 0xFF12ED // warmest white #define IR44_ORANGE 0xFF0AF5 // #define IR44_TURQUOISE 0xFF8A75 // #define IR44_PURPLE 0xFFB24D // #define IR44_WARMWHITE 0xFF32CD // warm white #define IR44_YELLOWISH 0xFF38C7 // #define IR44_CYAN 0xFFB847 // #define IR44_MAGENTA 0xFF7887 // #define IR44_COLDWHITE 0xFFF807 // cold white #define IR44_YELLOW 0xFF18E7 // #define IR44_AQUA 0xFF9867 // #define IR44_PINK 0xFF58A7 // #define IR44_COLDWHITE2 0xFFD827 // coldest white #define IR44_REDPLUS 0xFF28D7 // #define IR44_GREENPLUS 0xFFA857 // #define IR44_BLUEPLUS 0xFF6897 // #define IR44_QUICK 0xFFE817 // #define IR44_REDMINUS 0xFF08F7 // #define IR44_GREENMINUS 0xFF8877 // #define IR44_BLUEMINUS 0xFF48B7 // #define IR44_SLOW 0xFFC837 // #define IR44_DIY1 0xFF30CF // #define IR44_DIY2 0xFFB04F // #define IR44_DIY3 0xFF708F // #define IR44_AUTO 0xFFF00F // #define IR44_DIY4 0xFF10EF // #define IR44_DIY5 0xFF906F // #define IR44_DIY6 0xFF50AF // #define IR44_FLASH 0xFFD02F // #define IR44_JUMP3 0xFF20DF // #define IR44_JUMP7 0xFFA05F // #define IR44_FADE3 0xFF609F // #define IR44_FADE7 0xFFE01F // //Infrared codes for 21-key remote https://images-na.ssl-images-amazon.com/images/I/51NMA0XucnL.jpg #define IR21_BRIGHTER 0xFFE01F #define IR21_DARKER 0xFFA857 #define IR21_OFF 0xFF629D #define IR21_ON 0xFFA25D #define IR21_RED 0xFF6897 #define IR21_REDDISH 0xFF30CF #define IR21_ORANGE 0xFF10EF #define IR21_YELLOWISH 0xFF42BD #define IR21_GREEN 0xFF9867 #define IR21_GREENISH 0xFF18E7 #define IR21_TURQUOISE 0xFF38C7 #define IR21_CYAN 0xFF4AB5 #define IR21_BLUE 0xFFB04F #define IR21_DEEPBLUE 0xFF7A85 #define IR21_PURPLE 0xFF5AA5 #define IR21_PINK 0xFF52AD #define IR21_WHITE 0xFF906F #define IR21_FLASH 0xFFE21D #define IR21_STROBE 0xFF22DD #define IR21_FADE 0xFF02FD #define IR21_SMOOTH 0xFFC23D #define COLOR_RED 0xFF0000 #define COLOR_REDDISH 0xFF7800 #define COLOR_ORANGE 0xFFA000 #define COLOR_YELLOWISH 0xFFC800 #define COLOR_YELLOW 0xFFFF00 #define COLOR_GREEN 0x00FF00 #define COLOR_GREENISH 0x00FF78 #define COLOR_TURQUOISE 0x00FFA0 #define COLOR_CYAN 0x00FFDC #define COLOR_AQUA 0x00C8FF #define COLOR_BLUE 0x00A0FF #define COLOR_DEEPBLUE 0x0000FF #define COLOR_PURPLE 0xAA00FF #define COLOR_MAGENTA 0xFF00DC #define COLOR_PINK 0xFF00A0 #define COLOR_WHITE 0xFFFFFFFF #define COLOR_WARMWHITE2 0xFFFFAA69 #define COLOR_WARMWHITE 0xFFFFBF8E #define COLOR_NEUTRALWHITE 0xFFFFD4B4 #define COLOR_COLDWHITE 0xFFFFE9D9 #define COLOR_COLDWHITE2 0xFFFFFFFF #define ACTION_NONE 0 #define ACTION_BRIGHT_UP 1 #define ACTION_BRIGHT_DOWN 2 #define ACTION_SPEED_UP 3 #define ACTION_SPEED_DOWN 4 #define ACTION_INTENSITY_UP 5 #define ACTION_INTENSITY_DOWN 6 #define ACTION_POWER 7 ================================================ FILE: wled00/json.cpp ================================================ #include "wled.h" #define JSON_PATH_STATE 1 #define JSON_PATH_INFO 2 #define JSON_PATH_STATE_INFO 3 #define JSON_PATH_NODES 4 #define JSON_PATH_PALETTES 5 #define JSON_PATH_FXDATA 6 #define JSON_PATH_NETWORKS 7 #define JSON_PATH_EFFECTS 8 /* * JSON API (De)serialization */ namespace { typedef struct { uint32_t colors[NUM_COLORS]; uint16_t start; uint16_t stop; uint16_t offset; uint16_t grouping; uint16_t spacing; uint16_t startY; uint16_t stopY; uint16_t options; uint8_t mode; uint8_t palette; uint8_t opacity; uint8_t speed; uint8_t intensity; uint8_t custom1; uint8_t custom2; uint8_t custom3; bool check1; bool check2; bool check3; } SegmentCopy; uint8_t differs(const Segment& b, const SegmentCopy& a) { uint8_t d = 0; if (a.start != b.start) d |= SEG_DIFFERS_BOUNDS; if (a.stop != b.stop) d |= SEG_DIFFERS_BOUNDS; if (a.offset != b.offset) d |= SEG_DIFFERS_GSO; if (a.grouping != b.grouping) d |= SEG_DIFFERS_GSO; if (a.spacing != b.spacing) d |= SEG_DIFFERS_GSO; if (a.opacity != b.opacity) d |= SEG_DIFFERS_BRI; if (a.mode != b.mode) d |= SEG_DIFFERS_FX; if (a.speed != b.speed) d |= SEG_DIFFERS_FX; if (a.intensity != b.intensity) d |= SEG_DIFFERS_FX; if (a.palette != b.palette) d |= SEG_DIFFERS_FX; if (a.custom1 != b.custom1) d |= SEG_DIFFERS_FX; if (a.custom2 != b.custom2) d |= SEG_DIFFERS_FX; if (a.custom3 != b.custom3) d |= SEG_DIFFERS_FX; if (a.check1 != b.check1) d |= SEG_DIFFERS_FX; if (a.check2 != b.check2) d |= SEG_DIFFERS_FX; if (a.check3 != b.check3) d |= SEG_DIFFERS_FX; if (a.startY != b.startY) d |= SEG_DIFFERS_BOUNDS; if (a.stopY != b.stopY) d |= SEG_DIFFERS_BOUNDS; //bit pattern: (msb first) // set:2, sound:2, mapping:3, transposed, mirrorY, reverseY, [reset,] paused, mirrored, on, reverse, [selected] if ((a.options & 0b1111111111011110U) != (b.options & 0b1111111111011110U)) d |= SEG_DIFFERS_OPT; if ((a.options & 0x0001U) != (b.options & 0x0001U)) d |= SEG_DIFFERS_SEL; for (unsigned i = 0; i < NUM_COLORS; i++) if (a.colors[i] != b.colors[i]) d |= SEG_DIFFERS_COL; return d; } } static bool deserializeSegment(JsonObject elem, byte it, byte presetId = 0) { byte id = elem["id"] | it; if (id >= WS2812FX::getMaxSegments()) return false; bool newSeg = false; int stop = elem["stop"] | -1; // append segment if (id >= strip.getSegmentsNum()) { if (stop <= 0) return false; // ignore empty/inactive segments strip.appendSegment(0, strip.getLengthTotal()); id = strip.getSegmentsNum()-1; // segments are added at the end of list newSeg = true; } //DEBUG_PRINTLN(F("-- JSON deserialize segment.")); Segment& seg = strip.getSegment(id); // we do not want to make segment copy as it may use a lot of RAM (effect data and pixel buffer) // so we will create a copy of segment options and compare it with original segment when done processing SegmentCopy prev = { {seg.colors[0], seg.colors[1], seg.colors[2]}, seg.start, seg.stop, seg.offset, seg.grouping, seg.spacing, seg.startY, seg.stopY, seg.options, seg.mode, seg.palette, seg.opacity, seg.speed, seg.intensity, seg.custom1, seg.custom2, seg.custom3, seg.check1, seg.check2, seg.check3 }; int start = elem["start"] | seg.start; if (stop < 0) { int len = elem["len"]; stop = (len > 0) ? start + len : seg.stop; } // 2D segments int startY = elem["startY"] | seg.startY; int stopY = elem["stopY"] | seg.stopY; //repeat, multiplies segment until all LEDs are used, or max segments reached bool repeat = elem["rpt"] | false; if (repeat && stop>0) { elem.remove("id"); // remove for recursive call elem.remove("rpt"); // remove for recursive call elem.remove("n"); // remove for recursive call unsigned len = stop - start; for (size_t i=id+1; i= strip.getLengthTotal()) break; //TODO: add support for 2D elem["start"] = start; elem["stop"] = start + len; elem["rev"] = !elem["rev"]; // alternate reverse on even/odd segments deserializeSegment(elem, i, presetId); // recursive call with new id } return true; } if (elem["n"]) { // name field exists const char * name = elem["n"].as(); seg.setName(name); // will resolve empty and null correctly } else if (start != seg.start || stop != seg.stop) { // clearing or setting segment without name field seg.clearName(); } uint16_t grp = elem["grp"] | seg.grouping; uint16_t spc = elem[F("spc")] | seg.spacing; uint16_t of = seg.offset; uint8_t soundSim = elem["si"] | seg.soundSim; uint8_t map1D2D = elem["m12"] | seg.map1D2D; uint8_t set = elem[F("set")] | seg.set; bool selected = getBoolVal(elem["sel"], seg.selected); bool reverse = getBoolVal(elem["rev"], seg.reverse); bool mirror = getBoolVal(elem["mi"] , seg.mirror); #ifndef WLED_DISABLE_2D bool reverse_y = getBoolVal(elem["rY"] , seg.reverse_y); bool mirror_y = getBoolVal(elem["mY"] , seg.mirror_y); bool transpose = getBoolVal(elem[F("tp")], seg.transpose); #endif // if segment's virtual dimensions change we need to restart effect (segment blending and PS rely on dimensions) if (seg.mirror != mirror) seg.markForReset(); #ifndef WLED_DISABLE_2D if (seg.mirror_y != mirror_y || seg.transpose != transpose) seg.markForReset(); #endif int len = (stop > start) ? stop - start : 1; int offset = elem[F("of")] | INT32_MAX; if (offset != INT32_MAX) { int offsetAbs = abs(offset); if (offsetAbs > len - 1) offsetAbs %= len; if (offset < 0) offsetAbs = len - offsetAbs; of = offsetAbs; } if (stop > start && of > len -1) of = len -1; // update segment (delete if necessary) seg.setGeometry(start, stop, grp, spc, of, startY, stopY, map1D2D); // strip needs to be suspended for this to work without issues if (newSeg) seg.refreshLightCapabilities(); // fix for #3403 if (seg.reset && seg.stop == 0) { if (id == strip.getMainSegmentId()) strip.setMainSegmentId(0); // fix for #3403 return true; // segment was deleted & is marked for reset, no need to change anything else } byte segbri = seg.opacity; if (getVal(elem["bri"], segbri)) { if (segbri > 0) seg.setOpacity(segbri); // use transition seg.setOption(SEG_OPTION_ON, segbri); // use transition } seg.setOption(SEG_OPTION_ON, getBoolVal(elem["on"], seg.on)); // use transition seg.freeze = getBoolVal(elem["frz"], seg.freeze); seg.setCCT(elem["cct"] | seg.cct); JsonArray colarr = elem["col"]; if (!colarr.isNull()) { if (seg.getLightCapabilities() & 3) { // segment has RGB or White for (size_t i = 0; i < NUM_COLORS; i++) { // JSON "col" array can contain the following values for each of segment's colors (primary, background, custom): // "col":[int|string|object|array, int|string|object|array, int|string|object|array] // int = Kelvin temperature or 0 for black // string = hex representation of [WW]RRGGBB or "r" for random color // object = individual channel control {"r":0,"g":127,"b":255,"w":255}, each being optional (valid to send {}) // array = direct channel values [r,g,b,w] (w element being optional) int rgbw[] = {0,0,0,0}; bool colValid = false; JsonArray colX = colarr[i]; if (colX.isNull()) { JsonObject oCol = colarr[i]; if (!oCol.isNull()) { // we have a JSON object for color {"w":123,"r":123,...}; allows individual channel control rgbw[0] = oCol["r"] | R(seg.colors[i]); rgbw[1] = oCol["g"] | G(seg.colors[i]); rgbw[2] = oCol["b"] | B(seg.colors[i]); rgbw[3] = oCol["w"] | W(seg.colors[i]); colValid = true; } else { byte brgbw[] = {0,0,0,0}; const char* hexCol = colarr[i]; if (hexCol == nullptr) { //Kelvin color temperature (or invalid), e.g 2400 int kelvin = colarr[i] | -1; if (kelvin < 0) continue; if (kelvin == 0) seg.setColor(i, 0); if (kelvin > 0) colorKtoRGB(kelvin, brgbw); colValid = true; } else if (hexCol[0] == 'r' && hexCol[1] == '\0') { // Random colors via JSON API in Segment object like col=["r","r","r"] · Issue #4996 setRandomColor(brgbw); colValid = true; } else { //HEX string, e.g. "FFAA00" colValid = colorFromHexString(brgbw, hexCol); } for (size_t c = 0; c < 4; c++) rgbw[c] = brgbw[c]; } } else { //Array of ints (RGB or RGBW color), e.g. [255,160,0] byte sz = colX.size(); if (sz == 0) continue; //do nothing on empty array copyArray(colX, rgbw, 4); colValid = true; } if (!colValid) continue; seg.setColor(i, RGBW32(rgbw[0],rgbw[1],rgbw[2],rgbw[3])); // use transition if (seg.mode == FX_MODE_STATIC) strip.trigger(); //instant refresh } } else { // non RGB & non White segment (usually On/Off bus) seg.setColor(0, ULTRAWHITE); // use transition seg.setColor(1, BLACK); // use transition } } // lx parser #ifdef WLED_ENABLE_LOXONE int lx = elem[F("lx")] | -1; if (lx >= 0) { parseLxJson(lx, id, false); } int ly = elem[F("ly")] | -1; if (ly >= 0) { parseLxJson(ly, id, true); } #endif seg.set = constrain(set, 0, 3); seg.soundSim = constrain(soundSim, 0, 3); seg.selected = selected; seg.reverse = reverse; seg.mirror = mirror; #ifndef WLED_DISABLE_2D seg.reverse_y = reverse_y; seg.mirror_y = mirror_y; seg.transpose = transpose; #endif byte fx = seg.mode; if (getVal(elem["fx"], fx, 0, strip.getModeCount())) { if (!presetId && currentPlaylist>=0) unloadPlaylist(); if (fx != seg.mode) seg.setMode(fx, elem[F("fxdef")]); // use transition (WARNING: may change map1D2D causing geometry change) } getVal(elem["sx"], seg.speed); getVal(elem["ix"], seg.intensity); uint8_t pal = seg.palette; if (seg.getLightCapabilities() & 1) { // ignore palette for White and On/Off segments if (getVal(elem["pal"], pal, 0, getPaletteCount())) seg.setPalette(pal); } getVal(elem["c1"], seg.custom1); getVal(elem["c2"], seg.custom2); uint8_t cust3 = seg.custom3; getVal(elem["c3"], cust3, 0, 31); // we can't pass reference to bitfield seg.custom3 = constrain(cust3, 0, 31); seg.check1 = getBoolVal(elem["o1"], seg.check1); seg.check2 = getBoolVal(elem["o2"], seg.check2); seg.check3 = getBoolVal(elem["o3"], seg.check3); uint8_t blend = seg.blendMode; getVal(elem["bm"], blend, 0, 15); // we can't pass reference to bitfield seg.blendMode = constrain(blend, 0, 15); JsonArray iarr = elem[F("i")]; //set individual LEDs if (!iarr.isNull()) { // set brightness immediately and disable transition jsonTransitionOnce = true; if (seg.isInTransition()) seg.startTransition(0); // setting transition time to 0 will stop transition in next frame strip.setTransition(0); strip.setBrightness(bri, true); // freeze and init to black if (!seg.freeze) { seg.freeze = true; seg.clear(); } unsigned iStart = 0, iStop = 0; unsigned iSet = 0; //0 nothing set, 1 start set, 2 range set for (size_t i = 0; i < iarr.size(); i++) { if (iarr[i].is()) { if (!iSet) { iStart = abs(iarr[i].as()); iSet++; } else { iStop = abs(iarr[i].as()); iSet++; } } else { //color uint8_t rgbw[] = {0,0,0,0}; JsonArray icol = iarr[i]; if (!icol.isNull()) { //array, e.g. [255,0,0] byte sz = icol.size(); if (sz > 0 && sz < 5) copyArray(icol, rgbw); } else { //hex string, e.g. "FF0000" byte brgbw[] = {0,0,0,0}; const char* hexCol = iarr[i]; if (colorFromHexString(brgbw, hexCol)) { for (size_t c = 0; c < 4; c++) rgbw[c] = brgbw[c]; } } if (iSet < 2 || iStop <= iStart) iStop = iStart + 1; uint32_t c = RGBW32(rgbw[0], rgbw[1], rgbw[2], rgbw[3]); while (iStart < iStop) seg.setRawPixelColor(iStart++, c); // sets pixel color without 1D->2D expansion, grouping or spacing iSet = 0; } } strip.trigger(); // force segment update } // send UDP/WS if segment options changed (except selection; will also deselect current preset) if (differs(seg, prev) & ~SEG_DIFFERS_SEL) stateChanged = true; return true; } // deserializes WLED state // presetId is non-0 if called from handlePreset() bool deserializeState(JsonObject root, byte callMode, byte presetId) { bool stateResponse = root[F("v")] | false; #if defined(WLED_DEBUG) && defined(WLED_DEBUG_HOST) netDebugEnabled = root[F("debug")] | netDebugEnabled; #endif bool onBefore = bri; getVal(root["bri"], bri); if (bri != briOld) stateChanged = true; bool on = root["on"] | (bri > 0); if (!on != !bri) toggleOnOff(); if (root["on"].is() && root["on"].as()[0] == 't') { if (onBefore || !bri) toggleOnOff(); // do not toggle off again if just turned on by bri (makes e.g. "{"on":"t","bri":32}" work) } if (bri && !onBefore) { // unfreeze all segments when turning on for (size_t s=0; s < strip.getSegmentsNum(); s++) { strip.getSegment(s).freeze = false; } if (realtimeMode && !realtimeOverride && useMainSegmentOnly) { // keep live segment frozen if live strip.getMainSegment().freeze = true; } } long tr = -1; if (!presetId || currentPlaylist < 0) { //do not apply transition time from preset if playlist active, as it would override playlist transition times tr = root[F("transition")] | -1; if (tr >= 0) { transitionDelay = tr * 100; strip.setTransition(transitionDelay); } } blendingStyle = root[F("bs")] | blendingStyle; blendingStyle &= 0x1F; // temporary transition (applies only once) tr = root[F("tt")] | -1; if (tr >= 0) { jsonTransitionOnce = true; strip.setTransition(tr * 100); } tr = root[F("tb")] | -1; if (tr >= 0) strip.timebase = (unsigned long)tr - millis(); JsonObject nl = root["nl"]; if (!nl.isNull()) stateChanged = true; nightlightActive = getBoolVal(nl["on"], nightlightActive); nightlightDelayMins = nl["dur"] | nightlightDelayMins; nightlightMode = nl["mode"] | nightlightMode; nightlightTargetBri = nl[F("tbri")] | nightlightTargetBri; JsonObject udpn = root["udpn"]; sendNotificationsRT = getBoolVal(udpn[F("send")], sendNotificationsRT); syncGroups = udpn[F("sgrp")] | syncGroups; receiveGroups = udpn[F("rgrp")] | receiveGroups; if ((bool)udpn[F("nn")]) callMode = CALL_MODE_NO_NOTIFY; //send no notification just for this request unsigned long timein = root["time"] | UINT32_MAX; //backup time source if NTP not synced if (timein != UINT32_MAX) { setTimeFromAPI(timein); if (presetsModifiedTime == 0) presetsModifiedTime = timein; } if (root[F("psave")].isNull()) doReboot = root[F("rb")] | doReboot; // do not allow changing main segment while in realtime mode (may get odd results else) if (!realtimeMode) strip.setMainSegmentId(root[F("mainseg")] | strip.getMainSegmentId()); // must be before realtimeLock() if "live" realtimeOverride = root[F("lor")] | realtimeOverride; if (realtimeOverride > 2) realtimeOverride = REALTIME_OVERRIDE_ALWAYS; if (realtimeMode && useMainSegmentOnly) { strip.getMainSegment().freeze = !realtimeOverride; realtimeOverride = REALTIME_OVERRIDE_NONE; // ignore request for override if using main segment only } if (root.containsKey("live")) { if (root["live"].as()) { jsonTransitionOnce = true; strip.setTransition(0); realtimeLock(65000); } else { exitRealtime(); } } int it = 0; JsonVariant segVar = root["seg"]; if (!segVar.isNull()) { // we may be called during strip.service() so we must not modify segments while effects are executing strip.suspend(); strip.waitForIt(); if (segVar.is()) { int id = segVar["id"] | -1; //if "seg" is not an array and ID not specified, apply to all selected/checked segments if (id < 0) { //apply all selected segments for (size_t s = 0; s < strip.getSegmentsNum(); s++) { const Segment &sg = strip.getSegment(s); if (sg.isActive() && sg.isSelected()) { deserializeSegment(segVar, s, presetId); } } } else { deserializeSegment(segVar, id, presetId); //apply only the segment with the specified ID } } else { size_t deleted = 0; JsonArray segs = segVar.as(); for (JsonObject elem : segs) { if (deserializeSegment(elem, it++, presetId) && !elem["stop"].isNull() && elem["stop"]==0) deleted++; } if (strip.getSegmentsNum() > 3 && deleted >= strip.getSegmentsNum()/2U) strip.purgeSegments(); // batch deleting more than half segments } strip.resume(); } UsermodManager::readFromJsonState(root); loadLedmap = root[F("ledmap")] | loadLedmap; byte ps = root[F("psave")]; if (ps > 0 && ps < 251) savePreset(ps, nullptr, root); ps = root[F("pdel")]; //deletion if (ps > 0 && ps < 251) deletePreset(ps); // HTTP API commands (must be handled before "ps") const char* httpwin = root["win"]; if (httpwin) { String apireq = "win"; apireq += '&'; // reduce flash string usage apireq += httpwin; handleSet(nullptr, apireq, false); // may set stateChanged } // Applying preset from JSON API has 2 cases: a) "pd" AKA "preset direct" and b) "ps" AKA "preset select" // a) "preset direct" can only be an integer value representing preset ID. "preset direct" assumes JSON API contains the rest of preset content (i.e. from UI call) // "preset direct" JSON can contain "ps" API (i.e. call from UI to cycle presets) in such case stateChanged has to be false (i.e. no "win" or "seg" API) // b) "preset select" can be cycling ("1~5~""), random ("r" or "1~5r"), ID, etc. value allowed from JSON API. This type of call assumes no state changing content in API call byte presetToRestore = 0; if (!root[F("pd")].isNull() && stateChanged) { // a) already applied preset content (requires "seg" or "win" but will ignore the rest) currentPreset = root[F("pd")] | currentPreset; if (root["win"].isNull()) presetCycCurr = currentPreset; // otherwise presetCycCurr was set in handleSet() [set.cpp] presetToRestore = currentPreset; // stateUpdated() will clear the preset, so we need to restore it after DEBUG_PRINTF_P(PSTR("Preset direct: %d\n"), currentPreset); } else if (!root["ps"].isNull()) { // we have "ps" call (i.e. from button or external API call) or "pd" that includes "ps" (i.e. from UI call) if (root["win"].isNull() && getVal(root["ps"], presetCycCurr, 1, 250) && presetCycCurr > 0 && presetCycCurr < 251 && presetCycCurr != currentPreset) { DEBUG_PRINTF_P(PSTR("Preset select: %d\n"), presetCycCurr); // b) preset ID only or preset that does not change state (use embedded cycling limits if they exist in getVal()) applyPreset(presetCycCurr, callMode); // async load from file system (only preset ID was specified) return stateResponse; } else presetCycCurr = currentPreset; // restore presetCycCurr } JsonObject playlist = root[F("playlist")]; if (!playlist.isNull() && loadPlaylist(playlist, presetId)) { //do not notify here, because the first playlist entry will do if (root["on"].isNull()) callMode = CALL_MODE_NO_NOTIFY; else callMode = CALL_MODE_DIRECT_CHANGE; // possible bugfix for playlist only containing HTTP API preset FX=~ } if (root.containsKey(F("rmcpal"))) { char fileName[32]; sprintf_P(fileName, PSTR("/palette%d.json"), root[F("rmcpal")].as()); if (WLED_FS.exists(fileName)) WLED_FS.remove(fileName); loadCustomPalettes(); } doAdvancePlaylist = root[F("np")] | doAdvancePlaylist; //advances to next preset in playlist when true JsonObject wifi = root[F("wifi")]; if (!wifi.isNull()) { bool apMode = getBoolVal(wifi[F("ap")], apActive); if (!apActive && apMode) WLED::instance().initAP(); // start AP mode immediately else if (apActive && !apMode) { // stop AP mode immediately dnsServer.stop(); WiFi.softAPdisconnect(true); apActive = false; } //bool restart = wifi[F("restart")] | false; //if (restart) forceReconnect = true; } if (stateChanged) stateUpdated(callMode); if (presetToRestore) currentPreset = presetToRestore; return stateResponse; } static void serializeSegment(JsonObject& root, const Segment& seg, byte id, bool forPreset, bool segmentBounds) { root["id"] = id; if (segmentBounds) { root["start"] = seg.start; root["stop"] = seg.stop; #ifndef WLED_DISABLE_2D if (strip.isMatrix) { root[F("startY")] = seg.startY; root[F("stopY")] = seg.stopY; } #endif } if (!forPreset) root["len"] = seg.stop - seg.start; root["grp"] = seg.grouping; root[F("spc")] = seg.spacing; root[F("of")] = seg.offset; root["on"] = seg.on; root["frz"] = seg.freeze; byte segbri = seg.opacity; root["bri"] = (segbri) ? segbri : 255; root["cct"] = seg.cct; root[F("set")] = seg.set; root["lc"] = seg.getLightCapabilities(); if (seg.name != nullptr) root["n"] = reinterpret_cast(seg.name); //not good practice, but decreases required JSON buffer else if (forPreset) root["n"] = ""; // to conserve RAM we will serialize the col array manually // this will reduce RAM footprint from ~300 bytes to 84 bytes per segment char colstr[70]; colstr[0] = '['; colstr[1] = '\0'; //max len 68 (5 chan, all 255) const char *format = strip.hasWhiteChannel() ? PSTR("[%u,%u,%u,%u]") : PSTR("[%u,%u,%u]"); for (size_t i = 0; i < 3; i++) { byte segcol[4]; byte* c = segcol; segcol[0] = R(seg.colors[i]); segcol[1] = G(seg.colors[i]); segcol[2] = B(seg.colors[i]); segcol[3] = W(seg.colors[i]); char tmpcol[22]; sprintf_P(tmpcol, format, (unsigned)c[0], (unsigned)c[1], (unsigned)c[2], (unsigned)c[3]); strcat(colstr, i<2 ? strcat(tmpcol, ",") : tmpcol); } strcat(colstr, "]"); root["col"] = serialized(colstr); root["fx"] = seg.mode; root["sx"] = seg.speed; root["ix"] = seg.intensity; root["pal"] = seg.palette; root["c1"] = seg.custom1; root["c2"] = seg.custom2; root["c3"] = seg.custom3; root["sel"] = seg.isSelected(); root["rev"] = seg.reverse; root["mi"] = seg.mirror; #ifndef WLED_DISABLE_2D if (strip.isMatrix) { root["rY"] = seg.reverse_y; root["mY"] = seg.mirror_y; root[F("tp")] = seg.transpose; } #endif root["o1"] = seg.check1; root["o2"] = seg.check2; root["o3"] = seg.check3; root["si"] = seg.soundSim; root["m12"] = seg.map1D2D; root["bm"] = seg.blendMode; } void serializeState(JsonObject root, bool forPreset, bool includeBri, bool segmentBounds, bool selectedSegmentsOnly) { if (includeBri) { root["on"] = (bri > 0); root["bri"] = briLast; root[F("transition")] = transitionDelay/100; //in 100ms root[F("bs")] = blendingStyle; } if (!forPreset) { if (errorFlag) {root[F("error")] = errorFlag; errorFlag = ERR_NONE;} //prevent error message to persist on screen root["ps"] = (currentPreset > 0) ? currentPreset : -1; root[F("pl")] = currentPlaylist; root[F("ledmap")] = currentLedmap; UsermodManager::addToJsonState(root); JsonObject nl = root.createNestedObject("nl"); nl["on"] = nightlightActive; nl["dur"] = nightlightDelayMins; nl["mode"] = nightlightMode; nl[F("tbri")] = nightlightTargetBri; nl[F("rem")] = nightlightActive ? (int)(nightlightDelayMs - (millis() - nightlightStartTime)) / 1000 : -1; // seconds remaining JsonObject udpn = root.createNestedObject("udpn"); udpn[F("send")] = sendNotificationsRT; udpn[F("recv")] = receiveGroups != 0; udpn[F("sgrp")] = syncGroups; udpn[F("rgrp")] = receiveGroups; root[F("lor")] = realtimeOverride; } root[F("mainseg")] = strip.getMainSegmentId(); JsonArray seg = root.createNestedArray("seg"); for (size_t s = 0; s < WS2812FX::getMaxSegments(); s++) { if (s >= strip.getSegmentsNum()) { if (forPreset && segmentBounds && !selectedSegmentsOnly) { //disable segments not part of preset JsonObject seg0 = seg.createNestedObject(); seg0["stop"] = 0; continue; } else break; } const Segment &sg = strip.getSegment(s); if (forPreset && selectedSegmentsOnly && !sg.isSelected()) continue; if (sg.isActive()) { JsonObject seg0 = seg.createNestedObject(); serializeSegment(seg0, sg, s, forPreset, segmentBounds); } else if (forPreset && segmentBounds) { //disable segments not part of preset JsonObject seg0 = seg.createNestedObject(); seg0["stop"] = 0; } } } void serializeInfo(JsonObject root) { root[F("ver")] = versionString; root[F("vid")] = VERSION; root[F("cn")] = F(WLED_CODENAME); root[F("release")] = releaseString; root[F("repo")] = repoString; root[F("deviceId")] = getDeviceId(); JsonObject leds = root.createNestedObject(F("leds")); leds[F("count")] = strip.getLengthTotal(); leds[F("pwr")] = BusManager::currentMilliamps(); leds["fps"] = strip.getFps(); leds[F("maxpwr")] = BusManager::currentMilliamps()>0 ? BusManager::ablMilliampsMax() : 0; leds[F("maxseg")] = WS2812FX::getMaxSegments(); //leds[F("actseg")] = strip.getActiveSegmentsNum(); //leds[F("seglock")] = false; //might be used in the future to prevent modifications to segment config leds[F("bootps")] = bootPreset; #ifndef WLED_DISABLE_2D if (strip.isMatrix) { JsonObject matrix = leds.createNestedObject(F("matrix")); matrix["w"] = Segment::maxWidth; matrix["h"] = Segment::maxHeight; } #endif unsigned totalLC = 0; JsonArray lcarr = leds.createNestedArray(F("seglc")); // deprecated, use state.seg[].lc size_t nSegs = strip.getSegmentsNum(); for (size_t s = 0; s < nSegs; s++) { if (!strip.getSegment(s).isActive()) continue; unsigned lc = strip.getSegment(s).getLightCapabilities(); totalLC |= lc; lcarr.add(lc); // deprecated, use state.seg[].lc } leds["lc"] = totalLC; leds[F("rgbw")] = strip.hasRGBWBus(); // deprecated, use info.leds.lc leds[F("wv")] = totalLC & 0x02; // deprecated, true if white slider should be displayed for any segment leds["cct"] = totalLC & 0x04; // deprecated, use info.leds.lc #ifdef WLED_DEBUG JsonArray i2c = root.createNestedArray(F("i2c")); i2c.add(i2c_sda); i2c.add(i2c_scl); JsonArray spi = root.createNestedArray(F("spi")); spi.add(spi_mosi); spi.add(spi_sclk); spi.add(spi_miso); #endif root[F("str")] = false; // sync toggle receive root[F("name")] = serverDescription; root[F("udpport")] = udpPort; root[F("simplifiedui")] = simplifiedUI; root["live"] = (bool)realtimeMode; root[F("liveseg")] = useMainSegmentOnly ? strip.getMainSegmentId() : -1; // if using main segment only for live switch (realtimeMode) { case REALTIME_MODE_INACTIVE: root["lm"] = ""; break; case REALTIME_MODE_GENERIC: root["lm"] = ""; break; case REALTIME_MODE_UDP: root["lm"] = F("UDP"); break; case REALTIME_MODE_HYPERION: root["lm"] = F("Hyperion"); break; case REALTIME_MODE_E131: root["lm"] = F("E1.31"); break; case REALTIME_MODE_ADALIGHT: root["lm"] = F("USB Adalight/TPM2"); break; case REALTIME_MODE_ARTNET: root["lm"] = F("Art-Net"); break; case REALTIME_MODE_TPM2NET: root["lm"] = F("tpm2.net"); break; case REALTIME_MODE_DDP: root["lm"] = F("DDP"); break; } root[F("lip")] = realtimeIP[0] == 0 ? "" : realtimeIP.toString(); #ifdef WLED_ENABLE_WEBSOCKETS root[F("ws")] = ws.count(); #else root[F("ws")] = -1; #endif root[F("fxcount")] = strip.getModeCount(); root[F("palcount")] = getPaletteCount(); root[F("cpalcount")] = customPalettes.size(); // number of custom palettes root[F("cpalmax")] = WLED_MAX_CUSTOM_PALETTES; // maximum number of custom palettes JsonArray ledmaps = root.createNestedArray(F("maps")); for (size_t i=0; i>i) & 0x00000001U) { JsonObject ledmaps0 = ledmaps.createNestedObject(); ledmaps0["id"] = i; #ifndef ESP8266 if (i && ledmapNames[i-1]) ledmaps0["n"] = ledmapNames[i-1]; #endif } } JsonObject wifi_info = root.createNestedObject(F("wifi")); wifi_info[F("bssid")] = WiFi.BSSIDstr(); int qrssi = WiFi.RSSI(); wifi_info[F("rssi")] = qrssi; wifi_info[F("signal")] = getSignalQuality(qrssi); wifi_info[F("channel")] = WiFi.channel(); wifi_info[F("ap")] = apActive; JsonObject fs_info = root.createNestedObject("fs"); fs_info["u"] = fsBytesUsed / 1000; fs_info["t"] = fsBytesTotal / 1000; fs_info[F("pmt")] = presetsModifiedTime; root[F("ndc")] = nodeListEnabled ? (int)Nodes.size() : -1; #ifdef ARDUINO_ARCH_ESP32 #ifdef WLED_DEBUG wifi_info[F("txPower")] = (int) WiFi.getTxPower(); wifi_info[F("sleep")] = (bool) WiFi.getSleep(); #endif #if defined(ARDUINO_ARCH_ESP32) && defined(CONFIG_IDF_TARGET_ESP32) // classic esp32 only: report "esp32" without package details root[F("arch")] = "esp32"; #else root[F("arch")] = ESP.getChipModel(); #endif root[F("core")] = ESP.getSdkVersion(); root[F("clock")] = ESP.getCpuFreqMHz(); root[F("flash")] = (ESP.getFlashChipSize()/1024)/1024; #ifdef WLED_DEBUG root[F("maxalloc")] = getContiguousFreeHeap(); root[F("resetReason0")] = (int)rtc_get_reset_reason(0); root[F("resetReason1")] = (int)rtc_get_reset_reason(1); #endif root[F("lwip")] = 0; //deprecated #ifndef WLED_DISABLE_OTA root[F("bootloaderSHA256")] = getBootloaderSHA256Hex(); #endif #else root[F("arch")] = "esp8266"; root[F("core")] = ESP.getCoreVersion(); root[F("clock")] = ESP.getCpuFreqMHz(); root[F("flash")] = (ESP.getFlashChipSize()/1024)/1024; #ifdef WLED_DEBUG root[F("maxalloc")] = getContiguousFreeHeap(); root[F("resetReason")] = (int)ESP.getResetInfoPtr()->reason; #endif root[F("lwip")] = LWIP_VERSION_MAJOR; #endif root[F("freeheap")] = getFreeHeapSize(); #if defined(ARDUINO_ARCH_ESP32) && defined(BOARD_HAS_PSRAM) // Report PSRAM information // Free PSRAM in bytes (backward compatibility) root[F("psram")] = ESP.getFreePsram(); // Total PSRAM size in MB, round up to correct for allocator overhead root[F("psrSz")] = (ESP.getPsramSize() + (1024U * 1024U - 1)) / (1024U * 1024U); #endif root[F("uptime")] = millis()/1000 + rolloverMillis*4294967; char time[32]; getTimeString(time); root[F("time")] = time; UsermodManager::addToJsonInfo(root); uint16_t os = 0; #ifdef WLED_DEBUG os = 0x80; #ifdef WLED_DEBUG_HOST os |= 0x0100; if (!netDebugEnabled) os &= ~0x0080; #endif #endif #ifndef WLED_DISABLE_ALEXA os += 0x40; #endif //os += 0x20; // indicated now removed Blynk support, may be reused to indicate another build-time option #ifdef USERMOD_CRONIXIE os += 0x10; #endif #ifndef WLED_DISABLE_FILESYSTEM os += 0x08; #endif #ifndef WLED_DISABLE_HUESYNC os += 0x04; #endif #ifdef WLED_ENABLE_ADALIGHT os += 0x02; #endif #ifndef WLED_DISABLE_OTA os += 0x01; #endif root[F("opt")] = os; root[F("brand")] = F(WLED_BRAND); root[F("product")] = F(WLED_PRODUCT_NAME); root["mac"] = escapedMac; char s[16] = ""; if (Network.isConnected()) { IPAddress localIP = Network.localIP(); sprintf(s, "%d.%d.%d.%d", localIP[0], localIP[1], localIP[2], localIP[3]); } root["ip"] = s; } static void setPaletteColors(JsonArray json, CRGBPalette16 palette) { for (int i = 0; i < 16; i++) { JsonArray colors = json.createNestedArray(); CRGB color = palette[i]; colors.add(i<<4); colors.add(color.red); colors.add(color.green); colors.add(color.blue); } } static void setPaletteColors(JsonArray json, byte* tcp) { TRGBGradientPaletteEntryUnion* ent = (TRGBGradientPaletteEntryUnion*)(tcp); TRGBGradientPaletteEntryUnion u; // Count entries unsigned count = 0; do { u = *(ent + count); count++; } while ( u.index != 255); u = *ent; int indexstart = 0; while( indexstart < 255) { indexstart = u.index; JsonArray colors = json.createNestedArray(); colors.add(u.index); colors.add(u.r); colors.add(u.g); colors.add(u.b); ent++; u = *ent; } } void serializePalettes(JsonObject root, int page) { byte tcp[72]; #ifdef ESP8266 constexpr int itemPerPage = 5; #else constexpr int itemPerPage = 8; #endif const int customPalettesCount = customPalettes.size(); const int palettesCount = FIXED_PALETTE_COUNT; // palettesCount is number of palettes, not palette index const int maxPage = (palettesCount + customPalettesCount) / itemPerPage; if (page > maxPage) page = maxPage; const int start = itemPerPage * page; int end = min(start + itemPerPage, palettesCount + customPalettesCount); root[F("m")] = maxPage; // inform caller how many pages there are JsonObject palettes = root.createNestedObject("p"); for (int i = start; i < end; i++) { JsonArray curPalette = palettes.createNestedArray(String(i >= palettesCount ? 255 - i + palettesCount : i)); switch (i) { case 0: //default palette setPaletteColors(curPalette, PartyColors_p); break; case 1: //random for (int j = 0; j < 4; j++) curPalette.add("r"); break; case 2: //primary color only curPalette.add("c1"); break; case 3: //primary + secondary curPalette.add("c1"); curPalette.add("c1"); curPalette.add("c2"); curPalette.add("c2"); break; case 4: //primary + secondary + tertiary curPalette.add("c3"); curPalette.add("c2"); curPalette.add("c1"); break; case 5: //primary + secondary (+tertiary if not off), more distinct for (int j = 0; j < 5; j++) curPalette.add("c1"); for (int j = 0; j < 5; j++) curPalette.add("c2"); for (int j = 0; j < 5; j++) curPalette.add("c3"); curPalette.add("c1"); break; default: if (i >= palettesCount) // custom palettes setPaletteColors(curPalette, customPalettes[i - palettesCount]); else if (i < DYNAMIC_PALETTE_COUNT + FASTLED_PALETTE_COUNT) // palette 6 - 12, fastled palettes setPaletteColors(curPalette, *fastledPalettes[i - DYNAMIC_PALETTE_COUNT]); else { memcpy_P(tcp, (byte*)pgm_read_dword(&(gGradientPalettes[i - (DYNAMIC_PALETTE_COUNT + FASTLED_PALETTE_COUNT)])), sizeof(tcp)); setPaletteColors(curPalette, tcp); } break; } } } void serializeNetworks(JsonObject root) { JsonArray networks = root.createNestedArray(F("networks")); int16_t status = WiFi.scanComplete(); switch (status) { case WIFI_SCAN_FAILED: WiFi.scanNetworks(true); return; case WIFI_SCAN_RUNNING: return; } for (int i = 0; i < status; i++) { JsonObject node = networks.createNestedObject(); node[F("ssid")] = WiFi.SSID(i); node[F("rssi")] = WiFi.RSSI(i); node[F("bssid")] = WiFi.BSSIDstr(i); node[F("channel")] = WiFi.channel(i); node[F("enc")] = WiFi.encryptionType(i); } WiFi.scanDelete(); if (WiFi.scanComplete() == WIFI_SCAN_FAILED) { WiFi.scanNetworks(true); } } void serializeNodes(JsonObject root) { JsonArray nodes = root.createNestedArray("nodes"); for (NodesMap::iterator it = Nodes.begin(); it != Nodes.end(); ++it) { if (it->second.ip[0] != 0) { JsonObject node = nodes.createNestedObject(); node[F("name")] = it->second.nodeName; node["type"] = it->second.nodeType; node["ip"] = it->second.ip.toString(); node[F("age")] = it->second.age; node[F("vid")] = it->second.build; } } } void serializePins(JsonObject root) { JsonArray pins = root.createNestedArray(F("pins")); #ifdef ESP8266 constexpr int ENUM_PINS = WLED_NUM_PINS; // GPIO0-16 (A0 (17) is analog input only and always assigned to any analog input, even if set "unused") TODO: can currently not be handled #else constexpr int ENUM_PINS = WLED_NUM_PINS; #endif for (int gpio = 0; gpio < ENUM_PINS; gpio++) { bool canInput = PinManager::isPinOk(gpio, false); bool canOutput = PinManager::isPinOk(gpio, true); bool isAllocated = PinManager::isPinAllocated(gpio); // Skip pins that are neither usable nor allocated (truly unusable pins) if (!canInput && !canOutput && !isAllocated) continue; JsonObject pinObj = pins.createNestedObject(); pinObj["p"] = gpio; // pin number // Pin capabilities // Touch capability is provided by appendGPIOinfo() via d.touch uint8_t caps = 0; #ifdef ARDUINO_ARCH_ESP32 if (PinManager::isAnalogPin(gpio)) caps |= PIN_CAP_ADC; // PWM on all ESP32 variants: all output pins can use ledc PWM so this is redundant //if (canOutput) caps |= PIN_CAP_PWM; // Input-only pins (ESP32 classic: GPIO34-39) if (canInput && !canOutput) caps |= PIN_CAP_INPUT_ONLY; // Bootloader/strapping pins #if defined(CONFIG_IDF_TARGET_ESP32S3) if (gpio == 0) caps |= PIN_CAP_BOOT; // pull low to enter bootloader mode if (gpio == 45 || gpio == 46) caps |= PIN_CAP_BOOTSTRAP; // IO46 must be low to enter bootloader mode, IO45 controls flash voltage, keep low for 3.3V flash #elif defined(CONFIG_IDF_TARGET_ESP32S2) if (gpio == 0) caps |= PIN_CAP_BOOT; // pull low to enter bootloader mode if (gpio == 45 || gpio == 46) caps |= PIN_CAP_BOOTSTRAP; // IO46 must be low to enter bootloader mode, IO45 controls flash voltage, keep low for 3.3V flash #elif defined(CONFIG_IDF_TARGET_ESP32C3) if (gpio == 9) caps |= PIN_CAP_BOOT; // pull low to enter bootloader mode if (gpio == 2 || gpio == 8) caps |= PIN_CAP_BOOTSTRAP; // both GPIO2 and GPIO8 must be high to enter bootloader mode #elif defined(CONFIG_IDF_TARGET_ESP32) // ESP32 classic if (gpio == 0) caps |= PIN_CAP_BOOT; // pull low to enter bootloader mode if (gpio == 2 || gpio == 12) caps |= PIN_CAP_BOOTSTRAP; // note: if GPIO12 must be low at boot, (high=1.8V flash mode), GPIO 2 must be low or floating to enter bootloader mode #endif #else // ESP8266: GPIO 0-16 + GPIO17=A0 // if (gpio < 16) caps |= PIN_CAP_PWM; // software PWM available on all GPIO except GPIO16 // ESP8266 strapping pins if (gpio == 0) caps |= PIN_CAP_BOOT; if (gpio == 2 || gpio == 15) caps |= PIN_CAP_BOOTSTRAP; // GPIO2 must be high, GPIO15 low to boot normally if (gpio == 17) caps = PIN_CAP_INPUT_ONLY | PIN_CAP_ADC; // TODO: display as A0 pin #endif pinObj["c"] = caps; // capabilities // Add allocated status and owner pinObj["a"] = isAllocated; // allocated status // check if this pin is used as a button (need to get button type for owner name) int buttonIndex = PinManager::getButtonIndex(gpio); // returns -1 if not a button pin, otherwise returns index in buttons array // Add owner ID and name PinOwner owner = PinManager::getPinOwner(gpio); if (isAllocated) { pinObj["o"] = static_cast(owner); // owner ID (can be used for UI lookup) pinObj["n"] = PinManager::getPinOwnerName(gpio); // owner name (string) // Relay pin if (owner == PinOwner::Relay) { pinObj["m"] = 1; // mode: output pinObj["s"] = digitalRead(rlyPin); // read state from hardware (digitalRead returns output state for output pins) } // Button pins, get type and state using isButtonPressed() else if (buttonIndex >= 0) { pinObj["m"] = 0; // mode: input pinObj["t"] = buttons[buttonIndex].type; // button type pinObj["s"] = isButtonPressed(buttonIndex) ? 1 : 0; // state // for touch buttons, get raw reading value (useful for debugging threshold) #if defined(CONFIG_IDF_TARGET_ESP32) || defined(CONFIG_IDF_TARGET_ESP32S2) || defined(CONFIG_IDF_TARGET_ESP32S3) if (buttons[buttonIndex].type == BTN_TYPE_TOUCH || buttons[buttonIndex].type == BTN_TYPE_TOUCH_SWITCH) { if (digitalPinToTouchChannel(gpio) >= 0) { #ifdef SOC_TOUCH_VERSION_2 // ESP32 S2 and S3 pinObj["r"] = touchRead(gpio) >> 4; // Touch V2 returns larger values, right shift by 4 to match threshold range, see set.cpp #else pinObj["r"] = touchRead(gpio); // send raw value #endif } } #endif // for analog buttons, get raw reading value if (buttons[buttonIndex].type == BTN_TYPE_ANALOG || buttons[buttonIndex].type == BTN_TYPE_ANALOG_INVERTED) { int analogRaw = 0; #ifdef ESP8266 analogRaw = analogRead(A0) >> 2; // convert 10bit read to 8bit, ESP8266 only has one analog pin #else if (digitalPinToAnalogChannel(gpio) >= 0) { analogRaw = (analogRead(gpio)>>4); // right shift to match button value (8bit) see button.cpp } #endif if (buttons[buttonIndex].type == BTN_TYPE_ANALOG_INVERTED) analogRaw = 255 - analogRaw; pinObj["r"] = analogRaw; // send raw value } } // other allocated output pins that are simple GPIO (BusOnOff, Multi Relay, etc.) TODO: expand for other pin owners as needed else if (owner == PinOwner::BusOnOff || owner == PinOwner::UM_MultiRelay) { pinObj["m"] = 1; // mode: output pinObj["s"] = digitalRead(gpio); // read state from hardware (digitalRead returns output state for output pins) } } } } // deserializes mode data string into JsonArray void serializeModeData(JsonArray fxdata) { char lineBuffer[256]; for (size_t i = 0; i < strip.getModeCount(); i++) { strncpy_P(lineBuffer, strip.getModeData(i), sizeof(lineBuffer)/sizeof(char)-1); lineBuffer[sizeof(lineBuffer)/sizeof(char)-1] = '\0'; // terminate string if (lineBuffer[0] != 0) { char* dataPtr = strchr(lineBuffer,'@'); if (dataPtr) fxdata.add(dataPtr+1); else fxdata.add(""); } } } // deserializes mode names string into JsonArray // also removes effect data extensions (@...) from deserialised names void serializeModeNames(JsonArray arr) { char lineBuffer[256]; for (size_t i = 0; i < strip.getModeCount(); i++) { strncpy_P(lineBuffer, strip.getModeData(i), sizeof(lineBuffer)/sizeof(char)-1); lineBuffer[sizeof(lineBuffer)/sizeof(char)-1] = '\0'; // terminate string if (lineBuffer[0] != 0) { char* dataPtr = strchr(lineBuffer,'@'); if (dataPtr) *dataPtr = 0; // terminate mode data after name arr.add(lineBuffer); } } } // Global buffer locking response helper class (to make sure lock is released when AsyncJsonResponse is destroyed) class LockedJsonResponse: public AsyncJsonResponse { bool _holding_lock; public: // WARNING: constructor assumes requestJSONBufferLock() was successfully acquired externally/prior to constructing the instance // Not a good practice with C++. Unfortunately AsyncJsonResponse only has 2 constructors - for dynamic buffer or existing buffer, // with existing buffer it clears its content during construction // if the lock was not acquired (using JSONBufferGuard class) previous implementation still cleared existing buffer inline LockedJsonResponse(JsonDocument* doc, bool isArray) : AsyncJsonResponse(doc, isArray), _holding_lock(true) {}; virtual size_t _fillBuffer(uint8_t *buf, size_t maxLen) { size_t result = AsyncJsonResponse::_fillBuffer(buf, maxLen); // Release lock as soon as we're done filling content if (((result + _sentLength) >= (_contentLength)) && _holding_lock) { releaseJSONBufferLock(); _holding_lock = false; } return result; } // destructor will remove JSON buffer lock when response is destroyed in AsyncWebServer virtual ~LockedJsonResponse() { if (_holding_lock) releaseJSONBufferLock(); }; }; void serveJson(AsyncWebServerRequest* request) { enum class json_target { all, state, info, state_info, nodes, effects, palettes, fxdata, networks, config, pins }; json_target subJson = json_target::all; const String& url = request->url(); if (url.indexOf("state") > 0) subJson = json_target::state; else if (url.indexOf("info") > 0) subJson = json_target::info; else if (url.indexOf("si") > 0) subJson = json_target::state_info; else if (url.indexOf(F("nodes")) > 0) subJson = json_target::nodes; else if (url.indexOf(F("eff")) > 0) subJson = json_target::effects; else if (url.indexOf(F("palx")) > 0) subJson = json_target::palettes; else if (url.indexOf(F("fxda")) > 0) subJson = json_target::fxdata; else if (url.indexOf(F("net")) > 0) subJson = json_target::networks; else if (url.indexOf(F("cfg")) > 0) subJson = json_target::config; else if (url.indexOf(F("pins")) > 0) subJson = json_target::pins; #ifdef WLED_ENABLE_JSONLIVE else if (url.indexOf("live") > 0) { serveLiveLeds(request); return; } #endif else if (url.indexOf("pal") > 0) { request->send_P(200, FPSTR(CONTENT_TYPE_JSON), JSON_palette_names); return; } else if (url.length() > 6) { //not just /json serveJsonError(request, 501, ERR_NOT_IMPL); return; } if (!requestJSONBufferLock(JSON_LOCK_SERVEJSON)) { request->deferResponse(); return; } // releaseJSONBufferLock() will be called when "response" is destroyed (from AsyncWebServer) // make sure you delete "response" if no "request->send(response);" is made LockedJsonResponse *response = new LockedJsonResponse(pDoc, subJson==json_target::fxdata || subJson==json_target::effects); // will clear and convert JsonDocument into JsonArray if necessary JsonVariant lDoc = response->getRoot(); switch (subJson) { case json_target::state: serializeState(lDoc); break; case json_target::info: serializeInfo(lDoc); break; case json_target::nodes: serializeNodes(lDoc); break; case json_target::palettes: serializePalettes(lDoc, request->hasParam(F("page")) ? request->getParam(F("page"))->value().toInt() : 0); break; case json_target::effects: serializeModeNames(lDoc); break; case json_target::fxdata: serializeModeData(lDoc); break; case json_target::networks: serializeNetworks(lDoc); break; case json_target::config: serializeConfig(lDoc); break; case json_target::pins: serializePins(lDoc); break; case json_target::state_info: case json_target::all: JsonObject state = lDoc.createNestedObject("state"); serializeState(state); JsonObject info = lDoc.createNestedObject("info"); serializeInfo(info); if (subJson == json_target::all) { JsonArray effects = lDoc.createNestedArray(F("effects")); serializeModeNames(effects); // remove WLED-SR extensions from effect names lDoc[F("palettes")] = serialized((const __FlashStringHelper*)JSON_palette_names); } //lDoc["m"] = lDoc.memoryUsage(); // JSON buffer usage, for remote debugging } DEBUG_PRINTF_P(PSTR("JSON buffer size: %u for request: %d\n"), lDoc.memoryUsage(), subJson); [[maybe_unused]] size_t len = response->setLength(); DEBUG_PRINTF_P(PSTR("JSON content length: %u\n"), len); request->send(response); } #ifdef WLED_ENABLE_JSONLIVE #define MAX_LIVE_LEDS 256 bool serveLiveLeds(AsyncWebServerRequest* request, uint32_t wsClient) { #ifdef WLED_ENABLE_WEBSOCKETS AsyncWebSocketClient * wsc = nullptr; if (!request) { //not HTTP, use Websockets wsc = ws.client(wsClient); if (!wsc || wsc->queueLength() > 0) return false; //only send if queue free } #endif unsigned used = strip.getLengthTotal(); unsigned n = (used -1) /MAX_LIVE_LEDS +1; //only serve every n'th LED if count over MAX_LIVE_LEDS #ifndef WLED_DISABLE_2D if (strip.isMatrix) { // ignore anything behid matrix (i.e. extra strip) used = Segment::maxWidth*Segment::maxHeight; // always the size of matrix (more or less than strip.getLengthTotal()) n = 1; if (used > MAX_LIVE_LEDS) n = 2; if (used > MAX_LIVE_LEDS*4) n = 4; } #endif DynamicBuffer buffer(9 + (9*(1+(used/n))) + 7 + 5 + 6 + 5 + 6 + 5 + 2); char* buf = buffer.data(); // assign buffer for oappnd() functions strncpy_P(buffer.data(), PSTR("{\"leds\":["), buffer.size()); buf += 9; // sizeof(PSTR()) from last line for (size_t i = 0; i < used; i += n) { #ifndef WLED_DISABLE_2D if (strip.isMatrix && n>1 && (i/Segment::maxWidth)%n) i += Segment::maxWidth * (n-1); #endif uint32_t c = strip.getPixelColor(i); uint8_t r = R(c); uint8_t g = G(c); uint8_t b = B(c); uint8_t w = W(c); r = scale8(qadd8(w, r), strip.getBrightness()); //R, add white channel to RGB channels as a simple RGBW -> RGB map g = scale8(qadd8(w, g), strip.getBrightness()); //G b = scale8(qadd8(w, b), strip.getBrightness()); //B buf += sprintf_P(buf, PSTR("\"%06X\","), RGBW32(r,g,b,0)); } buf--; // remove last comma buf += sprintf_P(buf, PSTR("],\"n\":%d"), n); #ifndef WLED_DISABLE_2D if (strip.isMatrix) { buf += sprintf_P(buf, PSTR(",\"w\":%d"), Segment::maxWidth/n); buf += sprintf_P(buf, PSTR(",\"h\":%d"), Segment::maxHeight/n); } #endif (*buf++) = '}'; (*buf++) = 0; if (request) { request->send(200, FPSTR(CONTENT_TYPE_JSON), toString(std::move(buffer))); } #ifdef WLED_ENABLE_WEBSOCKETS else { wsc->text(toString(std::move(buffer))); } #endif return true; } #endif ================================================ FILE: wled00/led.cpp ================================================ #include "wled.h" /* * LED methods */ // applies chosen setment properties to legacy values void setValuesFromSegment(uint8_t s) { const Segment& seg = strip.getSegment(s); colPri[0] = R(seg.colors[0]); colPri[1] = G(seg.colors[0]); colPri[2] = B(seg.colors[0]); colPri[3] = W(seg.colors[0]); colSec[0] = R(seg.colors[1]); colSec[1] = G(seg.colors[1]); colSec[2] = B(seg.colors[1]); colSec[3] = W(seg.colors[1]); effectCurrent = seg.mode; effectSpeed = seg.speed; effectIntensity = seg.intensity; effectPalette = seg.palette; } // applies global legacy values (colPri, colSec, effectCurrent...) to each selected segment void applyValuesToSelectedSegs() { for (unsigned i = 0; i < strip.getSegmentsNum(); i++) { Segment& seg = strip.getSegment(i); if (!(seg.isActive() && seg.isSelected())) continue; if (effectSpeed != seg.speed) {seg.speed = effectSpeed; stateChanged = true;} if (effectIntensity != seg.intensity) {seg.intensity = effectIntensity; stateChanged = true;} if (effectPalette != seg.palette) {seg.setPalette(effectPalette);} if (effectCurrent != seg.mode) {seg.setMode(effectCurrent);} uint32_t col0 = RGBW32(colPri[0], colPri[1], colPri[2], colPri[3]); uint32_t col1 = RGBW32(colSec[0], colSec[1], colSec[2], colSec[3]); if (col0 != seg.colors[0]) {seg.setColor(0, col0);} if (col1 != seg.colors[1]) {seg.setColor(1, col1);} } } void toggleOnOff() { if (bri == 0) { bri = briLast; strip.restartRuntime(); } else { briLast = bri; bri = 0; } stateChanged = true; } //scales the brightness with the briMultiplier factor byte scaledBri(byte in) { unsigned val = ((unsigned)in*briMultiplier)/100; if (val > 255) val = 255; return (byte)val; } //applies global temporary brightness (briT) to strip void applyBri() { if (realtimeOverride || !(realtimeMode && arlsForceMaxBri)) { //DEBUG_PRINTF_P(PSTR("Applying strip brightness: %d (%d,%d)\n"), (int)briT, (int)bri, (int)briOld); strip.setBrightness(briT); } } //applies global brightness and sets it as the "current" brightness (no transition) void applyFinalBri() { briOld = bri; briT = bri; applyBri(); strip.trigger(); // force one last update } //called after every state changes, schedules interface updates, handles brightness transition and nightlight activation //unlike colorUpdated(), does NOT apply any colors or FX to segments void stateUpdated(byte callMode) { //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 11: ws send only 12: button preset setValuesFromFirstSelectedSeg(); // a much better approach would be to use main segment: setValuesFromMainSeg() if (bri != briOld || stateChanged) { if (stateChanged) currentPreset = 0; //something changed, so we are no longer in the preset if (callMode != CALL_MODE_NOTIFICATION && callMode != CALL_MODE_NO_NOTIFY) notify(callMode); if (bri != briOld && nodeBroadcastEnabled) sendSysInfoUDP(); // update on state //set flag to update ws and mqtt interfaceUpdateCallMode = callMode; } else { if (nightlightActive && !nightlightActiveOld && callMode != CALL_MODE_NOTIFICATION && callMode != CALL_MODE_NO_NOTIFY) { notify(CALL_MODE_NIGHTLIGHT); interfaceUpdateCallMode = CALL_MODE_NIGHTLIGHT; } } unsigned long now = millis(); if (callMode != CALL_MODE_NO_NOTIFY && nightlightActive && (nightlightMode == NL_MODE_FADE || nightlightMode == NL_MODE_COLORFADE)) { briNlT = bri; nightlightDelayMs -= (now - nightlightStartTime); nightlightStartTime = now; } if (briT == 0) { if (callMode != CALL_MODE_NOTIFICATION) strip.resetTimebase(); //effect start from beginning } if (bri > 0) briLast = bri; //deactivate nightlight if target brightness is reached if (bri == nightlightTargetBri && callMode != CALL_MODE_NO_NOTIFY && nightlightMode != NL_MODE_SUN) nightlightActive = false; // notify usermods of state change UsermodManager::onStateChange(callMode); if (strip.getTransition() == 0) { jsonTransitionOnce = false; transitionActive = false; applyFinalBri(); strip.trigger(); } else { if (transitionActive) { briOld = briT; } else if (bri != briOld || stateChanged) strip.setTransitionMode(true); // force all segments to transition mode transitionActive = true; transitionStartTime = now; } stateChanged = false; } void updateInterfaces(uint8_t callMode) { if (!interfaceUpdateCallMode || millis() - lastInterfaceUpdate < INTERFACE_UPDATE_COOLDOWN) return; sendDataWs(); lastInterfaceUpdate = millis(); interfaceUpdateCallMode = CALL_MODE_INIT; //disable further updates if (callMode == CALL_MODE_WS_SEND) return; #ifndef WLED_DISABLE_ALEXA if (espalexaDevice != nullptr && callMode != CALL_MODE_ALEXA) { espalexaDevice->setValue(bri); espalexaDevice->setColor(colPri[0], colPri[1], colPri[2]); } #endif #ifndef WLED_DISABLE_MQTT publishMqtt(); #endif } void handleTransitions() { //handle still pending interface update updateInterfaces(interfaceUpdateCallMode); if (transitionActive && strip.getTransition() > 0) { int ti = millis() - transitionStartTime; int tr = strip.getTransition(); if (ti/tr) { strip.setTransitionMode(false); // stop all transitions // restore (global) transition time if not called from UDP notifier or single/temporary transition from JSON (also playlist) if (jsonTransitionOnce) strip.setTransition(transitionDelay); transitionActive = false; jsonTransitionOnce = false; applyFinalBri(); return; } byte briTO = briT; int deltaBri = (int)bri - (int)briOld; briT = briOld + (deltaBri * ti / tr); if (briTO != briT) applyBri(); } } // legacy method, applies values from col, effectCurrent, ... to selected segments void colorUpdated(byte callMode) { applyValuesToSelectedSegs(); stateUpdated(callMode); } void handleNightlight() { unsigned long now = millis(); if (now < 100 && lastNlUpdate > 0) lastNlUpdate = 0; // take care of millis() rollover if (now - lastNlUpdate < 100) return; // allow only 10 NL updates per second lastNlUpdate = now; if (nightlightActive) { if (!nightlightActiveOld) //init { nightlightStartTime = millis(); nightlightDelayMs = (unsigned)(nightlightDelayMins*60000); nightlightActiveOld = true; briNlT = bri; for (unsigned i=0; i<4; i++) colNlT[i] = colPri[i]; // remember starting color if (nightlightMode == NL_MODE_SUN) { //save current colNlT[0] = effectCurrent; colNlT[1] = effectSpeed; colNlT[2] = effectPalette; strip.getFirstSelectedSeg().setMode(FX_MODE_STATIC); // make sure seg runtime is reset if it was in sunrise mode effectCurrent = FX_MODE_SUNRISE; // colorUpdated() will take care of assigning that to all selected segments effectSpeed = nightlightDelayMins; effectPalette = 0; if (effectSpeed > 60) effectSpeed = 60; //currently limited to 60 minutes if (bri) effectSpeed += 60; //sunset if currently on briNlT = !bri; //true == sunrise, false == sunset if (!bri) bri = briLast; colorUpdated(CALL_MODE_NO_NOTIFY); } } float nper = (millis() - nightlightStartTime)/((float)nightlightDelayMs); if (nightlightMode == NL_MODE_FADE || nightlightMode == NL_MODE_COLORFADE) { bri = briNlT + ((nightlightTargetBri - briNlT)*nper); if (nightlightMode == NL_MODE_COLORFADE) // color fading only is enabled with "NF=2" { for (unsigned i=0; i<4; i++) colPri[i] = colNlT[i]+ ((colSec[i] - colNlT[i])*nper); // fading from actual color to secondary color } colorUpdated(CALL_MODE_NO_NOTIFY); } if (nper >= 1) //nightlight duration over { nightlightActive = false; if (nightlightMode == NL_MODE_SET) { bri = nightlightTargetBri; colorUpdated(CALL_MODE_NO_NOTIFY); } if (bri == 0) briLast = briNlT; if (nightlightMode == NL_MODE_SUN) { if (!briNlT) { //turn off if sunset effectCurrent = colNlT[0]; effectSpeed = colNlT[1]; effectPalette = colNlT[2]; toggleOnOff(); applyFinalBri(); } } if (macroNl > 0) applyPreset(macroNl); nightlightActiveOld = false; } } else if (nightlightActiveOld) //early de-init { if (nightlightMode == NL_MODE_SUN) { //restore previous effect effectCurrent = colNlT[0]; effectSpeed = colNlT[1]; effectPalette = colNlT[2]; colorUpdated(CALL_MODE_NO_NOTIFY); } nightlightActiveOld = false; } } //utility for FastLED to use our custom timer uint32_t get_millisecond_timer() { return strip.now; } ================================================ FILE: wled00/lx_parser.cpp ================================================ #include "wled.h" #ifdef WLED_ENABLE_LOXONE /* * Parser for Loxone formats */ bool parseLx(int lxValue, byte* rgbw) { DEBUG_PRINT(F("LX: Lox = ")); DEBUG_PRINTLN(lxValue); bool ok = false; float lxRed = 0, lxGreen = 0, lxBlue = 0; if (lxValue < 200000000) { // Loxone RGB ok = true; lxRed = round((lxValue % 1000) * 2.55); lxGreen = round(((lxValue / 1000) % 1000) * 2.55); lxBlue = round(((lxValue / 1000000) % 1000) * 2.55); } else if ((lxValue >= 200000000) && (lxValue <= 201006500)) { // Loxone Lumitech ok = true; float tmpBri = floor((lxValue - 200000000) / 10000); uint16_t ct = (lxValue - 200000000) - (((uint8_t)tmpBri) * 10000); tmpBri *= 2.55f; tmpBri = constrain(tmpBri, 0, 255); colorKtoRGB(ct, rgbw); lxRed = rgbw[0]; lxGreen = rgbw[1]; lxBlue = rgbw[2]; lxRed *= (tmpBri/255); lxGreen *= (tmpBri/255); lxBlue *= (tmpBri/255); } if (ok) { rgbw[0] = (uint8_t) constrain(lxRed, 0, 255); rgbw[1] = (uint8_t) constrain(lxGreen, 0, 255); rgbw[2] = (uint8_t) constrain(lxBlue, 0, 255); rgbw[3] = 0; return true; } return false; } void parseLxJson(int lxValue, byte segId, bool secondary) { if (secondary) { DEBUG_PRINT(F("LY: Lox secondary = ")); } else { DEBUG_PRINT(F("LX: Lox primary = ")); } DEBUG_PRINTLN(lxValue); byte rgbw[] = {0,0,0,0}; if (parseLx(lxValue, rgbw)) { if (bri == 0) { DEBUG_PRINTLN(F("LX: turn on")); toggleOnOff(); } bri = 255; nightlightActive = false; //always disable nightlight when toggling DEBUG_PRINT(F("LX: segment ")); DEBUG_PRINTLN(segId); strip.getSegment(segId).setColor(secondary, RGBW32(rgbw[0], rgbw[1], rgbw[2], rgbw[3])); // legacy values handled as well in json.cpp by stateUpdated() } } #endif ================================================ FILE: wled00/mqtt.cpp ================================================ #include "wled.h" /* * MQTT communication protocol for home automation */ #ifndef WLED_DISABLE_MQTT #define MQTT_KEEP_ALIVE_TIME 60 // contact the MQTT broker every 60 seconds #if MQTT_MAX_TOPIC_LEN > 32 #warning "MQTT topics length > 32 is not recommended for compatibility with usermods!" #endif static const char* sTopicFormat PROGMEM = "%.*s/%s"; // parse payload for brightness, ON/OFF or toggle // briLast is used to remember last brightness value in case of ON/OFF or toggle // bri is set to 0 if payload is "0" or "OFF" or "false" static void parseMQTTBriPayload(char* payload) { if (strstr(payload, "ON") || strstr(payload, "on") || strstr(payload, "true")) {bri = briLast; stateUpdated(CALL_MODE_DIRECT_CHANGE);} else if (strstr(payload, "T" ) || strstr(payload, "t" )) {toggleOnOff(); stateUpdated(CALL_MODE_DIRECT_CHANGE);} else { uint8_t in = strtoul(payload, NULL, 10); if (in == 0 && bri > 0) briLast = bri; bri = in; stateUpdated(CALL_MODE_DIRECT_CHANGE); } } static void onMqttConnect(bool sessionPresent) { //(re)subscribe to required topics char subuf[MQTT_MAX_TOPIC_LEN + 9]; if (mqttDeviceTopic[0] != 0) { mqtt->subscribe(mqttDeviceTopic, 0); snprintf_P(subuf, sizeof(subuf)-1, sTopicFormat, MQTT_MAX_TOPIC_LEN, mqttDeviceTopic, "col"); mqtt->subscribe(subuf, 0); snprintf_P(subuf, sizeof(subuf)-1, sTopicFormat, MQTT_MAX_TOPIC_LEN, mqttDeviceTopic, "api"); mqtt->subscribe(subuf, 0); } if (mqttGroupTopic[0] != 0) { mqtt->subscribe(mqttGroupTopic, 0); snprintf_P(subuf, sizeof(subuf)-1, sTopicFormat, MQTT_MAX_TOPIC_LEN, mqttGroupTopic, "col"); mqtt->subscribe(subuf, 0); snprintf_P(subuf, sizeof(subuf)-1, sTopicFormat, MQTT_MAX_TOPIC_LEN, mqttGroupTopic, "api"); mqtt->subscribe(subuf, 0); } UsermodManager::onMqttConnect(sessionPresent); DEBUG_PRINTLN(F("MQTT ready")); #ifndef USERMOD_SMARTNEST snprintf_P(subuf, sizeof(subuf)-1, sTopicFormat, MQTT_MAX_TOPIC_LEN, mqttDeviceTopic, "status"); mqtt->publish(subuf, 0, true, "online"); // retain message for a LWT #endif publishMqtt(); } static void onMqttMessage(char* topic, char* payload, AsyncMqttClientMessageProperties properties, size_t len, size_t index, size_t total) { static char *payloadStr; DEBUG_PRINTF_P(PSTR("MQTT msg: %s\n"), topic); // paranoia check to avoid npe if no payload if (payload==nullptr) { DEBUG_PRINTLN(F("no payload -> leave")); return; } if (index == 0) { // start (1st partial packet or the only packet) p_free(payloadStr); // release buffer if it exists payloadStr = static_cast(p_malloc(total+1)); // allocate new buffer } if (payloadStr == nullptr) return; // buffer not allocated // copy (partial) packet to buffer and 0-terminate it if it is last packet char* buff = payloadStr + index; memcpy(buff, payload, len); if (index + len >= total) { // at end payloadStr[total] = '\0'; // terminate c style string } else { DEBUG_PRINTLN(F("MQTT partial packet received.")); return; // process next packet } DEBUG_PRINTLN(payloadStr); size_t topicPrefixLen = strlen(mqttDeviceTopic); if (strncmp(topic, mqttDeviceTopic, topicPrefixLen) == 0) { topic += topicPrefixLen; } else { topicPrefixLen = strlen(mqttGroupTopic); if (strncmp(topic, mqttGroupTopic, topicPrefixLen) == 0) { topic += topicPrefixLen; } else { // Non-Wled Topic used here. Probably a usermod subscribed to this topic. UsermodManager::onMqttMessage(topic, payloadStr); p_free(payloadStr); payloadStr = nullptr; return; } } //Prefix is stripped from the topic at this point if (strcmp_P(topic, PSTR("/col")) == 0) { colorFromDecOrHexString(colPri, payloadStr); colorUpdated(CALL_MODE_DIRECT_CHANGE); } else if (strcmp_P(topic, PSTR("/api")) == 0) { if (requestJSONBufferLock(JSON_LOCK_MQTT)) { if (payloadStr[0] == '{') { //JSON API deserializeJson(*pDoc, payloadStr); deserializeState(pDoc->as()); } else { //HTTP API String apireq = "win"; apireq += '&'; // reduce flash string usage apireq += payloadStr; handleSet(nullptr, apireq); } releaseJSONBufferLock(); } } else if (strlen(topic) != 0) { // non standard topic, check with usermods UsermodManager::onMqttMessage(topic, payloadStr); } else { // topmost topic (just wled/MAC) parseMQTTBriPayload(payloadStr); } p_free(payloadStr); payloadStr = nullptr; } // Print adapter for flat buffers namespace { class bufferPrint : public Print { char* _buf; size_t _size, _offset; public: bufferPrint(char* buf, size_t size) : _buf(buf), _size(size), _offset(0) {}; size_t write(const uint8_t *buffer, size_t size) { size = std::min(size, _size - _offset); memcpy(_buf + _offset, buffer, size); _offset += size; return size; } size_t write(uint8_t c) { return this->write(&c, 1); } char* data() const { return _buf; } size_t size() const { return _offset; } size_t capacity() const { return _size; } }; }; // anonymous namespace void publishMqtt() { if (!WLED_MQTT_CONNECTED) return; DEBUG_PRINTLN(F("Publish MQTT")); #ifndef USERMOD_SMARTNEST char s[10]; char subuf[MQTT_MAX_TOPIC_LEN + 16]; sprintf_P(s, PSTR("%u"), bri); snprintf_P(subuf, sizeof(subuf)-1, sTopicFormat, MQTT_MAX_TOPIC_LEN, mqttDeviceTopic, "g"); mqtt->publish(subuf, 0, retainMqttMsg, s); // optionally retain message (#2263) sprintf_P(s, PSTR("#%06X"), (colPri[3] << 24) | (colPri[0] << 16) | (colPri[1] << 8) | (colPri[2])); snprintf_P(subuf, sizeof(subuf)-1, sTopicFormat, MQTT_MAX_TOPIC_LEN, mqttDeviceTopic, "c"); mqtt->publish(subuf, 0, retainMqttMsg, s); // optionally retain message (#2263) snprintf_P(subuf, sizeof(subuf)-1, sTopicFormat, MQTT_MAX_TOPIC_LEN, mqttDeviceTopic, "status"); mqtt->publish(subuf, 0, true, "online"); // retain message for a LWT // TODO: use a DynamicBufferList. Requires a list-read-capable MQTT client API. DynamicBuffer buf(1024); bufferPrint pbuf(buf.data(), buf.size()); XML_response(pbuf); snprintf_P(subuf, sizeof(subuf)-1, sTopicFormat, MQTT_MAX_TOPIC_LEN, mqttDeviceTopic, "v"); mqtt->publish(subuf, 0, retainMqttMsg, buf.data(), pbuf.size()); // optionally retain message (#2263) #endif } //HA autodiscovery was removed in favor of the native integration in HA v0.102.0 bool initMqtt() { if (!mqttEnabled || mqttServer[0] == 0 || !WLED_CONNECTED) return false; if (mqtt == nullptr) { void *ptr = p_malloc(sizeof(AsyncMqttClient)); mqtt = new (ptr) AsyncMqttClient(); // use placement new (into PSRAM), client will never be deleted if (!mqtt) return false; mqtt->onMessage(onMqttMessage); mqtt->onConnect(onMqttConnect); } if (mqtt->connected()) return true; DEBUG_PRINTLN(F("Reconnecting MQTT")); IPAddress mqttIP; if (mqttIP.fromString(mqttServer)) //see if server is IP or domain { mqtt->setServer(mqttIP, mqttPort); } else { #ifdef ARDUINO_ARCH_ESP32 String mqttMDNS = mqttServer; mqttMDNS.toLowerCase(); // make sure we have a lowercase hostname int pos = mqttMDNS.indexOf(F(".local")); if (pos > 0) mqttMDNS.remove(pos); // remove .local domain if present (and anything following it) if (strlen(cmDNS) > 0 && mqttMDNS.length() > 0 && mqttMDNS.indexOf('.') < 0) { // if mDNS is enabled and server does not have domain mqttIP = MDNS.queryHost(mqttMDNS.c_str()); if (mqttIP != IPAddress()) // if MDNS resolved the hostname mqtt->setServer(mqttIP, mqttPort); else mqtt->setServer(mqttServer, mqttPort); } else #endif mqtt->setServer(mqttServer, mqttPort); } mqtt->setClientId(mqttClientID); if (mqttUser[0] && mqttPass[0]) mqtt->setCredentials(mqttUser, mqttPass); #ifndef USERMOD_SMARTNEST snprintf_P(mqttStatusTopic, sizeof(mqttStatusTopic)-1, sTopicFormat, MQTT_MAX_TOPIC_LEN, mqttDeviceTopic, "status"); mqtt->setWill(mqttStatusTopic, 0, true, "offline"); // LWT message #endif mqtt->setKeepAlive(MQTT_KEEP_ALIVE_TIME); mqtt->connect(); return true; } #endif ================================================ FILE: wled00/my_config_sample.h ================================================ #pragma once /* * Welcome! * You can use the file "my_config.h" to make changes to the way WLED is compiled! * It is possible to enable and disable certain features as well as set defaults for some runtime changeable settings. * * How to use: * PlatformIO: Just compile the unmodified code once! The file "my_config.h" will be generated automatically and now you can make your changes. * * ArduinoIDE: Make a copy of this file and name it "my_config.h". Go to wled.h and uncomment "#define WLED_USE_MY_CONFIG" in the top of the file. * * DO NOT make changes to the "my_config_sample.h" file directly! Your changes will not be applied. */ // uncomment to force the compiler to show a warning to confirm that this file is included //#warning **** my_config.h: Settings from this file are honored **** /* Uncomment to use your WIFI settings as defaults //WARNING: this will hardcode these as the default even after a factory reset #define CLIENT_SSID "Your_SSID" #define CLIENT_PASS "Your_Password" */ //#define MAX_LEDS 1500 // Maximum total LEDs. More than 1500 might create a low memory situation on ESP8266. //#define MDNS_NAME "wled" // mDNS hostname, ie: *.local ================================================ FILE: wled00/net_debug.cpp ================================================ #include "wled.h" #ifdef WLED_DEBUG_HOST size_t NetworkDebugPrinter::write(uint8_t c) { if (!WLED_CONNECTED || !netDebugEnabled) return 0; if (!debugPrintHostIP && !debugPrintHostIP.fromString(netDebugPrintHost)) { #ifdef ESP8266 WiFi.hostByName(netDebugPrintHost, debugPrintHostIP, 750); #else #ifdef WLED_USE_ETHERNET ETH.hostByName(netDebugPrintHost, debugPrintHostIP); #else WiFi.hostByName(netDebugPrintHost, debugPrintHostIP); #endif #endif } debugUdp.beginPacket(debugPrintHostIP, netDebugPrintPort); debugUdp.write(c); debugUdp.endPacket(); return 1; } size_t NetworkDebugPrinter::write(const uint8_t *buf, size_t size) { if (!WLED_CONNECTED || buf == nullptr || !netDebugEnabled) return 0; if (!debugPrintHostIP && !debugPrintHostIP.fromString(netDebugPrintHost)) { #ifdef ESP8266 WiFi.hostByName(netDebugPrintHost, debugPrintHostIP, 750); #else #ifdef WLED_USE_ETHERNET ETH.hostByName(netDebugPrintHost, debugPrintHostIP); #else WiFi.hostByName(netDebugPrintHost, debugPrintHostIP); #endif #endif } debugUdp.beginPacket(debugPrintHostIP, netDebugPrintPort); size = debugUdp.write(buf, size); debugUdp.endPacket(); return size; } NetworkDebugPrinter NetDebug; #endif ================================================ FILE: wled00/net_debug.h ================================================ #ifndef WLED_NET_DEBUG_H #define WLED_NET_DEBUG_H #include #include class NetworkDebugPrinter : public Print { private: WiFiUDP debugUdp; // needs to be here otherwise UDP messages get truncated upon destruction IPAddress debugPrintHostIP; public: virtual size_t write(uint8_t c); virtual size_t write(const uint8_t *buf, size_t s); }; // use it on your linux/macOS with: nc -p 7868 -u -l -s extern NetworkDebugPrinter NetDebug; #endif ================================================ FILE: wled00/network.cpp ================================================ #include "wled.h" #include "fcn_declare.h" #include "wled_ethernet.h" #if defined(ARDUINO_ARCH_ESP32) && defined(WLED_USE_ETHERNET) // The following six pins are neither configurable nor // can they be re-assigned through IOMUX / GPIO matrix. // See https://docs.espressif.com/projects/esp-idf/en/latest/esp32/hw-reference/esp32/get-started-ethernet-kit-v1.1.html#ip101gri-phy-interface const managed_pin_type esp32_nonconfigurable_ethernet_pins[WLED_ETH_RSVD_PINS_COUNT] = { { 21, true }, // RMII EMAC TX EN == When high, clocks the data on TXD0 and TXD1 to transmitter { 19, true }, // RMII EMAC TXD0 == First bit of transmitted data { 22, true }, // RMII EMAC TXD1 == Second bit of transmitted data { 25, false }, // RMII EMAC RXD0 == First bit of received data { 26, false }, // RMII EMAC RXD1 == Second bit of received data { 27, true }, // RMII EMAC CRS_DV == Carrier Sense and RX Data Valid }; const ethernet_settings ethernetBoards[] = { // None { }, // WT32-EHT01 // Please note, from my testing only these pins work for LED outputs: // IO2, IO4, IO12, IO14, IO15 // These pins do not appear to work from my testing: // IO35, IO36, IO39 { 1, // eth_address, 16, // eth_power, 23, // eth_mdc, 18, // eth_mdio, ETH_PHY_LAN8720, // eth_type, ETH_CLOCK_GPIO0_IN // eth_clk_mode }, // ESP32-POE { 0, // eth_address, 12, // eth_power, 23, // eth_mdc, 18, // eth_mdio, ETH_PHY_LAN8720, // eth_type, ETH_CLOCK_GPIO17_OUT // eth_clk_mode }, // WESP32 { 0, // eth_address, -1, // eth_power, 16, // eth_mdc, 17, // eth_mdio, ETH_PHY_LAN8720, // eth_type, ETH_CLOCK_GPIO0_IN // eth_clk_mode }, // QuinLed-ESP32-Ethernet { 0, // eth_address, 5, // eth_power, 23, // eth_mdc, 18, // eth_mdio, ETH_PHY_LAN8720, // eth_type, ETH_CLOCK_GPIO17_OUT // eth_clk_mode }, // TwilightLord-ESP32 Ethernet Shield { 0, // eth_address, 5, // eth_power, 23, // eth_mdc, 18, // eth_mdio, ETH_PHY_LAN8720, // eth_type, ETH_CLOCK_GPIO17_OUT // eth_clk_mode }, // ESP3DEUXQuattro { 1, // eth_address, -1, // eth_power, 23, // eth_mdc, 18, // eth_mdio, ETH_PHY_LAN8720, // eth_type, ETH_CLOCK_GPIO17_OUT // eth_clk_mode }, // ESP32-ETHERNET-KIT-VE { 0, // eth_address, 5, // eth_power, 23, // eth_mdc, 18, // eth_mdio, ETH_PHY_IP101, // eth_type, ETH_CLOCK_GPIO0_IN // eth_clk_mode }, // QuinLed-Dig-Octa Brainboard-32-8L and LilyGO-T-ETH-POE { 0, // eth_address, -1, // eth_power, 23, // eth_mdc, 18, // eth_mdio, ETH_PHY_LAN8720, // eth_type, ETH_CLOCK_GPIO17_OUT // eth_clk_mode }, // ABC! WLED Controller V43 + Ethernet Shield & compatible { 1, // eth_address, 5, // eth_power, 23, // eth_mdc, 33, // eth_mdio, ETH_PHY_LAN8720, // eth_type, ETH_CLOCK_GPIO17_OUT // eth_clk_mode }, // Serg74-ESP32 Ethernet Shield { 1, // eth_address, 5, // eth_power, 23, // eth_mdc, 18, // eth_mdio, ETH_PHY_LAN8720, // eth_type, ETH_CLOCK_GPIO17_OUT // eth_clk_mode }, // ESP32-POE-WROVER { 0, // eth_address, 12, // eth_power, 23, // eth_mdc, 18, // eth_mdio, ETH_PHY_LAN8720, // eth_type, ETH_CLOCK_GPIO0_OUT // eth_clk_mode }, // LILYGO T-POE Pro // https://github.com/Xinyuan-LilyGO/LilyGO-T-ETH-Series/blob/master/schematic/T-POE-PRO.pdf { 0, // eth_address, 5, // eth_power, 23, // eth_mdc, 18, // eth_mdio, ETH_PHY_LAN8720, // eth_type, ETH_CLOCK_GPIO0_OUT // eth_clk_mode }, // Gledopto Series With Ethernet { 1, // eth_address, 5, // eth_power, 23, // eth_mdc, 33, // eth_mdio, ETH_PHY_LAN8720, // eth_type, ETH_CLOCK_GPIO0_IN // eth_clk_mode }, }; bool initEthernet() { static bool successfullyConfiguredEthernet = false; if (successfullyConfiguredEthernet) { // DEBUG_PRINTLN(F("initE: ETH already successfully configured, ignoring")); return false; } if (ethernetType == WLED_ETH_NONE) { return false; } if (ethernetType >= WLED_NUM_ETH_TYPES) { DEBUG_PRINTF_P(PSTR("initE: Ignoring attempt for invalid ethernetType (%d)\n"), ethernetType); return false; } DEBUG_PRINTF_P(PSTR("initE: Attempting ETH config: %d\n"), ethernetType); // Ethernet initialization should only succeed once -- else reboot required ethernet_settings es = ethernetBoards[ethernetType]; managed_pin_type pinsToAllocate[10] = { // first six pins are non-configurable esp32_nonconfigurable_ethernet_pins[0], esp32_nonconfigurable_ethernet_pins[1], esp32_nonconfigurable_ethernet_pins[2], esp32_nonconfigurable_ethernet_pins[3], esp32_nonconfigurable_ethernet_pins[4], esp32_nonconfigurable_ethernet_pins[5], { (int8_t)es.eth_mdc, true }, // [6] = MDC is output and mandatory { (int8_t)es.eth_mdio, true }, // [7] = MDIO is bidirectional and mandatory { (int8_t)es.eth_power, true }, // [8] = optional pin, not all boards use { ((int8_t)0xFE), false }, // [9] = replaced with eth_clk_mode, mandatory }; // update the clock pin.... if (es.eth_clk_mode == ETH_CLOCK_GPIO0_IN) { pinsToAllocate[9].pin = 0; pinsToAllocate[9].isOutput = false; } else if (es.eth_clk_mode == ETH_CLOCK_GPIO0_OUT) { pinsToAllocate[9].pin = 0; pinsToAllocate[9].isOutput = true; } else if (es.eth_clk_mode == ETH_CLOCK_GPIO16_OUT) { pinsToAllocate[9].pin = 16; pinsToAllocate[9].isOutput = true; } else if (es.eth_clk_mode == ETH_CLOCK_GPIO17_OUT) { pinsToAllocate[9].pin = 17; pinsToAllocate[9].isOutput = true; } else { DEBUG_PRINTF_P(PSTR("initE: Failing due to invalid eth_clk_mode (%d)\n"), es.eth_clk_mode); return false; } if (!PinManager::allocateMultiplePins(pinsToAllocate, 10, PinOwner::Ethernet)) { DEBUG_PRINTLN(F("initE: Failed to allocate ethernet pins")); return false; } /* For LAN8720 the most correct way is to perform clean reset each time before init applying LOW to power or nRST pin for at least 100 us (please refer to datasheet, page 59) ESP_IDF > V4 implements it (150 us, lan87xx_reset_hw(esp_eth_phy_t *phy) function in /components/esp_eth/src/esp_eth_phy_lan87xx.c, line 280) but ESP_IDF < V4 does not. Lets do it: [not always needed, might be relevant in some EMI situations at startup and for hot resets] */ #if ESP_IDF_VERSION_MAJOR==3 if(es.eth_power>0 && es.eth_type==ETH_PHY_LAN8720) { pinMode(es.eth_power, OUTPUT); digitalWrite(es.eth_power, 0); delayMicroseconds(150); digitalWrite(es.eth_power, 1); delayMicroseconds(10); } #endif if (!ETH.begin( (uint8_t) es.eth_address, (int) es.eth_power, (int) es.eth_mdc, (int) es.eth_mdio, (eth_phy_type_t) es.eth_type, (eth_clock_mode_t) es.eth_clk_mode )) { DEBUG_PRINTLN(F("initE: ETH.begin() failed")); // de-allocate the allocated pins for (managed_pin_type mpt : pinsToAllocate) { PinManager::deallocatePin(mpt.pin, PinOwner::Ethernet); } return false; } // https://github.com/wled/WLED/issues/5247 if (multiWiFi[0].staticIP != (uint32_t)0x00000000 && multiWiFi[0].staticGW != (uint32_t)0x00000000) { ETH.config(multiWiFi[0].staticIP, multiWiFi[0].staticGW, multiWiFi[0].staticSN, dnsAddress); } else { ETH.config(INADDR_NONE, INADDR_NONE, INADDR_NONE); } successfullyConfiguredEthernet = true; DEBUG_PRINTLN(F("initE: *** Ethernet successfully configured! ***")); return true; } #endif //by https://github.com/tzapu/WiFiManager/blob/master/WiFiManager.cpp int getSignalQuality(int rssi) { int quality = 0; if (rssi <= -100) { quality = 0; } else if (rssi >= -50) { quality = 100; } else { quality = 2 * (rssi + 100); } return quality; } void fillMAC2Str(char *str, const uint8_t *mac) { sprintf_P(str, PSTR("%02x%02x%02x%02x%02x%02x"), MAC2STR(mac)); byte nul = 0; for (int i = 0; i < 6; i++) nul |= *mac++; // do we have 0 if (!nul) str[0] = '\0'; // empty string } void fillStr2MAC(uint8_t *mac, const char *str) { for (int i = 0; i < 6; i++) *mac++ = 0; // clear if (!str) return; // null string uint64_t MAC = strtoull(str, nullptr, 16); for (int i = 0; i < 6; i++) { *--mac = MAC & 0xFF; MAC >>= 8; } } // performs asynchronous scan for available networks (which may take couple of seconds to finish) // returns configured WiFi ID with the strongest signal (or default if no configured networks available) int findWiFi(bool doScan) { if (multiWiFi.size() <= 1) { DEBUG_PRINTF_P(PSTR("WiFi: Default SSID (%s) used.\n"), multiWiFi[0].clientSSID); return 0; } int status = WiFi.scanComplete(); // complete scan may take as much as several seconds (usually <6s with not very crowded air) if (doScan || status == WIFI_SCAN_FAILED) { DEBUG_PRINTF_P(PSTR("WiFi: Scan started. @ %lus\n"), millis()/1000); WiFi.scanNetworks(true); // start scanning in asynchronous mode (will delete old scan) } else if (status >= 0) { // status contains number of found networks (including duplicate SSIDs with different BSSID) DEBUG_PRINTF_P(PSTR("WiFi: Found %d SSIDs. @ %lus\n"), status, millis()/1000); int rssi = -9999; int selected = selectedWiFi; for (int o = 0; o < status; o++) { DEBUG_PRINTF_P(PSTR(" SSID: %s (BSSID: %s) RSSI: %ddB\n"), WiFi.SSID(o).c_str(), WiFi.BSSIDstr(o).c_str(), WiFi.RSSI(o)); for (unsigned n = 0; n < multiWiFi.size(); n++) if (!strcmp(WiFi.SSID(o).c_str(), multiWiFi[n].clientSSID)) { bool foundBSSID = memcmp(multiWiFi[n].bssid, WiFi.BSSID(o), 6) == 0; // find the WiFi with the strongest signal (but keep priority of entry if signal difference is not big) if (foundBSSID || (n < selected && WiFi.RSSI(o) > rssi-10) || WiFi.RSSI(o) > rssi) { rssi = foundBSSID ? 0 : WiFi.RSSI(o); // RSSI is only ever negative selected = n; } break; } } DEBUG_PRINTF_P(PSTR("WiFi: Selected SSID: %s RSSI: %ddB\n"), multiWiFi[selected].clientSSID, rssi); return selected; } //DEBUG_PRINT(F("WiFi scan running.")); return status; // scan is still running or there was an error } bool isWiFiConfigured() { return multiWiFi.size() > 1 || (strlen(multiWiFi[0].clientSSID) >= 1 && strcmp_P(multiWiFi[0].clientSSID, PSTR(DEFAULT_CLIENT_SSID)) != 0); } #if defined(ESP8266) #define ARDUINO_EVENT_WIFI_AP_STADISCONNECTED WIFI_EVENT_SOFTAPMODE_STADISCONNECTED #define ARDUINO_EVENT_WIFI_AP_STACONNECTED WIFI_EVENT_SOFTAPMODE_STACONNECTED #define ARDUINO_EVENT_WIFI_STA_GOT_IP WIFI_EVENT_STAMODE_GOT_IP #define ARDUINO_EVENT_WIFI_STA_CONNECTED WIFI_EVENT_STAMODE_CONNECTED #define ARDUINO_EVENT_WIFI_STA_DISCONNECTED WIFI_EVENT_STAMODE_DISCONNECTED #elif defined(ARDUINO_ARCH_ESP32) && !defined(ESP_ARDUINO_VERSION_MAJOR) //ESP_IDF_VERSION_MAJOR==3 // not strictly IDF v3 but Arduino core related #define ARDUINO_EVENT_WIFI_AP_STADISCONNECTED SYSTEM_EVENT_AP_STADISCONNECTED #define ARDUINO_EVENT_WIFI_AP_STACONNECTED SYSTEM_EVENT_AP_STACONNECTED #define ARDUINO_EVENT_WIFI_STA_GOT_IP SYSTEM_EVENT_STA_GOT_IP #define ARDUINO_EVENT_WIFI_STA_CONNECTED SYSTEM_EVENT_STA_CONNECTED #define ARDUINO_EVENT_WIFI_STA_DISCONNECTED SYSTEM_EVENT_STA_DISCONNECTED #define ARDUINO_EVENT_WIFI_AP_START SYSTEM_EVENT_AP_START #define ARDUINO_EVENT_WIFI_AP_STOP SYSTEM_EVENT_AP_STOP #define ARDUINO_EVENT_WIFI_SCAN_DONE SYSTEM_EVENT_SCAN_DONE #define ARDUINO_EVENT_ETH_START SYSTEM_EVENT_ETH_START #define ARDUINO_EVENT_ETH_CONNECTED SYSTEM_EVENT_ETH_CONNECTED #define ARDUINO_EVENT_ETH_DISCONNECTED SYSTEM_EVENT_ETH_DISCONNECTED #endif //handle Ethernet connection event void WiFiEvent(WiFiEvent_t event) { switch (event) { case ARDUINO_EVENT_WIFI_AP_STADISCONNECTED: // AP client disconnected if (--apClients == 0 && isWiFiConfigured()) forceReconnect = true; // no clients reconnect WiFi if awailable DEBUG_PRINTF_P(PSTR("WiFi-E: AP Client Disconnected (%d) @ %lus.\n"), (int)apClients, millis()/1000); break; case ARDUINO_EVENT_WIFI_AP_STACONNECTED: // AP client connected apClients++; DEBUG_PRINTF_P(PSTR("WiFi-E: AP Client Connected (%d) @ %lus.\n"), (int)apClients, millis()/1000); break; case ARDUINO_EVENT_WIFI_STA_GOT_IP: DEBUG_PRINT(F("WiFi-E: IP address: ")); DEBUG_PRINTLN(Network.localIP()); break; case ARDUINO_EVENT_WIFI_STA_CONNECTED: // followed by IDLE and SCAN_DONE DEBUG_PRINTF_P(PSTR("WiFi-E: Connected! @ %lus\n"), millis()/1000); wasConnected = true; break; case ARDUINO_EVENT_WIFI_STA_DISCONNECTED: if (wasConnected && interfacesInited) { DEBUG_PRINTF_P(PSTR("WiFi-E: Disconnected! @ %lus\n"), millis()/1000); if (interfacesInited && multiWiFi.size() > 1 && WiFi.scanComplete() >= 0) { findWiFi(true); // reinit WiFi scan forceReconnect = true; } interfacesInited = false; } break; #ifdef ARDUINO_ARCH_ESP32 case ARDUINO_EVENT_WIFI_SCAN_DONE: // also triggered when connected to selected SSID DEBUG_PRINTLN(F("WiFi-E: SSID scan completed.")); break; case ARDUINO_EVENT_WIFI_AP_START: DEBUG_PRINTLN(F("WiFi-E: AP Started")); break; case ARDUINO_EVENT_WIFI_AP_STOP: DEBUG_PRINTLN(F("WiFi-E: AP Stopped")); break; #if defined(WLED_USE_ETHERNET) case ARDUINO_EVENT_ETH_START: DEBUG_PRINTLN(F("ETH-E: Started")); break; case ARDUINO_EVENT_ETH_CONNECTED: { DEBUG_PRINTLN(F("ETH-E: Connected")); if (!apActive) { WiFi.disconnect(true); // disable WiFi entirely } // convert the "serverDescription" into a valid DNS hostname (alphanumeric) char hostname[64]; prepareHostname(hostname); ETH.setHostname(hostname); showWelcomePage = false; break; } case ARDUINO_EVENT_ETH_DISCONNECTED: DEBUG_PRINTLN(F("ETH-E: Disconnected")); // This doesn't really affect ethernet per se, // as it's only configured once. Rather, it // may be necessary to reconnect the WiFi when // ethernet disconnects, as a way to provide // alternative access to the device. if (interfacesInited && WiFi.scanComplete() >= 0) findWiFi(true); // reinit WiFi scan forceReconnect = true; break; #endif #endif default: DEBUG_PRINTF_P(PSTR("WiFi-E: Event %d\n"), (int)event); break; } } ================================================ FILE: wled00/ntp.cpp ================================================ #include "src/dependencies/timezone/Timezone.h" #include "wled.h" #include "fcn_declare.h" // forward declarations static void sendNTPPacket(); static bool checkNTPResponse(); // WARNING: may cause errors in sunset calculations on ESP8266, see #3400 // building with `-D WLED_USE_REAL_MATH` will prevent those errors at the expense of flash and RAM /* * Acquires time from NTP server */ //#define WLED_DEBUG_NTP #define NTP_SYNC_INTERVAL 42000UL //Get fresh NTP time about twice per day static Timezone* tz; #define TZ_UTC 0 #define TZ_UK 1 #define TZ_EUROPE_CENTRAL 2 #define TZ_EUROPE_EASTERN 3 #define TZ_US_EASTERN 4 #define TZ_US_CENTRAL 5 #define TZ_US_MOUNTAIN 6 #define TZ_US_ARIZONA 7 #define TZ_US_PACIFIC 8 #define TZ_CHINA 9 #define TZ_JAPAN 10 #define TZ_AUSTRALIA_EASTERN 11 #define TZ_NEW_ZEALAND 12 #define TZ_NORTH_KOREA 13 #define TZ_INDIA 14 #define TZ_SASKACHEWAN 15 #define TZ_AUSTRALIA_NORTHERN 16 #define TZ_AUSTRALIA_SOUTHERN 17 #define TZ_HAWAII 18 #define TZ_NOVOSIBIRSK 19 #define TZ_ANCHORAGE 20 #define TZ_MX_CENTRAL 21 #define TZ_PAKISTAN 22 #define TZ_BRASILIA 23 #define TZ_AUSTRALIA_WESTERN 24 #define TZ_COUNT 25 #define TZ_INIT 255 static byte tzCurrent = TZ_INIT; //uninitialized /* C++11 form -- static std::array, TZ_COUNT> TZ_TABLE PROGMEM = {{ */ static const std::pair TZ_TABLE[] PROGMEM = { /* TZ_UTC */ { {Last, Sun, Mar, 1, 0}, // UTC {Last, Sun, Mar, 1, 0} // Same }, /* TZ_UK */ { {Last, Sun, Mar, 1, 60}, //British Summer Time {Last, Sun, Oct, 2, 0} //Standard Time }, /* TZ_EUROPE_CENTRAL */ { {Last, Sun, Mar, 2, 120}, //Central European Summer Time {Last, Sun, Oct, 3, 60} //Central European Standard Time }, /* TZ_EUROPE_EASTERN */ { {Last, Sun, Mar, 3, 180}, //East European Summer Time {Last, Sun, Oct, 4, 120} //East European Standard Time }, /* TZ_US_EASTERN */ { {Second, Sun, Mar, 2, -240}, //EDT = UTC - 4 hours {First, Sun, Nov, 2, -300} //EST = UTC - 5 hours }, /* TZ_US_CENTRAL */ { {Second, Sun, Mar, 2, -300}, //CDT = UTC - 5 hours {First, Sun, Nov, 2, -360} //CST = UTC - 6 hours }, /* TZ_US_MOUNTAIN */ { {Second, Sun, Mar, 2, -360}, //MDT = UTC - 6 hours {First, Sun, Nov, 2, -420} //MST = UTC - 7 hours }, /* TZ_US_ARIZONA */ { {First, Sun, Nov, 2, -420}, //MST = UTC - 7 hours {First, Sun, Nov, 2, -420} //MST = UTC - 7 hours }, /* TZ_US_PACIFIC */ { {Second, Sun, Mar, 2, -420}, //PDT = UTC - 7 hours {First, Sun, Nov, 2, -480} //PST = UTC - 8 hours }, /* TZ_CHINA */ { {Last, Sun, Mar, 1, 480}, //CST = UTC + 8 hours {Last, Sun, Mar, 1, 480} }, /* TZ_JAPAN */ { {Last, Sun, Mar, 1, 540}, //JST = UTC + 9 hours {Last, Sun, Mar, 1, 540} }, /* TZ_AUSTRALIA_EASTERN */ { {First, Sun, Oct, 2, 660}, //AEDT = UTC + 11 hours {First, Sun, Apr, 3, 600} //AEST = UTC + 10 hours }, /* TZ_NEW_ZEALAND */ { {Last, Sun, Sep, 2, 780}, //NZDT = UTC + 13 hours {First, Sun, Apr, 3, 720} //NZST = UTC + 12 hours }, /* TZ_NORTH_KOREA */ { {Last, Sun, Mar, 1, 510}, //Pyongyang Time = UTC + 8.5 hours {Last, Sun, Mar, 1, 510} }, /* TZ_INDIA */ { {Last, Sun, Mar, 1, 330}, //India Standard Time = UTC + 5.5 hours {Last, Sun, Mar, 1, 330} }, /* TZ_SASKACHEWAN */ { {First, Sun, Nov, 2, -360}, //CST = UTC - 6 hours {First, Sun, Nov, 2, -360} }, /* TZ_AUSTRALIA_NORTHERN */ { {First, Sun, Apr, 3, 570}, //ACST = UTC + 9.5 hours {First, Sun, Apr, 3, 570} }, /* TZ_AUSTRALIA_SOUTHERN */ { {First, Sun, Oct, 2, 630}, //ACDT = UTC + 10.5 hours {First, Sun, Apr, 3, 570} //ACST = UTC + 9.5 hours }, /* TZ_HAWAII */ { {Last, Sun, Mar, 1, -600}, //HST = UTC - 10 hours {Last, Sun, Mar, 1, -600} }, /* TZ_NOVOSIBIRSK */ { {Last, Sun, Mar, 1, 420}, //CST = UTC + 7 hours {Last, Sun, Mar, 1, 420} }, /* TZ_ANCHORAGE */ { {Second, Sun, Mar, 2, -480}, //AKDT = UTC - 8 hours {First, Sun, Nov, 2, -540} //AKST = UTC - 9 hours }, /* TZ_MX_CENTRAL */ { {First, Sun, Apr, 2, -360}, //CST = UTC - 6 hours {First, Sun, Apr, 2, -360} }, /* TZ_PAKISTAN */ { {Last, Sun, Mar, 1, 300}, //Pakistan Standard Time = UTC + 5 hours {Last, Sun, Mar, 1, 300} }, /* TZ_BRASILIA */ { {Last, Sun, Mar, 1, -180}, //Brasília Standard Time = UTC - 3 hours {Last, Sun, Mar, 1, -180} }, /* TZ_AUSTRALIA_WESTERN */ { {Last, Sun, Mar, 1, 480}, //AWST = UTC + 8 hours {Last, Sun, Mar, 1, 480} //AWST = UTC + 8 hours (no DST) } }; void updateTimezone() { delete tz; TimeChangeRule tcrDaylight, tcrStandard; auto tz_table_entry = currentTimezone; if (tz_table_entry >= TZ_COUNT) { tz_table_entry = 0; } tzCurrent = currentTimezone; memcpy_P(&tcrDaylight, &TZ_TABLE[tz_table_entry].first, sizeof(tcrDaylight)); memcpy_P(&tcrStandard, &TZ_TABLE[tz_table_entry].second, sizeof(tcrStandard)); tz = new Timezone(tcrDaylight, tcrStandard); } void handleTime() { handleNetworkTime(); toki.millisecond(); toki.setTick(); if (toki.isTick()) //true only in the first loop after a new second started { #ifdef WLED_DEBUG_NTP Serial.print(F("TICK! ")); toki.printTime(toki.getTime()); #endif updateLocalTime(); checkTimers(); checkCountdown(); } } void handleNetworkTime() { if (ntpEnabled && ntpConnected && millis() - ntpLastSyncTime > (1000*NTP_SYNC_INTERVAL) && WLED_CONNECTED) { if (millis() - ntpPacketSentTime > 10000) { #ifdef ARDUINO_ARCH_ESP32 // I had problems using udp.flush() on 8266 while (ntpUdp.parsePacket() > 0) ntpUdp.flush(); // flush any existing packets #endif sendNTPPacket(); ntpPacketSentTime = millis(); } if (checkNTPResponse()) { ntpLastSyncTime = millis(); } } } static void sendNTPPacket() { if (!ntpServerIP.fromString(ntpServerName)) //see if server is IP or domain { #ifdef ESP8266 WiFi.hostByName(ntpServerName, ntpServerIP, 750); #else WiFi.hostByName(ntpServerName, ntpServerIP); #endif } DEBUG_PRINTLN(F("send NTP")); byte pbuf[NTP_PACKET_SIZE]; memset(pbuf, 0, NTP_PACKET_SIZE); pbuf[0] = 0b11100011; // LI, Version, Mode pbuf[1] = 0; // Stratum, or type of clock pbuf[2] = 6; // Polling Interval pbuf[3] = 0xEC; // Peer Clock Precision // 8 bytes of zero for Root Delay & Root Dispersion pbuf[12] = 49; pbuf[13] = 0x4E; pbuf[14] = 49; pbuf[15] = 52; ntpUdp.beginPacket(ntpServerIP, 123); //NTP requests are to port 123 ntpUdp.write(pbuf, NTP_PACKET_SIZE); ntpUdp.endPacket(); } static bool isValidNtpResponse(const byte* ntpPacket) { // Perform a few validity checks on the packet // based on https://github.com/taranais/NTPClient/blob/master/NTPClient.cpp if((ntpPacket[0] & 0b11000000) == 0b11000000) return false; //reject LI=UNSYNC // if((ntpPacket[0] & 0b00111000) >> 3 < 0b100) return false; //reject Version < 4 if((ntpPacket[0] & 0b00000111) != 0b100) return false; //reject Mode != Server if((ntpPacket[1] < 1) || (ntpPacket[1] > 15)) return false; //reject invalid Stratum if( ntpPacket[16] == 0 && ntpPacket[17] == 0 && ntpPacket[18] == 0 && ntpPacket[19] == 0 && ntpPacket[20] == 0 && ntpPacket[21] == 0 && ntpPacket[22] == 0 && ntpPacket[23] == 0) //reject ReferenceTimestamp == 0 return false; return true; } static bool checkNTPResponse() { int cb = ntpUdp.parsePacket(); if (cb < NTP_MIN_PACKET_SIZE) { #ifdef ARDUINO_ARCH_ESP32 // I had problems using udp.flush() on 8266 if (cb > 0) ntpUdp.flush(); // this avoids memory leaks on esp32 #endif return false; } uint32_t ntpPacketReceivedTime = millis(); DEBUG_PRINTF_P(PSTR("NTP recv, l=%d\n"), cb); byte pbuf[NTP_PACKET_SIZE]; ntpUdp.read(pbuf, NTP_PACKET_SIZE); // read the packet into the buffer if (!isValidNtpResponse(pbuf)) return false; // verify we have a valid response to client Toki::Time arrived = toki.fromNTP(pbuf + 32); Toki::Time departed = toki.fromNTP(pbuf + 40); if (departed.sec == 0) return false; //basic half roundtrip estimation uint32_t serverDelay = toki.msDifference(arrived, departed); uint32_t offset = (ntpPacketReceivedTime - ntpPacketSentTime - serverDelay) >> 1; #ifdef WLED_DEBUG_NTP //the time the packet departed the NTP server toki.printTime(departed); #endif toki.adjust(departed, offset); toki.setTime(departed, TOKI_TS_NTP); #ifdef WLED_DEBUG_NTP Serial.print("Arrived: "); toki.printTime(arrived); Serial.print("Time: "); toki.printTime(departed); Serial.print("Roundtrip: "); Serial.println(ntpPacketReceivedTime - ntpPacketSentTime); Serial.print("Offset: "); Serial.println(offset); Serial.print("Serverdelay: "); Serial.println(serverDelay); #endif if (countdownTime - toki.second() > 0) countdownOverTriggered = false; // if time changed re-calculate sunrise/sunset updateLocalTime(); calculateSunriseAndSunset(); return true; } void updateLocalTime() { if (currentTimezone != tzCurrent) updateTimezone(); unsigned long tmc = toki.second()+ utcOffsetSecs; localTime = tz->toLocal(tmc); } void getTimeString(char* out) { updateLocalTime(); byte hr = hour(localTime); if (useAMPM) { if (hr > 11) hr -= 12; if (hr == 0) hr = 12; } sprintf_P(out,PSTR("%i-%i-%i, %02d:%02d:%02d"),year(localTime), month(localTime), day(localTime), hr, minute(localTime), second(localTime)); if (useAMPM) { strcat_P(out,PSTR(" ")); strcat(out,(hour(localTime) > 11)? "PM":"AM"); } } void setCountdown() { if (currentTimezone != tzCurrent) updateTimezone(); countdownTime = tz->toUTC(getUnixTime(countdownHour, countdownMin, countdownSec, countdownDay, countdownMonth, countdownYear)); if (countdownTime - toki.second() > 0) countdownOverTriggered = false; } //returns true if countdown just over bool checkCountdown() { unsigned long n = toki.second(); if (countdownMode) localTime = countdownTime - n + utcOffsetSecs; if (n > countdownTime) { if (countdownMode) localTime = n - countdownTime + utcOffsetSecs; if (!countdownOverTriggered) { if (macroCountdown != 0) applyPreset(macroCountdown); countdownOverTriggered = true; return true; } } return false; } byte weekdayMondayFirst() { byte wd = weekday(localTime) -1; if (wd == 0) wd = 7; return wd; } bool isTodayInDateRange(byte monthStart, byte dayStart, byte monthEnd, byte dayEnd) { if (monthStart == 0 || dayStart == 0) return true; if (monthEnd == 0) monthEnd = monthStart; if (dayEnd == 0) dayEnd = 31; byte d = day(localTime); byte m = month(localTime); if (monthStart < monthEnd) { if (m > monthStart && m < monthEnd) return true; if (m == monthStart) return (d >= dayStart); if (m == monthEnd) return (d <= dayEnd); return false; } if (monthEnd < monthStart) { //range spans change of year if (m > monthStart || m < monthEnd) return true; if (m == monthStart) return (d >= dayStart); if (m == monthEnd) return (d <= dayEnd); return false; } //start month and end month are the same if (dayEnd < dayStart) return (m != monthStart || (d <= dayEnd || d >= dayStart)); //all year, except the designated days in this month return (m == monthStart && d >= dayStart && d <= dayEnd); //just the designated days this month } void checkTimers() { if (lastTimerMinute != minute(localTime)) //only check once a new minute begins { lastTimerMinute = minute(localTime); // re-calculate sunrise and sunset just after midnight if (!hour(localTime) && minute(localTime)==1) calculateSunriseAndSunset(); DEBUG_PRINTF_P(PSTR("Local time: %02d:%02d\n"), hour(localTime), minute(localTime)); for (unsigned i = 0; i < 8; i++) { if (timerMacro[i] != 0 && (timerWeekday[i] & 0x01) //timer is enabled && (timerHours[i] == hour(localTime) || timerHours[i] == 24) //if hour is set to 24, activate every hour && timerMinutes[i] == minute(localTime) && ((timerWeekday[i] >> weekdayMondayFirst()) & 0x01) //timer should activate at current day of week && isTodayInDateRange(((timerMonth[i] >> 4) & 0x0F), timerDay[i], timerMonth[i] & 0x0F, timerDayEnd[i]) ) { applyPreset(timerMacro[i]); } } // sunrise macro if (sunrise) { time_t tmp = sunrise + timerMinutes[8]*60; // NOTE: may not be ok DEBUG_PRINTF_P(PSTR("Trigger time: %02d:%02d\n"), hour(tmp), minute(tmp)); if (timerMacro[8] != 0 && hour(tmp) == hour(localTime) && minute(tmp) == minute(localTime) && (timerWeekday[8] & 0x01) //timer is enabled && ((timerWeekday[8] >> weekdayMondayFirst()) & 0x01)) //timer should activate at current day of week { applyPreset(timerMacro[8]); DEBUG_PRINTF_P(PSTR("Sunrise macro %d triggered."),timerMacro[8]); } } // sunset macro if (sunset) { time_t tmp = sunset + timerMinutes[9]*60; // NOTE: may not be ok DEBUG_PRINTF_P(PSTR("Trigger time: %02d:%02d\n"), hour(tmp), minute(tmp)); if (timerMacro[9] != 0 && hour(tmp) == hour(localTime) && minute(tmp) == minute(localTime) && (timerWeekday[9] & 0x01) //timer is enabled && ((timerWeekday[9] >> weekdayMondayFirst()) & 0x01)) //timer should activate at current day of week { applyPreset(timerMacro[9]); DEBUG_PRINTF_P(PSTR("Sunset macro %d triggered."),timerMacro[9]); } } } } #define ZENITH -0.83 // get sunrise (or sunset) time (in minutes) for a given day at a given geo location. Returns >= INT16_MAX in case of "no sunset" static int getSunriseUTC(int year, int month, int day, float lat, float lon, bool sunset=false) { //1. first calculate the day of the year float N1 = 275 * month / 9; float N2 = (month + 9) / 12; float N3 = (1.0f + floor_t((year - 4 * floor_t(year / 4) + 2.0f) / 3.0f)); float N = N1 - (N2 * N3) + day - 30.0f; //2. convert the longitude to hour value and calculate an approximate time float lngHour = lon / 15.0f; float t = N + (((sunset ? 18 : 6) - lngHour) / 24); //3. calculate the Sun's mean anomaly float M = (0.9856f * t) - 3.289f; //4. calculate the Sun's true longitude float L = fmod_t(M + (1.916f * sin_t(DEG_TO_RAD*M)) + (0.02f * sin_t(2*DEG_TO_RAD*M)) + 282.634f, 360.0f); //5a. calculate the Sun's right ascension float RA = fmod_t(RAD_TO_DEG*atan_t(0.91764f * tan_t(DEG_TO_RAD*L)), 360.0f); //5b. right ascension value needs to be in the same quadrant as L float Lquadrant = floor_t( L/90) * 90; float RAquadrant = floor_t(RA/90) * 90; RA = RA + (Lquadrant - RAquadrant); //5c. right ascension value needs to be converted into hours RA /= 15.0f; //6. calculate the Sun's declination float sinDec = 0.39782f * sin_t(DEG_TO_RAD*L); float cosDec = cos_t(asin_t(sinDec)); //7a. calculate the Sun's local hour angle float cosH = (sin_t(DEG_TO_RAD*ZENITH) - (sinDec * sin_t(DEG_TO_RAD*lat))) / (cosDec * cos_t(DEG_TO_RAD*lat)); if ((cosH > 1.0f) && !sunset) return INT16_MAX; // the sun never rises on this location (on the specified date) if ((cosH < -1.0f) && sunset) return INT16_MAX; // the sun never sets on this location (on the specified date) //7b. finish calculating H and convert into hours float H = sunset ? RAD_TO_DEG*acos_t(cosH) : 360 - RAD_TO_DEG*acos_t(cosH); H /= 15.0f; //8. calculate local mean time of rising/setting float T = H + RA - (0.06571f * t) - 6.622f; //9. adjust back to UTC float UT = fmod_t(T - lngHour, 24.0f); // return in minutes from midnight return UT*60; } #define SUNSET_MAX (24*60) // 1day = max expected absolute value for sun offset in minutes // calculate sunrise and sunset (if longitude and latitude are set) void calculateSunriseAndSunset() { if ((int)(longitude*10.) || (int)(latitude*10.)) { struct tm tim_0; tim_0.tm_year = year(localTime)-1900; tim_0.tm_mon = month(localTime)-1; tim_0.tm_mday = day(localTime); tim_0.tm_sec = 0; tim_0.tm_isdst = 0; // Due to limited accuracy, its possible to get a bad sunrise/sunset displayed as "00:00" (see issue #3601) // So in case of invalid result, we try to use the sunset/sunrise of previous day. Max 3 days back, this worked well in all cases I tried. // When latitude = 66,6 (N or S), the functions sometimes returns 2147483647, so this "unexpected large" is another condition for retry int minUTC = 0; int retryCount = 0; do { time_t theDay = localTime - retryCount * 86400; // one day back = 86400 seconds minUTC = getSunriseUTC(year(theDay), month(theDay), day(theDay), latitude, longitude, false); DEBUG_PRINTF_P(PSTR("* sunrise (minutes from UTC) = %d\n"), minUTC); retryCount ++; } while ((abs(minUTC) > SUNSET_MAX) && (retryCount <= 3)); if (abs(minUTC) <= SUNSET_MAX) { // there is a sunrise if (minUTC < 0) minUTC += 24*60; // add a day if negative tim_0.tm_hour = minUTC / 60; tim_0.tm_min = minUTC % 60; sunrise = tz->toLocal(mktime(&tim_0) + utcOffsetSecs); DEBUG_PRINTF_P(PSTR("Sunrise: %02d:%02d\n"), hour(sunrise), minute(sunrise)); } else { sunrise = 0; } retryCount = 0; do { time_t theDay = localTime - retryCount * 86400; // one day back = 86400 seconds minUTC = getSunriseUTC(year(theDay), month(theDay), day(theDay), latitude, longitude, true); DEBUG_PRINTF_P(PSTR("* sunset (minutes from UTC) = %d\n"), minUTC); retryCount ++; } while ((abs(minUTC) > SUNSET_MAX) && (retryCount <= 3)); if (abs(minUTC) <= SUNSET_MAX) { // there is a sunset if (minUTC < 0) minUTC += 24*60; // add a day if negative tim_0.tm_hour = minUTC / 60; tim_0.tm_min = minUTC % 60; sunset = tz->toLocal(mktime(&tim_0) + utcOffsetSecs); DEBUG_PRINTF_P(PSTR("Sunset: %02d:%02d\n"), hour(sunset), minute(sunset)); } else { sunset = 0; } } } //time from JSON and HTTP API void setTimeFromAPI(uint32_t timein) { if (timein == 0 || timein == UINT32_MAX) return; uint32_t prev = toki.second(); //only apply if more accurate or there is a significant difference to the "more accurate" time source uint32_t diff = (timein > prev) ? timein - prev : prev - timein; if (toki.getTimeSource() > TOKI_TS_JSON && diff < 60U) return; toki.setTime(timein, TOKI_NO_MS_ACCURACY, TOKI_TS_JSON); if (diff >= 60U) { updateLocalTime(); calculateSunriseAndSunset(); } if (presetsModifiedTime == 0) presetsModifiedTime = timein; } ================================================ FILE: wled00/ota_update.cpp ================================================ #include "ota_update.h" #include "wled.h" #ifdef ESP32 #include #include #include #include #endif // Platform-specific metadata locations #ifdef ESP32 constexpr size_t METADATA_OFFSET = 256; // ESP32: metadata appears after Espressif metadata #define UPDATE_ERROR errorString // Bootloader is at fixed offset 0x1000 (4KB), 0x0000 (0KB), or 0x2000 (8KB), and is typically 32KB // Bootloader offsets for different MCUs => see https://github.com/wled/WLED/issues/5064 #if defined(CONFIG_IDF_TARGET_ESP32S3) || defined(CONFIG_IDF_TARGET_ESP32C3) || defined(CONFIG_IDF_TARGET_ESP32C6) constexpr size_t BOOTLOADER_OFFSET = 0x0000; // esp32-S3, esp32-C3 and (future support) esp32-c6 constexpr size_t BOOTLOADER_SIZE = 0x8000; // 32KB, typical bootloader size #define BOOTLOADER_OTA_UNSUPPORTED // still needs validation on these platforms. #elif defined(CONFIG_IDF_TARGET_ESP32P4) || defined(CONFIG_IDF_TARGET_ESP32C5) constexpr size_t BOOTLOADER_OFFSET = 0x2000; // (future support) esp32-P4 and esp32-C5 constexpr size_t BOOTLOADER_SIZE = 0x8000; // 32KB, typical bootloader size #define BOOTLOADER_OTA_UNSUPPORTED // still needs testing on these platforms #else constexpr size_t BOOTLOADER_OFFSET = 0x1000; // esp32 and esp32-s2 constexpr size_t BOOTLOADER_SIZE = 0x8000; // 32KB, typical bootloader size #endif #elif defined(ESP8266) constexpr size_t METADATA_OFFSET = 0x1000; // ESP8266: metadata appears at 4KB offset #define UPDATE_ERROR getErrorString #endif constexpr size_t METADATA_SEARCH_RANGE = 512; // bytes /** * Check if OTA should be allowed based on release compatibility using custom description * @param binaryData Pointer to binary file data (not modified) * @param dataSize Size of binary data in bytes * @param errorMessage Buffer to store error message if validation fails * @param errorMessageLen Maximum length of error message buffer * @return true if OTA should proceed, false if it should be blocked */ static bool validateOTA(const uint8_t* binaryData, size_t dataSize, char* errorMessage, size_t errorMessageLen) { // Clear error message if (errorMessage && errorMessageLen > 0) { errorMessage[0] = '\0'; } // Try to extract WLED structure directly from binary data wled_metadata_t extractedDesc; bool hasDesc = findWledMetadata(binaryData, dataSize, &extractedDesc); if (hasDesc) { return shouldAllowOTA(extractedDesc, errorMessage, errorMessageLen); } else { // No custom description - this could be a legacy binary if (errorMessage && errorMessageLen > 0) { strncpy_P(errorMessage, PSTR("This firmware file is missing compatibility metadata."), errorMessageLen - 1); errorMessage[errorMessageLen - 1] = '\0'; } return false; } } struct UpdateContext { // State flags // FUTURE: the flags could be replaced by a state machine bool replySent = false; bool needsRestart = false; bool updateStarted = false; bool uploadComplete = false; bool releaseCheckPassed = false; String errorMessage; // Buffer to hold block data across posts, if needed std::vector releaseMetadataBuffer; }; static void endOTA(AsyncWebServerRequest *request) { UpdateContext* context = reinterpret_cast(request->_tempObject); request->_tempObject = nullptr; DEBUG_PRINTF_P(PSTR("EndOTA %x --> %x (%d)\n"), (uintptr_t)request,(uintptr_t) context, context ? context->uploadComplete : 0); if (context) { if (context->updateStarted) { // We initialized the update // We use Update.end() because not all forms of Update() support an abort. // If the upload is incomplete, Update.end(false) should error out. if (Update.end(context->uploadComplete)) { // Update successful! #ifndef ESP8266 bootloopCheckOTA(); // let the bootloop-checker know there was an OTA update #endif doReboot = true; context->needsRestart = false; } } if (context->needsRestart) { strip.resume(); UsermodManager::onUpdateBegin(false); #if WLED_WATCHDOG_TIMEOUT > 0 WLED::instance().enableWatchdog(); #endif } delete context; } }; static bool beginOTA(AsyncWebServerRequest *request, UpdateContext* context) { #ifdef ESP8266 Update.runAsync(true); #endif if (Update.isRunning()) { request->send(503); setOTAReplied(request); return false; } #if WLED_WATCHDOG_TIMEOUT > 0 WLED::instance().disableWatchdog(); #endif UsermodManager::onUpdateBegin(true); // notify usermods that update is about to begin (some may require task de-init) strip.suspend(); backupConfig(); // backup current config in case the update ends badly strip.resetSegments(); // free as much memory as you can context->needsRestart = true; DEBUG_PRINTF_P(PSTR("OTA Update Start, %x --> %x\n"), (uintptr_t)request,(uintptr_t) context); auto skipValidationParam = request->getParam("skipValidation", true); if (skipValidationParam && (skipValidationParam->value() == "1")) { context->releaseCheckPassed = true; DEBUG_PRINTLN(F("OTA validation skipped by user")); } // Begin update with the firmware size from content length size_t updateSize = request->contentLength() > 0 ? request->contentLength() : ((ESP.getFreeSketchSpace() - 0x1000) & 0xFFFFF000); if (!Update.begin(updateSize)) { context->errorMessage = Update.UPDATE_ERROR(); DEBUG_PRINTF_P(PSTR("OTA Failed to begin: %s\n"), context->errorMessage.c_str()); return false; } context->updateStarted = true; return true; } // Create an OTA context object on an AsyncWebServerRequest // Returns true if successful, false on failure. bool initOTA(AsyncWebServerRequest *request) { // Allocate update context UpdateContext* context = new (std::nothrow) UpdateContext {}; if (context) { request->_tempObject = context; request->onDisconnect([=]() { endOTA(request); }); // ensures we restart on failure }; DEBUG_PRINTF_P(PSTR("OTA Update init, %x --> %x\n"), (uintptr_t)request,(uintptr_t) context); return (context != nullptr); } void setOTAReplied(AsyncWebServerRequest *request) { UpdateContext* context = reinterpret_cast(request->_tempObject); if (!context) return; context->replySent = true; }; // Returns pointer to error message, or nullptr if OTA was successful. std::pair getOTAResult(AsyncWebServerRequest* request) { UpdateContext* context = reinterpret_cast(request->_tempObject); if (!context) return { true, F("OTA context unexpectedly missing") }; if (context->replySent) return { false, {} }; if (context->errorMessage.length()) return { true, context->errorMessage }; if (context->updateStarted) { // Release the OTA context now. endOTA(request); if (Update.hasError()) { return { true, Update.UPDATE_ERROR() }; } else { return { true, {} }; } } // Should never happen return { true, F("Internal software failure") }; } void handleOTAData(AsyncWebServerRequest *request, size_t index, uint8_t *data, size_t len, bool isFinal) { UpdateContext* context = reinterpret_cast(request->_tempObject); if (!context) return; //DEBUG_PRINTF_P(PSTR("HandleOTAData: %d %d %d\n"), index, len, isFinal); if (context->replySent || (context->errorMessage.length())) return; if (index == 0) { if (!beginOTA(request, context)) return; } // Perform validation if we haven't done it yet and we have reached the metadata offset if (!context->releaseCheckPassed && (index+len) > METADATA_OFFSET) { // Current chunk contains the metadata offset size_t availableDataAfterOffset = (index + len) - METADATA_OFFSET; DEBUG_PRINTF_P(PSTR("OTA metadata check: %d in buffer, %d received, %d available\n"), context->releaseMetadataBuffer.size(), len, availableDataAfterOffset); if (availableDataAfterOffset >= METADATA_SEARCH_RANGE) { // We have enough data to validate, one way or another const uint8_t* search_data = data; size_t search_len = len; // If we have saved data, use that instead if (context->releaseMetadataBuffer.size()) { // Add this data context->releaseMetadataBuffer.insert(context->releaseMetadataBuffer.end(), data, data+len); search_data = context->releaseMetadataBuffer.data(); search_len = context->releaseMetadataBuffer.size(); } // Do the checking char errorMessage[128]; bool OTA_ok = validateOTA(search_data, search_len, errorMessage, sizeof(errorMessage)); // Release buffer if there was one context->releaseMetadataBuffer = decltype(context->releaseMetadataBuffer){}; if (!OTA_ok) { DEBUG_PRINTF_P(PSTR("OTA declined: %s\n"), errorMessage); context->errorMessage = errorMessage; context->errorMessage += F(" Enable 'Ignore firmware validation' to proceed anyway."); return; } else { DEBUG_PRINTLN(F("OTA allowed: Release compatibility check passed")); context->releaseCheckPassed = true; } } else { // Store the data we just got for next pass context->releaseMetadataBuffer.insert(context->releaseMetadataBuffer.end(), data, data+len); } } // Check if validation was still pending (shouldn't happen normally) // This is done before writing the last chunk, so endOTA can abort if (isFinal && !context->releaseCheckPassed) { DEBUG_PRINTLN(F("OTA failed: Validation never completed")); // Don't write the last chunk to the updater: this will trip an error later context->errorMessage = F("Release check data never arrived?"); return; } // Write chunk data to OTA update (only if release check passed or still pending) if (!Update.hasError()) { if (Update.write(data, len) != len) { DEBUG_PRINTF_P(PSTR("OTA write failed on chunk %zu: %s\n"), index, Update.UPDATE_ERROR()); } } if(isFinal) { DEBUG_PRINTLN(F("OTA Update End")); // Upload complete context->uploadComplete = true; } } void markOTAvalid() { #ifndef ESP8266 const esp_partition_t* running = esp_ota_get_running_partition(); esp_ota_img_states_t ota_state; if (esp_ota_get_state_partition(running, &ota_state) == ESP_OK) { if (ota_state == ESP_OTA_IMG_PENDING_VERIFY) { esp_ota_mark_app_valid_cancel_rollback(); // only needs to be called once, it marks the ota_state as ESP_OTA_IMG_VALID DEBUG_PRINTLN(F("Current firmware validated")); } } #endif } #if defined(ARDUINO_ARCH_ESP32) && !defined(WLED_DISABLE_OTA) // Class for computing the expected bootloader data size given a stream of the data. // If the image includes an SHA256 appended after the data stream, we do not consider it here. class BootloaderImageSizer { public: bool feed(const uint8_t* data, size_t len) { if (error) return false; //DEBUG_PRINTF("Feed %d\n", len); if (imageSize == 0) { // Parse header first if (len < sizeof(esp_image_header_t)) { error = true; return false; } esp_image_header_t header; memcpy(&header, data, sizeof(esp_image_header_t)); if (header.segment_count == 0) { error = true; return false; } imageSize = sizeof(esp_image_header_t); segmentsLeft = header.segment_count; data += sizeof(esp_image_header_t); len -= sizeof(esp_image_header_t); //DEBUG_PRINTF("BLS parsed image header, segment count %d, is %d\n", segmentsLeft, imageSize); } while (len && segmentsLeft) { if (segmentHeaderBytes < sizeof(esp_image_segment_header_t)) { size_t headerBytes = std::min(len, sizeof(esp_image_segment_header_t) - segmentHeaderBytes); memcpy(reinterpret_cast(&segmentHeader) + segmentHeaderBytes, data, headerBytes); segmentHeaderBytes += headerBytes; if (segmentHeaderBytes < sizeof(esp_image_segment_header_t)) { return true; // needs more bytes for the header } //DEBUG_PRINTF("BLS parsed segment [%08X %08X=%d], segment count %d, is %d\n", segmentHeader.load_addr, segmentHeader.data_len, segmentHeader.data_len, segmentsLeft, imageSize); // Validate segment size if (segmentHeader.data_len > BOOTLOADER_SIZE) { error = true; return false; } data += headerBytes; len -= headerBytes; imageSize += sizeof(esp_image_segment_header_t) + segmentHeader.data_len; --segmentsLeft; if (segmentsLeft == 0) { // all done, actually; we don't need to read any more // Round up to nearest 16 bytes. // Always add 1 to account for the checksum byte. imageSize = ((imageSize/ 16) + 1) * 16; //DEBUG_PRINTF("BLS complete, is %d\n", imageSize); return false; } } // If we don't have enough bytes ... if (len < segmentHeader.data_len) { //DEBUG_PRINTF("Needs more bytes\n"); segmentHeader.data_len -= len; return true; // still in this segment } // Segment complete len -= segmentHeader.data_len; data += segmentHeader.data_len; segmentHeaderBytes = 0; //DEBUG_PRINTF("Segment complete: len %d\n", len); } return !error; } bool hasError() const { return error; } bool isSizeKnown() const { return !error && imageSize != 0 && segmentsLeft == 0; } size_t totalSize() const { if (!isSizeKnown()) return 0; return imageSize; } private: size_t imageSize = 0; size_t segmentsLeft = 0; esp_image_segment_header_t segmentHeader; size_t segmentHeaderBytes = 0; bool error = false; }; static bool bootloaderSHA256CacheValid = false; static uint8_t bootloaderSHA256Cache[32]; /** * Calculate and cache the bootloader SHA256 digest * Reads the bootloader from flash and computes SHA256 hash * * Strictly speaking, most bootloader images already contain a hash at the end of the image; * we could in theory just read it. The trouble is that we have to parse the structure anyways * to find the actual endpoint, so we might as well always calculate it ourselves rather than * handle a special case if the hash isn't stored. * */ static void calculateBootloaderSHA256() { // Calculate SHA256 mbedtls_sha256_context ctx; mbedtls_sha256_init(&ctx); mbedtls_sha256_starts(&ctx, 0); // 0 = SHA256 (not SHA224) const size_t chunkSize = 256; alignas(esp_image_header_t) uint8_t buffer[chunkSize]; size_t bootloaderSize = BOOTLOADER_SIZE; BootloaderImageSizer sizer; size_t totalHashLen = 0; for (uint32_t offset = 0; offset < bootloaderSize; offset += chunkSize) { size_t readSize = min((size_t)(bootloaderSize - offset), chunkSize); if (esp_flash_read(NULL, buffer, BOOTLOADER_OFFSET + offset, readSize) == ESP_OK) { sizer.feed(buffer, readSize); size_t hashLen = readSize; if (sizer.isSizeKnown()) { size_t totalSize = sizer.totalSize(); if (totalSize > 0 && totalSize <= BOOTLOADER_SIZE) { bootloaderSize = totalSize; if (offset + readSize > totalSize) { hashLen = (totalSize > offset) ? (totalSize - offset) : 0; } } } if (hashLen > 0) { totalHashLen += hashLen; mbedtls_sha256_update(&ctx, buffer, hashLen); } } } mbedtls_sha256_finish(&ctx, bootloaderSHA256Cache); mbedtls_sha256_free(&ctx); bootloaderSHA256CacheValid = true; } // Get bootloader SHA256 as hex string String getBootloaderSHA256Hex() { if (!bootloaderSHA256CacheValid) { calculateBootloaderSHA256(); } // Convert to hex string String result; result.reserve(65); for (int i = 0; i < 32; i++) { unsigned char b1 = bootloaderSHA256Cache[i]; unsigned char b2 = b1 >> 4; b1 &= 0x0F; b1 += '0'; b2 += '0'; if (b1 > '9') b1 += 39; if (b2 > '9') b2 += 39; result.concat(b2); result.concat(b1); } return result; } /** * Invalidate cached bootloader SHA256 (call after bootloader update) * Forces recalculation on next call to calculateBootloaderSHA256 or getBootloaderSHA256Hex */ static void invalidateBootloaderSHA256Cache() { bootloaderSHA256CacheValid = false; } /** * Verify complete buffered bootloader using ESP-IDF validation approach * This matches the key validation steps from esp_image_verify() in ESP-IDF * @param buffer Reference to pointer to bootloader binary data (will be adjusted if offset detected) * @param len Reference to length of bootloader data (will be adjusted to actual size) * @param bootloaderErrorMsg Pointer to String to store error message (must not be null) * @return true if validation passed, false otherwise */ static bool verifyBootloaderImage(const uint8_t* &buffer, size_t &len, String& bootloaderErrorMsg) { const size_t MIN_IMAGE_HEADER_SIZE = sizeof(esp_image_header_t); // 1. Validate minimum size for header if (len < MIN_IMAGE_HEADER_SIZE) { bootloaderErrorMsg = "Too small"; return false; } // Check if the bootloader starts at offset 0x1000 (common in partition table dumps) // This happens when someone uploads a complete flash dump instead of just the bootloader if (len > BOOTLOADER_OFFSET + MIN_IMAGE_HEADER_SIZE && buffer[BOOTLOADER_OFFSET] == ESP_IMAGE_HEADER_MAGIC && buffer[0] != ESP_IMAGE_HEADER_MAGIC) { DEBUG_PRINTF_P(PSTR("Bootloader detected at offset\n")); // Adjust buffer pointer to start at the actual bootloader buffer = buffer + BOOTLOADER_OFFSET; len = len - BOOTLOADER_OFFSET; // Re-validate size after adjustment if (len < MIN_IMAGE_HEADER_SIZE) { bootloaderErrorMsg = "Too small"; return false; } } size_t availableLen = len; esp_image_header_t imageHeader{}; memcpy(&imageHeader, buffer, sizeof(imageHeader)); // 2. Basic header sanity checks (matches early esp_image_verify checks) if (imageHeader.magic != ESP_IMAGE_HEADER_MAGIC || imageHeader.segment_count == 0 || imageHeader.segment_count > 16 || imageHeader.spi_mode > 3 || imageHeader.entry_addr < 0x40000000 || imageHeader.entry_addr > 0x50000000) { bootloaderErrorMsg = "Invalid header"; return false; } // 3. Chip ID validation (matches esp_image_verify step 3) if (imageHeader.chip_id != CONFIG_IDF_FIRMWARE_CHIP_ID) { bootloaderErrorMsg = "Chip ID mismatch"; return false; } // 4. Validate image size BootloaderImageSizer sizer; sizer.feed(buffer, availableLen); if (!sizer.isSizeKnown()) { bootloaderErrorMsg = "Invalid image"; return false; } size_t actualBootloaderSize = sizer.totalSize(); // 5. SHA256 checksum (optional) if (imageHeader.hash_appended == 1) { actualBootloaderSize += 32; } if (actualBootloaderSize > len) { // Same as above bootloaderErrorMsg = "Too small"; return false; } DEBUG_PRINTF_P(PSTR("Bootloader validation: %d segments, actual size %d bytes (buffer size %d bytes, hash_appended=%d)\n"), imageHeader.segment_count, actualBootloaderSize, len, imageHeader.hash_appended); // Update len to reflect actual bootloader size (including hash and checksum, with alignment) // This is critical - we must write the complete image including checksums len = actualBootloaderSize; return true; } // Bootloader OTA context structure struct BootloaderUpdateContext { // State flags bool replySent = false; bool uploadComplete = false; String errorMessage; // Buffer to hold bootloader data uint8_t* buffer = nullptr; size_t bytesBuffered = 0; const uint32_t bootloaderOffset = 0x1000; const uint32_t maxBootloaderSize = 0x10000; // 64KB buffer size }; // Cleanup bootloader OTA context static void endBootloaderOTA(AsyncWebServerRequest *request) { BootloaderUpdateContext* context = reinterpret_cast(request->_tempObject); request->_tempObject = nullptr; DEBUG_PRINTF_P(PSTR("EndBootloaderOTA %x --> %x\n"), (uintptr_t)request, (uintptr_t)context); if (context) { if (context->buffer) { free(context->buffer); context->buffer = nullptr; } // If update failed, restore system state if (!context->uploadComplete || !context->errorMessage.isEmpty()) { strip.resume(); #if WLED_WATCHDOG_TIMEOUT > 0 WLED::instance().enableWatchdog(); #endif } delete context; } } // Initialize bootloader OTA context bool initBootloaderOTA(AsyncWebServerRequest *request) { if (request->_tempObject) { return true; // Already initialized } BootloaderUpdateContext* context = new BootloaderUpdateContext(); if (!context) { DEBUG_PRINTLN(F("Failed to allocate bootloader OTA context")); return false; } request->_tempObject = context; request->onDisconnect([=]() { endBootloaderOTA(request); }); // ensures cleanup on disconnect #ifdef BOOTLOADER_OTA_UNSUPPORTED context->errorMessage = F("Bootloader update not supported on this chip"); return false; #else DEBUG_PRINTLN(F("Bootloader Update Start - initializing buffer")); #if WLED_WATCHDOG_TIMEOUT > 0 WLED::instance().disableWatchdog(); #endif lastEditTime = millis(); // make sure PIN does not lock during update strip.suspend(); strip.resetSegments(); // Check available heap before attempting allocation DEBUG_PRINTF_P(PSTR("Free heap before bootloader buffer allocation: %d bytes (need %d bytes)\n"), getContiguousFreeHeap(), context->maxBootloaderSize); context->buffer = (uint8_t*)malloc(context->maxBootloaderSize); if (!context->buffer) { size_t freeHeapNow = getContiguousFreeHeap(); DEBUG_PRINTF_P(PSTR("Failed to allocate %d byte bootloader buffer! Contiguous heap: %d bytes\n"), context->maxBootloaderSize, freeHeapNow); context->errorMessage = "Out of memory! Contiguous heap: " + String(freeHeapNow) + " bytes, need: " + String(context->maxBootloaderSize) + " bytes"; strip.resume(); #if WLED_WATCHDOG_TIMEOUT > 0 WLED::instance().enableWatchdog(); #endif return false; } context->bytesBuffered = 0; return true; #endif } // Set bootloader OTA replied flag void setBootloaderOTAReplied(AsyncWebServerRequest *request) { BootloaderUpdateContext* context = reinterpret_cast(request->_tempObject); if (context) { context->replySent = true; } } // Get bootloader OTA result std::pair getBootloaderOTAResult(AsyncWebServerRequest *request) { BootloaderUpdateContext* context = reinterpret_cast(request->_tempObject); if (!context) { return std::make_pair(true, String(F("Internal error: No bootloader OTA context"))); } bool needsReply = !context->replySent; String errorMsg = context->errorMessage; // If upload was successful, return empty string and trigger reboot if (context->uploadComplete && errorMsg.isEmpty()) { doReboot = true; endBootloaderOTA(request); return std::make_pair(needsReply, String()); } // If there was an error, return it if (!errorMsg.isEmpty()) { endBootloaderOTA(request); return std::make_pair(needsReply, errorMsg); } // Should never happen return std::make_pair(true, String(F("Internal software failure"))); } // Handle bootloader OTA data void handleBootloaderOTAData(AsyncWebServerRequest *request, size_t index, uint8_t *data, size_t len, bool isFinal) { BootloaderUpdateContext* context = reinterpret_cast(request->_tempObject); if (!context) { DEBUG_PRINTLN(F("No bootloader OTA context - ignoring data")); return; } if (!context->errorMessage.isEmpty()) { return; } // Buffer the incoming data if (context->buffer && context->bytesBuffered + len <= context->maxBootloaderSize) { memcpy(context->buffer + context->bytesBuffered, data, len); context->bytesBuffered += len; DEBUG_PRINTF_P(PSTR("Bootloader buffer progress: %d / %d bytes\n"), context->bytesBuffered, context->maxBootloaderSize); } else if (!context->buffer) { DEBUG_PRINTLN(F("Bootloader buffer not allocated!")); context->errorMessage = "Internal error: Bootloader buffer not allocated"; return; } else { size_t totalSize = context->bytesBuffered + len; DEBUG_PRINTLN(F("Bootloader size exceeds maximum!")); context->errorMessage = "Bootloader file too large: " + String(totalSize) + " bytes (max: " + String(context->maxBootloaderSize) + " bytes)"; return; } // Only write to flash when upload is complete if (isFinal) { DEBUG_PRINTLN(F("Bootloader Upload Complete - validating and flashing")); if (context->buffer && context->bytesBuffered > 0) { // Prepare pointers for verification (may be adjusted if bootloader at offset) const uint8_t* bootloaderData = context->buffer; size_t bootloaderSize = context->bytesBuffered; // Verify the complete bootloader image before flashing // Note: verifyBootloaderImage may adjust bootloaderData pointer and bootloaderSize // for validation purposes only if (!verifyBootloaderImage(bootloaderData, bootloaderSize, context->errorMessage)) { DEBUG_PRINTLN(F("Bootloader validation failed!")); // Error message already set by verifyBootloaderImage } else { // Calculate offset to write to flash // If bootloaderData was adjusted (partition table detected), we need to skip it in flash too size_t flashOffset = context->bootloaderOffset; const uint8_t* dataToWrite = context->buffer; size_t bytesToWrite = context->bytesBuffered; // If validation adjusted the pointer, it means we have a partition table at the start // In this case, we should skip writing the partition table and write bootloader at 0x1000 if (bootloaderData != context->buffer) { // bootloaderData was adjusted - skip partition table in our data size_t partitionTableSize = bootloaderData - context->buffer; dataToWrite = bootloaderData; bytesToWrite = bootloaderSize; DEBUG_PRINTF_P(PSTR("Skipping %d bytes of partition table data\n"), partitionTableSize); } DEBUG_PRINTF_P(PSTR("Bootloader validation passed - writing %d bytes to flash at 0x%04X\n"), bytesToWrite, flashOffset); // Calculate erase size (must be multiple of 4KB) size_t eraseSize = ((bytesToWrite + 0xFFF) / 0x1000) * 0x1000; if (eraseSize > context->maxBootloaderSize) { eraseSize = context->maxBootloaderSize; } // Erase bootloader region DEBUG_PRINTF_P(PSTR("Erasing %d bytes at 0x%04X...\n"), eraseSize, flashOffset); esp_err_t err = esp_flash_erase_region(NULL, flashOffset, eraseSize); if (err != ESP_OK) { DEBUG_PRINTF_P(PSTR("Bootloader erase error: %d\n"), err); context->errorMessage = "Flash erase failed (error code: " + String(err) + ")"; } else { // Write the validated bootloader data to flash err = esp_flash_write(NULL, dataToWrite, flashOffset, bytesToWrite); if (err != ESP_OK) { DEBUG_PRINTF_P(PSTR("Bootloader flash write error: %d\n"), err); context->errorMessage = "Flash write failed (error code: " + String(err) + ")"; } else { DEBUG_PRINTF_P(PSTR("Bootloader Update Success - %d bytes written to 0x%04X\n"), bytesToWrite, flashOffset); // Invalidate cached bootloader hash invalidateBootloaderSHA256Cache(); context->uploadComplete = true; } } } } else if (context->bytesBuffered == 0) { context->errorMessage = "No bootloader data received"; } } } #endif ================================================ FILE: wled00/ota_update.h ================================================ // WLED OTA update interface #include #ifdef ESP8266 #include #else #include #endif #pragma once // Platform-specific metadata locations #ifdef ESP32 #define BUILD_METADATA_SECTION ".rodata_custom_desc" #elif defined(ESP8266) #define BUILD_METADATA_SECTION ".ver_number" #endif class AsyncWebServerRequest; /** * Create an OTA context object on an AsyncWebServerRequest * @param request Pointer to web request object * @return true if allocation was successful, false if not */ bool initOTA(AsyncWebServerRequest *request); /** * Indicate to the OTA subsystem that a reply has already been generated * @param request Pointer to web request object */ void setOTAReplied(AsyncWebServerRequest *request); /** * Retrieve the OTA result. * @param request Pointer to web request object * @return bool indicating if a reply is necessary; string with error message if the update failed. */ std::pair getOTAResult(AsyncWebServerRequest *request); /** * Process a block of OTA data. This is a passthrough of an ArUploadHandlerFunction. * Requires that initOTA be called on the handler object before any work will be done. * @param request Pointer to web request object * @param index Offset in to uploaded file * @param data New data bytes * @param len Length of new data bytes * @param isFinal Indicates that this is the last block * @return bool indicating if a reply is necessary; string with error message if the update failed. */ void handleOTAData(AsyncWebServerRequest *request, size_t index, uint8_t *data, size_t len, bool isFinal); /** * Mark currently running firmware as valid to prevent auto-rollback on reboot. * This option can be enabled in some builds/bootloaders, it is an sdkconfig flag. */ void markOTAvalid(); #if defined(ARDUINO_ARCH_ESP32) && !defined(WLED_DISABLE_OTA) /** * Get bootloader SHA256 as hex string * @return String containing 64-character hex representation of SHA256 hash */ String getBootloaderSHA256Hex(); /** * Create a bootloader OTA context object on an AsyncWebServerRequest * @param request Pointer to web request object * @return true if allocation was successful, false if not */ bool initBootloaderOTA(AsyncWebServerRequest *request); /** * Indicate to the bootloader OTA subsystem that a reply has already been generated * @param request Pointer to web request object */ void setBootloaderOTAReplied(AsyncWebServerRequest *request); /** * Retrieve the bootloader OTA result. * @param request Pointer to web request object * @return bool indicating if a reply is necessary; string with error message if the update failed. */ std::pair getBootloaderOTAResult(AsyncWebServerRequest *request); /** * Process a block of bootloader OTA data. This is a passthrough of an ArUploadHandlerFunction. * Requires that initBootloaderOTA be called on the handler object before any work will be done. * @param request Pointer to web request object * @param index Offset in to uploaded file * @param data New data bytes * @param len Length of new data bytes * @param isFinal Indicates that this is the last block */ void handleBootloaderOTAData(AsyncWebServerRequest *request, size_t index, uint8_t *data, size_t len, bool isFinal); #endif ================================================ FILE: wled00/overlay.cpp ================================================ #include "wled.h" // forward declarations static void _overlayAnalogCountdown(); /* * Used to draw clock overlays over the strip */ static void _overlayAnalogClock() { int overlaySize = overlayMax - overlayMin +1; if (countdownMode) { _overlayAnalogCountdown(); return; } float hourP = ((float)(hour(localTime)%12))/12.0f; float minuteP = ((float)minute(localTime))/60.0f; hourP = hourP + minuteP/12.0f; float secondP = ((float)second(localTime))/60.0f; unsigned hourPixel = floorf(analogClock12pixel + overlaySize*hourP); if (hourPixel > overlayMax) hourPixel = overlayMin -1 + hourPixel - overlayMax; unsigned minutePixel = floorf(analogClock12pixel + overlaySize*minuteP); if (minutePixel > overlayMax) minutePixel = overlayMin -1 + minutePixel - overlayMax; unsigned secondPixel = floorf(analogClock12pixel + overlaySize*secondP); if (secondPixel > overlayMax) secondPixel = overlayMin -1 + secondPixel - overlayMax; if (analogClockSecondsTrail) { if (secondPixel < analogClock12pixel) { strip.setRange(analogClock12pixel, overlayMax, color_fade(0xFF0000, bri)); strip.setRange(overlayMin, secondPixel, color_fade(0xFF0000, bri)); } else { strip.setRange(analogClock12pixel, secondPixel, color_fade(0xFF0000, bri)); } } if (analogClock5MinuteMarks) { for (unsigned i = 0; i <= 12; i++) { unsigned pix = analogClock12pixel + roundf((overlaySize / 12.0f) *i); if (pix > overlayMax) pix -= overlaySize; strip.setPixelColor(pix, color_fade(0x00FFAA, bri)); } } if (!analogClockSecondsTrail) strip.setPixelColor(secondPixel, color_fade(0xFF0000, bri)); strip.setPixelColor(minutePixel, color_fade(0x00FF00, bri)); strip.setPixelColor(hourPixel, color_fade(0x0000FF, bri)); } static void _overlayAnalogCountdown() { if ((unsigned long)toki.second() < countdownTime) { long diff = countdownTime - toki.second(); float pval = 60.0f; if (diff > 31557600L) //display in years if more than 365 days { pval = 315576000.0f; //10 years } else if (diff > 2592000L) //display in months if more than a month { pval = 31557600.0f; //1 year } else if (diff > 604800) //display in weeks if more than a week { pval = 2592000.0f; //1 month } else if (diff > 86400) //display in days if more than 24 hours { pval = 604800.0f; //1 week } else if (diff > 3600) //display in hours if more than 60 minutes { pval = 86400.0f; //1 day } else if (diff > 60) //display in minutes if more than 60 seconds { pval = 3600.0f; //1 hour } int overlaySize = overlayMax - overlayMin +1; float perc = (pval-(float)diff)/pval; if (perc > 1.0f) perc = 1.0f; byte pixelCnt = perc*overlaySize; if (analogClock12pixel + pixelCnt > overlayMax) { strip.setRange(analogClock12pixel, overlayMax, ((uint32_t)colSec[3] << 24)| ((uint32_t)colSec[0] << 16) | ((uint32_t)colSec[1] << 8) | colSec[2]); strip.setRange(overlayMin, overlayMin +pixelCnt -(1+ overlayMax -analogClock12pixel), ((uint32_t)colSec[3] << 24)| ((uint32_t)colSec[0] << 16) | ((uint32_t)colSec[1] << 8) | colSec[2]); } else { strip.setRange(analogClock12pixel, analogClock12pixel + pixelCnt, ((uint32_t)colSec[3] << 24)| ((uint32_t)colSec[0] << 16) | ((uint32_t)colSec[1] << 8) | colSec[2]); } } } void handleOverlayDraw() { UsermodManager::handleOverlayDraw(); if (analogClockSolidBlack) { for (unsigned i = 0; i < strip.getSegmentsNum(); i++) { const Segment& segment = strip.getSegment(i); if (!segment.isActive()) continue; if (segment.mode > 0 || segment.colors[0] > 0) { return; } } } if (overlayCurrent == 1) _overlayAnalogClock(); } /* * Support for the Cronixie clock has moved to a usermod, compile with "-D USERMOD_CRONIXIE" to enable */ ================================================ FILE: wled00/palettes.cpp ================================================ #include "wled.h" /* * WLED Color palettes * * Note: palettes imported from http://seaviewsensing.com/pub/cpt-city are gamma corrected using gammas (1.182, 1.0, 1.136) * this is done to match colors of the palettes after applying the (default) global gamma of 2.2 to versions * prior to WLED 0.16 which used pre-applied gammas of (2.6,2.2,2.5) for these palettes. * Palettes from FastLED are intended to be used without gamma correction, an inverse gamma of 2.2 is applied to original colors */ // Gradient palette "ib_jul01_gp", originally from // http://seaviewsensing.com/pub/cpt-city/ing/xmas/ib_jul01.c3g const uint8_t ib_jul01_gp[] PROGMEM = { 0, 226, 6, 12, 94, 26, 96, 78, 132, 130, 189, 94, 255, 177, 3, 9}; // Gradient palette "es_vintage_57_gp", originally from // http://seaviewsensing.com/pub/cpt-city/es/vintage/es_vintage_57.c3g const uint8_t es_vintage_57_gp[] PROGMEM = { 0, 29, 8, 3, 53, 76, 1, 0, 104, 142, 96, 28, 153, 211, 191, 61, 255, 117, 129, 42}; // Gradient palette "es_vintage_01_gp", originally from // http://seaviewsensing.com/pub/cpt-city/es/vintage/es_vintage_01.c3g const uint8_t es_vintage_01_gp[] PROGMEM = { 0, 41, 18, 24, 51, 73, 0, 22, 76, 165, 170, 38, 101, 255, 189, 80, 127, 139, 56, 40, 153, 73, 0, 22, 229, 41, 18, 24, 255, 41, 18, 24}; // Gradient palette "es_rivendell_15_gp", originally from // http://seaviewsensing.com/pub/cpt-city/es/rivendell/es_rivendell_15.c3g const uint8_t es_rivendell_15_gp[] PROGMEM = { 0, 24, 69, 44, 101, 73, 105, 70, 165, 129, 140, 97, 242, 200, 204, 166, 255, 200, 204, 166}; // Gradient palette "rgi_15_gp", originally from // http://seaviewsensing.com/pub/cpt-city/ds/rgi/rgi_15.c3g const uint8_t rgi_15_gp[] PROGMEM = { 0, 41, 14, 99, 31, 128, 24, 74, 63, 227, 34, 50, 95, 132, 31, 76, 127, 47, 29, 102, 159, 109, 47, 101, 191, 176, 66, 100, 223, 129, 57, 104, 255, 84, 48, 108}; // Gradient palette "retro2_16_gp", originally from // http://seaviewsensing.com/pub/cpt-city/ma/retro2/retro2_16.c3g const uint8_t retro2_16_gp[] PROGMEM = { 0, 222, 191, 8, 255, 117, 52, 1}; // Gradient palette "Analogous_1_gp", originally from // http://seaviewsensing.com/pub/cpt-city/nd/red/Analogous_1.c3g const uint8_t Analogous_1_gp[] PROGMEM = { 0, 38, 0, 255, 63, 86, 0, 255, 127, 139, 0, 255, 191, 196, 0, 117, 255, 255, 0, 0}; // Gradient palette "es_pinksplash_08_gp", originally from // http://seaviewsensing.com/pub/cpt-city/es/pink_splash/es_pinksplash_08.c3g const uint8_t es_pinksplash_08_gp[] PROGMEM = { 0, 186, 63, 255, 127, 227, 9, 85, 175, 234, 205, 213, 221, 205, 38, 176, 255, 205, 38, 176, }; // Gradient palette "es_ocean_breeze_036_gp", originally from // http://seaviewsensing.com/pub/cpt-city/es/ocean_breeze/es_ocean_breeze_036.c3g const uint8_t es_ocean_breeze_036_gp[] PROGMEM = { 0, 16, 48, 51, 89, 27, 166, 175, 153, 197, 233, 255, 255, 0, 145, 152}; // Gradient palette "departure_gp", originally from // http://seaviewsensing.com/pub/cpt-city/mjf/departure.c3g const uint8_t departure_gp[] PROGMEM = { 0, 53, 34, 0, 42, 86, 51, 0, 63, 147, 108, 49, 84, 212, 166, 108, 106, 235, 212, 180, 116, 255, 255, 255, 138, 191, 255, 193, 148, 84, 255, 88, 170, 0, 255, 0, 191, 0, 192, 0, 212, 0, 128, 0, 255, 0, 128, 0}; // Gradient palette "es_landscape_64_gp", originally from // http://seaviewsensing.com/pub/cpt-city/es/landscape/es_landscape_64.c3g const uint8_t es_landscape_64_gp[] PROGMEM = { 0, 0, 0, 0, 37, 31, 89, 19, 76, 72, 178, 43, 127, 150, 235, 5, 128, 186, 234, 119, 130, 222, 233, 252, 153, 197, 219, 231, 204, 132, 179, 253, 255, 28, 107, 225}; // Gradient palette "es_landscape_33_gp", originally from // http://seaviewsensing.com/pub/cpt-city/es/landscape/es_landscape_33.c3g const uint8_t es_landscape_33_gp[] PROGMEM = { 0, 12, 45, 0, 19, 101, 86, 2, 38, 207, 128, 4, 63, 243, 197, 18, 66, 109, 196, 146, 255, 5, 39, 7}; // Gradient palette "rainbowsherbet_gp", originally from // http://seaviewsensing.com/pub/cpt-city/ma/icecream/rainbowsherbet.c3g const uint8_t rainbowsherbet_gp[] PROGMEM = { 0, 255, 102, 41, 43, 255, 140, 90, 86, 255, 51, 90, 127, 255, 153, 169, 170, 255, 255, 249, 209, 113, 255, 85, 255, 157, 255, 137}; // Gradient palette "gr65_hult_gp", originally from // http://seaviewsensing.com/pub/cpt-city/hult/gr65_hult.c3g const uint8_t gr65_hult_gp[] PROGMEM = { 0, 251, 216, 252, 48, 255, 192, 255, 89, 239, 95, 241, 160, 51, 153, 217, 216, 24, 184, 174, 255, 24, 184, 174}; // Gradient palette "gr64_hult_gp", originally from // http://seaviewsensing.com/pub/cpt-city/hult/gr64_hult.c3g const uint8_t gr64_hult_gp[] PROGMEM = { 0, 24, 184, 174, 66, 8, 162, 150, 104, 124, 137, 7, 130, 178, 186, 22, 150, 124, 137, 7, 201, 6, 156, 144, 239, 0, 128, 117, 255, 0, 128, 117}; // Gradient palette "GMT_drywet_gp", originally from // http://seaviewsensing.com/pub/cpt-city/gmt/GMT_drywet.c3g const uint8_t GMT_drywet_gp[] PROGMEM = { 0, 119, 97, 33, 42, 235, 199, 88, 84, 169, 238, 124, 127, 37, 238, 232, 170, 7, 120, 236, 212, 27, 1, 175, 255, 4, 51, 101}; // Gradient palette "ib15_gp", originally from // http://seaviewsensing.com/pub/cpt-city/ing/general/ib15.c3g const uint8_t ib15_gp[] PROGMEM = { 0, 177, 160, 199, 72, 205, 158, 149, 89, 233, 155, 101, 107, 255, 95, 63, 141, 192, 98, 109, 255, 132, 101, 159}; // Gradient palette "Tertiary_01_gp", originally from // http://seaviewsensing.com/pub/cpt-city/nd/vermillion/Tertiary_01.c3g const uint8_t Tertiary_01_gp[] PROGMEM = { 0, 0, 25, 255, 63, 38, 140, 117, 127, 86, 255, 0, 191, 167, 140, 19, 255, 255, 25, 41}; // Gradient palette "lava_gp", originally from // http://seaviewsensing.com/pub/cpt-city/neota/elem/lava.c3g const uint8_t lava_gp[] PROGMEM = { 0, 0, 0, 0, 46, 77, 0, 0, 96, 177, 0, 0, 108, 196, 38, 9, 119, 215, 76, 19, 146, 235, 115, 29, 174, 255, 153, 41, 188, 255, 178, 41, 202, 255, 204, 41, 218, 255, 230, 41, 234, 255, 255, 41, 244, 255, 255, 143, 255, 255, 255, 255}; // Gradient palette "fierce-ice_gp", originally from // http://seaviewsensing.com/pub/cpt-city/neota/elem/fierce-ice.c3g const uint8_t fierce_ice_gp[] PROGMEM = { 0, 0, 0, 0, 59, 0, 51, 117, 119, 0, 102, 255, 149, 38, 153, 255, 180, 86, 204, 255, 217, 167, 230, 255, 255, 255, 255, 255}; // Gradient palette "Colorfull_gp", originally from // http://seaviewsensing.com/pub/cpt-city/nd/atmospheric/Colorfull.c3g const uint8_t Colorfull_gp[] PROGMEM = { 0, 61, 155, 44, 25, 95, 174, 77, 60, 132, 193, 113, 93, 154, 166, 125, 106, 175, 138, 136, 109, 183, 121, 137, 113, 194, 104, 138, 116, 225, 179, 165, 124, 255, 255, 192, 168, 167, 218, 203, 255, 84, 182, 215}; // Gradient palette "Pink_Purple_gp", originally from // http://seaviewsensing.com/pub/cpt-city/nd/atmospheric/Pink_Purple.c3g const uint8_t Pink_Purple_gp[] PROGMEM = { 0, 79, 32, 109, 25, 90, 40, 117, 51, 102, 48, 124, 76, 141, 135, 185, 102, 180, 222, 248, 109, 208, 236, 252, 114, 237, 250, 255, 122, 206, 200, 239, 149, 177, 149, 222, 183, 187, 130, 203, 255, 198, 111, 184}; // Gradient palette "Sunset_Real_gp", originally from // http://seaviewsensing.com/pub/cpt-city/nd/atmospheric/Sunset_Real.c3g const uint8_t Sunset_Real_gp[] PROGMEM = { 0, 181, 0, 0, 22, 218, 85, 0, 51, 255, 170, 0, 85, 211, 85, 77, 135, 167, 0, 169, 198, 73, 0, 188, 255, 0, 0, 207}; // Gradient palette "Sunset_Yellow_gp", originally from // http://seaviewsensing.com/pub/cpt-city/nd/atmospheric/Sunset_Yellow.c3g const uint8_t Sunset_Yellow_gp[] PROGMEM = { 0, 61, 135, 184, 36, 129, 188, 169, 87, 203, 241, 155, 100, 228, 237, 141, 107, 255, 232, 127, 115, 251, 202, 130, 120, 248, 172, 133, 128, 251, 202, 130, 180, 255, 232, 127, 223, 255, 242, 120, 255, 255, 252, 113}; // Gradient palette "Beech_gp", originally from // http://seaviewsensing.com/pub/cpt-city/nd/atmospheric/Beech.c3g const uint8_t Beech_gp[] PROGMEM = { 0, 255, 254, 236, 12, 255, 254, 236, 22, 255, 254, 236, 26, 223, 224, 178, 28, 192, 195, 124, 28, 176, 255, 231, 50, 123, 251, 236, 71, 74, 246, 241, 93, 33, 225, 228, 120, 0, 204, 215, 133, 4, 168, 178, 136, 10, 132, 143, 136, 51, 189, 212, 208, 23, 159, 201, 255, 0, 129, 190}; // Gradient palette "Another_Sunset_gp", originally from // http://seaviewsensing.com/pub/cpt-city/nd/atmospheric/Another_Sunset.c3g const uint8_t Another_Sunset_gp[] PROGMEM = { 0, 175, 121, 62, 29, 128, 103, 60, 68, 84, 84, 58, 68, 248, 184, 55, 97, 239, 204, 93, 124, 230, 225, 133, 178, 102, 125, 129, 255, 0, 26, 125}; // Gradient palette "es_autumn_19_gp", originally from // http://seaviewsensing.com/pub/cpt-city/es/autumn/es_autumn_19.c3g const uint8_t es_autumn_19_gp[] PROGMEM = { 0, 90, 14, 5, 51, 139, 41, 13, 84, 180, 70, 17, 104, 192, 202, 125, 112, 177, 137, 3, 122, 190, 200, 131, 124, 192, 202, 124, 135, 177, 137, 3, 142, 194, 203, 118, 163, 177, 68, 17, 204, 128, 35, 12, 249, 74, 5, 2, 255, 74, 5, 2}; // Gradient palette "BlacK_Blue_Magenta_White_gp", originally from // http://seaviewsensing.com/pub/cpt-city/nd/basic/BlacK_Blue_Magenta_White.c3g const uint8_t BlacK_Blue_Magenta_White_gp[] PROGMEM = { 0, 0, 0, 0, 42, 0, 0, 117, 84, 0, 0, 255, 127, 113, 0, 255, 170, 255, 0, 255, 212, 255, 128, 255, 255, 255, 255, 255}; // Gradient palette "BlacK_Magenta_Red_gp", originally from // http://seaviewsensing.com/pub/cpt-city/nd/basic/BlacK_Magenta_Red.c3g const uint8_t BlacK_Magenta_Red_gp[] PROGMEM = { 0, 0, 0, 0, 63, 113, 0, 117, 127, 255, 0, 255, 191, 255, 0, 117, 255, 255, 0, 0}; // Gradient palette "BlacK_Red_Magenta_Yellow_gp", originally from // http://seaviewsensing.com/pub/cpt-city/nd/basic/BlacK_Red_Magenta_Yellow.c3g const uint8_t BlacK_Red_Magenta_Yellow_gp[] PROGMEM = { 0, 0, 0, 0, 42, 113, 0, 0, 84, 255, 0, 0, 127, 255, 0, 117, 170, 255, 0, 255, 212, 255, 128, 117, 255, 255, 255, 0}; // Gradient palette "Blue_Cyan_Yellow_gp", originally from // http://seaviewsensing.com/pub/cpt-city/nd/basic/Blue_Cyan_Yellow.c3g const uint8_t Blue_Cyan_Yellow_gp[] PROGMEM = { 0, 0, 0, 255, 63, 0, 128, 255, 127, 0, 255, 255, 191, 113, 255, 117, 255, 255, 255, 0}; //Custom palette by Aircoookie const byte Orange_Teal_gp[] PROGMEM = { 0, 0,150, 92, 55, 0,150, 92, 200, 255, 72, 0, 255, 255, 72, 0}; //Custom palette by Aircoookie const byte Tiamat_gp[] PROGMEM = { 0, 1, 2, 14, //gc 33, 2, 5, 35, //gc from 47, 61,126 100, 13,135, 92, //gc from 88,242,247 120, 43,255,193, //gc from 135,255,253 140, 247, 7,249, //gc from 252, 69,253 160, 193, 17,208, //gc from 231, 96,237 180, 39,255,154, //gc from 130, 77,213 200, 4,213,236, //gc from 57,122,248 220, 39,252,135, //gc from 177,254,255 240, 193,213,253, //gc from 203,239,253 255, 255,249,255}; //Custom palette by Aircoookie const byte April_Night_gp[] PROGMEM = { 0, 1, 5, 45, //deep blue 10, 1, 5, 45, 25, 5,169,175, //light blue 40, 1, 5, 45, 61, 1, 5, 45, 76, 45,175, 31, //green 91, 1, 5, 45, 112, 1, 5, 45, 127, 249,150, 5, //yellow 143, 1, 5, 45, 162, 1, 5, 45, 178, 255, 92, 0, //pastel orange 193, 1, 5, 45, 214, 1, 5, 45, 229, 223, 45, 72, //pink 244, 1, 5, 45, 255, 1, 5, 45}; const byte Orangery_gp[] PROGMEM = { 0, 255, 95, 23, 30, 255, 82, 0, 60, 223, 13, 8, 90, 144, 44, 2, 120, 255,110, 17, 150, 255, 69, 0, 180, 158, 13, 11, 210, 241, 82, 17, 255, 213, 37, 4}; //inspired by Mark Kriegsman https://gist.github.com/kriegsman/756ea6dcae8e30845b5a const byte C9_gp[] PROGMEM = { 0, 184, 4, 0, //red 60, 184, 4, 0, 65, 144, 44, 2, //amber 125, 144, 44, 2, 130, 4, 96, 2, //green 190, 4, 96, 2, 195, 7, 7, 88, //blue 255, 7, 7, 88}; const byte Sakura_gp[] PROGMEM = { 0, 196, 19, 10, 65, 255, 69, 45, 130, 223, 45, 72, 195, 255, 82,103, 255, 223, 13, 17}; const byte Aurora_gp[] PROGMEM = { 0, 1, 5, 45, //deep blue 64, 0,200, 23, 128, 0,255, 0, //green 170, 0,243, 45, 200, 0,135, 7, 255, 1, 5, 45};//deep blue const byte Atlantica_gp[] PROGMEM = { 0, 0, 28,112, //#001C70 50, 32, 96,255, //#2060FF 100, 0,243, 45, 150, 12, 95, 82, //#0C5F52 200, 25,190, 95, //#19BE5F 255, 40,170, 80};//#28AA50 const byte C9_2_gp[] PROGMEM = { 0, 6, 126, 2, //green 45, 6, 126, 2, 46, 4, 30, 114, //blue 90, 4, 30, 114, 91, 255, 5, 0, //red 135, 255, 5, 0, 136, 196, 57, 2, //amber 180, 196, 57, 2, 181, 137, 85, 2, //yellow 255, 137, 85, 2}; //C9, but brighter and with a less purple blue const byte C9_new_gp[] PROGMEM = { 0, 255, 5, 0, //red 60, 255, 5, 0, 61, 196, 57, 2, //amber (start 61?) 120, 196, 57, 2, 121, 6, 126, 2, //green (start 126?) 180, 6, 126, 2, 181, 4, 30, 114, //blue (start 191?) 255, 4, 30, 114}; // Gradient palette "temperature_gp", originally from // http://seaviewsensing.com/pub/cpt-city/arendal/temperature.c3g const uint8_t temperature_gp[] PROGMEM = { 0, 20, 92, 171, 14, 15, 111, 186, 28, 6, 142, 211, 42, 2, 161, 227, 56, 16, 181, 239, 70, 38, 188, 201, 84, 86, 204, 200, 99, 139, 219, 176, 113, 182, 229, 125, 127, 196, 230, 63, 141, 241, 240, 22, 155, 254, 222, 30, 170, 251, 199, 4, 184, 247, 157, 9, 198, 243, 114, 15, 226, 213, 30, 29, 240, 151, 38, 35, 255, 151, 38, 35}; // Gradient palette "bhw1_01_gp", originally from // http://seaviewsensing.com/pub/cpt-city/bhw/bhw1/bhw1_01.c3g const uint8_t retro_clown_gp[] PROGMEM = { 0, 242, 168, 38, 117, 226, 78, 80, 255, 161, 54, 225, }; // Gradient palette "bhw1_04_gp", originally from // http://seaviewsensing.com/pub/cpt-city/bhw/bhw1/bhw1_04.c3g const uint8_t candy_gp[] PROGMEM = { 0, 243, 242, 23, 15, 242, 168, 38, 142, 111, 21, 151, 198, 74, 22, 150, 255, 0, 0, 117}; // Gradient palette "bhw1_05_gp", originally from // http://seaviewsensing.com/pub/cpt-city/bhw/bhw1/bhw1_05.c3g const uint8_t toxy_reaf_gp[] PROGMEM = { 0, 2, 239, 126, 255, 145, 35, 217}; // Gradient palette "bhw1_06_gp", originally from // http://seaviewsensing.com/pub/cpt-city/bhw/bhw1/bhw1_06.c3g const uint8_t fairy_reaf_gp[] PROGMEM = { 0, 220, 19, 187, 160, 12, 225, 219, 219, 203, 242, 223, 255, 255, 255, 255}; // Gradient palette "bhw1_14_gp", originally from // http://seaviewsensing.com/pub/cpt-city/bhw/bhw1/bhw1_14.c3g const uint8_t semi_blue_gp[] PROGMEM = { 0, 0, 0, 0, 12, 24, 4, 38, 53, 55, 8, 84, 80, 43, 48, 159, 119, 31, 89, 237, 145, 50, 59, 166, 186, 71, 30, 98, 233, 31, 15, 45, 255, 0, 0, 0}; // Gradient palette "bhw1_three_gp", originally from // http://seaviewsensing.com/pub/cpt-city/bhw/bhw1/bhw1_three.c3g const uint8_t pink_candy_gp[] PROGMEM = { 0, 255, 255, 255, 45, 50, 64, 255, 112, 242, 16, 186, 140, 255, 255, 255, 155, 242, 16, 186, 196, 116, 13, 166, 255, 255, 255, 255}; // Gradient palette "bhw1_w00t_gp", originally from // http://seaviewsensing.com/pub/cpt-city/bhw/bhw1/bhw1_w00t.c3g const uint8_t red_reaf_gp[] PROGMEM = { 0, 36, 68, 114, 104, 149, 195, 248, 188, 255, 0, 0, 255, 94, 14, 9}; // Gradient palette "bhw2_23_gp", originally from // http://seaviewsensing.com/pub/cpt-city/bhw/bhw2/bhw2_23.c3g const uint8_t aqua_flash_gp[] PROGMEM = { 0, 0, 0, 0, 66, 130, 242, 245, 96, 255, 255, 53, 124, 255, 255, 255, 153, 255, 255, 53, 188, 130, 242, 245, 255, 0, 0, 0}; // Gradient palette "bhw2_xc_gp", originally from // http://seaviewsensing.com/pub/cpt-city/bhw/bhw2/bhw2_xc.c3g const uint8_t yelblu_hot_gp[] PROGMEM = { 0, 43, 30, 57, 58, 73, 0, 119, 122, 87, 0, 74, 158, 197, 57, 22, 183, 218, 117, 27, 219, 239, 177, 32, 255, 246, 247, 27, }; // Gradient palette "bhw2_45_gp", originally from // http://seaviewsensing.com/pub/cpt-city/bhw/bhw2/bhw2_45.c3g const uint8_t lite_light_gp[] PROGMEM = { 0, 0, 0, 0, 9, 20, 21, 22, 40, 46, 43, 49, 66, 46, 43, 49, 101, 61, 16, 65, 255, 0, 0, 0}; // Gradient palette "bhw2_22_gp", originally from // http://seaviewsensing.com/pub/cpt-city/bhw/bhw2/bhw2_22.c3g const uint8_t red_flash_gp[] PROGMEM = { 0, 0, 0, 0, 99, 242, 12, 8, 130, 253, 228, 163, 155, 242, 12, 8, 255, 0, 0, 0}; // Gradient palette "bhw3_40_gp", originally from // http://seaviewsensing.com/pub/cpt-city/bhw/bhw3/bhw3_40.c3g const uint8_t blink_red_gp[] PROGMEM = { 0, 4, 7, 4, 43, 40, 25, 62, 76, 61, 15, 36, 109, 207, 39, 96, 127, 255, 156, 184, 165, 185, 73, 207, 204, 105, 66, 240, 255, 77, 29, 78}; // Gradient palette "bhw3_52_gp", originally from // http://seaviewsensing.com/pub/cpt-city/bhw/bhw3/bhw3_52.c3g const uint8_t red_shift_gp[] PROGMEM = { 0, 98, 22, 93, 45, 103, 22, 73, 99, 192, 45, 56, 132, 235, 187, 59, 175, 228, 85, 26, 201, 228, 56, 48, 255, 2, 0, 2}; // Gradient palette "bhw4_097_gp", originally from // http://seaviewsensing.com/pub/cpt-city/bhw/bhw4/bhw4_097.c3g const uint8_t red_tide_gp[] PROGMEM = { 0, 251, 46, 0, 28, 255, 139, 25, 43, 246, 158, 63, 58, 246, 216, 123, 84, 243, 94, 10, 114, 177, 65, 11, 140, 255, 241, 115, 168, 177, 65, 11, 196, 250, 233, 158, 216, 255, 94, 6, 255, 126, 8, 4}; // Gradient palette "bhw4_017_gp", originally from // http://seaviewsensing.com/pub/cpt-city/bhw/bhw4/bhw4_017.c3g const uint8_t candy2_gp[] PROGMEM = { 0, 109, 102, 102, 25, 42, 49, 71, 48, 121, 96, 84, 73, 241, 214, 26, 89, 216, 104, 44, 130, 42, 49, 71, 163, 255, 177, 47, 186, 241, 214, 26, 211, 109, 102, 102, 255, 20, 19, 13}; const byte trafficlight_gp[] PROGMEM = { 0, 0, 0, 0, //black 85, 0, 255, 0, //green 170, 255, 255, 0, //yellow 255, 255, 0, 0}; //red const byte Aurora2_gp[] PROGMEM = { 0, 17, 177, 13, //Greenish 64, 121, 242, 5, //Greenish 128, 25, 173, 121, //Turquoise 192, 250, 77, 127, //Pink 255, 171, 101, 221}; //Purple // FastLed palettes, corrected with inverse gamma of 2.2 to match original looks // Party colors const TProgmemRGBPalette16 PartyColors_gc22 FL_PROGMEM = { 0x9B00D5, 0xBD00B8, 0xDA0092, 0xF3005C, 0xF45500, 0xDC8F00, 0xD5B400, 0xD5D500, 0xD59B00, 0xEF6600, 0xF90044, 0xE10086, 0xC400B0, 0xA300CF, 0x7600E8, 0x0032FC}; // Rainbow colors const TProgmemRGBPalette16 RainbowColors_gc22 FL_PROGMEM = { 0xFF0000, 0xEB7000, 0xD59B00, 0xD5BA00, 0xD5D500, 0x9CEB00, 0x00FF00, 0x00EB70, 0x00D59B, 0x009CD4, 0x0000FF, 0x7000EB, 0x9B00D5, 0xBA00BB, 0xD5009B, 0xEB0072}; // Rainbow colors with alternatating stripes of black const TProgmemRGBPalette16 RainbowStripeColors_gc22 FL_PROGMEM = { 0xFF0000, 0x000000, 0xD59B00, 0x000000, 0xD5D500, 0x000000, 0x00FF00, 0x000000, 0x00D59B, 0x000000, 0x0000FF, 0x000000, 0x9B00D5, 0x000000, 0xD5009B, 0x000000}; // array of fastled palettes (palette 6 - 12) const TProgmemRGBPalette16 *const fastledPalettes[] PROGMEM = { &PartyColors_gc22, //06-00 Party &CloudColors_p, //07-01 Cloud &LavaColors_p, //08-02 Lava &OceanColors_p, //09-03 Ocean &ForestColors_p, //10-04 Forest &RainbowColors_gc22, //11-05 Rainbow &RainbowStripeColors_gc22 //12-06 Rainbow Bands }; // Single array of defined cpt-city color palettes. // This will let us programmatically choose one based on // a number, rather than having to activate each explicitly // by name every time. const uint8_t* const gGradientPalettes[] PROGMEM = { Sunset_Real_gp, //13-00 Sunset es_rivendell_15_gp, //14-01 Rivendell es_ocean_breeze_036_gp, //15-02 Breeze rgi_15_gp, //16-03 Red & Blue retro2_16_gp, //17-04 Yellowout Analogous_1_gp, //18-05 Analogous es_pinksplash_08_gp, //19-06 Splash Sunset_Yellow_gp, //20-07 Pastel Another_Sunset_gp, //21-08 Sunset2 Beech_gp, //22-09 Beech es_vintage_01_gp, //23-10 Vintage departure_gp, //24-11 Departure es_landscape_64_gp, //25-12 Landscape es_landscape_33_gp, //26-13 Beach rainbowsherbet_gp, //27-14 Sherbet gr65_hult_gp, //28-15 Hult gr64_hult_gp, //29-16 Hult64 GMT_drywet_gp, //30-17 Drywet ib_jul01_gp, //31-18 Jul es_vintage_57_gp, //32-19 Grintage ib15_gp, //33-20 Rewhi Tertiary_01_gp, //34-21 Tertiary lava_gp, //35-22 Fire fierce_ice_gp, //36-23 Icefire Colorfull_gp, //37-24 Cyane Pink_Purple_gp, //38-25 Light Pink es_autumn_19_gp, //39-26 Autumn BlacK_Blue_Magenta_White_gp, //40-27 Magenta BlacK_Magenta_Red_gp, //41-28 Magred BlacK_Red_Magenta_Yellow_gp, //42-29 Yelmag Blue_Cyan_Yellow_gp, //43-30 Yelblu Orange_Teal_gp, //44-31 Orange & Teal Tiamat_gp, //45-32 Tiamat April_Night_gp, //46-33 April Night Orangery_gp, //47-34 Orangery C9_gp, //48-35 C9 Sakura_gp, //49-36 Sakura Aurora_gp, //50-37 Aurora Atlantica_gp, //51-38 Atlantica C9_2_gp, //52-39 C9 2 C9_new_gp, //53-40 C9 New temperature_gp, //54-41 Temperature Aurora2_gp, //55-42 Aurora 2 retro_clown_gp, //56-43 Retro Clown candy_gp, //57-44 Candy toxy_reaf_gp, //58-45 Toxy Reaf fairy_reaf_gp, //59-46 Fairy Reaf semi_blue_gp, //60-47 Semi Blue pink_candy_gp, //61-48 Pink Candy red_reaf_gp, //62-49 Red Reaf aqua_flash_gp, //63-50 Aqua Flash yelblu_hot_gp, //64-51 Yelblu Hot lite_light_gp, //65-52 Lite Light red_flash_gp, //66-53 Red Flash blink_red_gp, //67-54 Blink Red red_shift_gp, //68-55 Red Shift red_tide_gp, //69-56 Red Tide candy2_gp, //70-57 Candy2 trafficlight_gp //71-58 Traffic Light }; ================================================ FILE: wled00/pin_manager.cpp ================================================ #include "wled.h" #include "pin_manager.h" #ifdef ARDUINO_ARCH_ESP32 #ifdef bitRead // Arduino variants assume 32 bit values #undef bitRead #undef bitSet #undef bitClear #define bitRead(var,bit) (((unsigned long long)(var)>>(bit))&0x1ULL) #define bitSet(var,bit) ((var)|=(1ULL<<(bit))) #define bitClear(var,bit) ((var)&=(~(1ULL<<(bit)))) #endif #endif // Pin management state variables #ifdef ESP8266 static uint32_t pinAlloc = 0UL; // 1 bit per pin, we use first 17bits #else static uint64_t pinAlloc = 0ULL; // 1 bit per pin, we use 50 bits on ESP32-S3 static uint16_t ledcAlloc = 0; // up to 16 LEDC channels (WLED_MAX_ANALOG_CHANNELS) #endif static uint8_t i2cAllocCount = 0; // allow multiple allocation of I2C bus pins but keep track of allocations static uint8_t spiAllocCount = 0; // allow multiple allocation of SPI bus pins but keep track of allocations static PinOwner ownerTag[WLED_NUM_PINS] = { PinOwner::None }; /// Actual allocation/deallocation routines bool PinManager::deallocatePin(byte gpio, PinOwner tag) { if (gpio == 0xFF) return true; // explicitly allow clients to free -1 as a no-op if (!isPinOk(gpio, false)) return false; // but return false for any other invalid pin // if a non-zero ownerTag, only allow de-allocation if the owner's tag is provided if ((ownerTag[gpio] != PinOwner::None) && (ownerTag[gpio] != tag)) { DEBUG_PRINTF_P(PSTR("PIN DEALLOC: FAIL GPIO %d allocated by 0x%02X, but attempted de-allocation by 0x%02X.\n"), gpio, static_cast(ownerTag[gpio]), static_cast(tag)); return false; } bitWrite(pinAlloc, gpio, false); ownerTag[gpio] = PinOwner::None; return true; } // support function for deallocating multiple pins bool PinManager::deallocateMultiplePins(const uint8_t *pinArray, byte arrayElementCount, PinOwner tag) { bool shouldFail = false; DEBUG_PRINTLN(F("MULTIPIN DEALLOC")); // first verify the pins are OK and allocated by selected owner for (int i = 0; i < arrayElementCount; i++) { byte gpio = pinArray[i]; if (gpio == 0xFF) { // explicit support for io -1 as a no-op (no allocation of pin), // as this can greatly simplify configuration arrays continue; } if (isPinAllocated(gpio, tag)) { // if the current pin is allocated by selected owner it is possible to release it continue; } DEBUG_PRINTF_P(PSTR("PIN DEALLOC: FAIL GPIO %d allocated by 0x%02X, but attempted de-allocation by 0x%02X.\n"), gpio, static_cast(ownerTag[gpio]), static_cast(tag)); shouldFail = true; } if (shouldFail) { return false; // no pins deallocated } if (tag==PinOwner::HW_I2C) { if (i2cAllocCount && --i2cAllocCount>0) { // no deallocation done until last owner releases pins return true; } } if (tag==PinOwner::HW_SPI) { if (spiAllocCount && --spiAllocCount>0) { // no deallocation done until last owner releases pins return true; } } for (int i = 0; i < arrayElementCount; i++) { deallocatePin(pinArray[i], tag); } return true; } bool PinManager::deallocateMultiplePins(const managed_pin_type * mptArray, byte arrayElementCount, PinOwner tag) { uint8_t pins[arrayElementCount]; for (int i=0; i(ownerTag[gpio])); shouldFail = true; } } if (shouldFail) { return false; } if (tag==PinOwner::HW_I2C) i2cAllocCount++; if (tag==PinOwner::HW_SPI) spiAllocCount++; // all pins are available .. track each one for (int i = 0; i < arrayElementCount; i++) { byte gpio = mptArray[i].pin; if (gpio == 0xFF) { // allow callers to include -1 value as non-requested pin // as this can greatly simplify configuration arrays continue; } if (gpio >= WLED_NUM_PINS) continue; // other unexpected GPIO => avoid array bounds violation bitWrite(pinAlloc, gpio, true); ownerTag[gpio] = tag; DEBUG_PRINTF_P(PSTR("PIN ALLOC: Pin %d allocated by 0x%02X.\n"), gpio, static_cast(tag)); } DEBUG_PRINTF_P(PSTR("PIN ALLOC: 0x%014llX.\n"), (unsigned long long)pinAlloc); return true; } bool PinManager::allocateMultiplePins(const int8_t * mptArray, byte arrayElementCount, PinOwner tag, boolean output) { PinManagerPinType pins[arrayElementCount]; for (int i=0; i= WLED_NUM_PINS) || tag==PinOwner::HW_I2C || tag==PinOwner::HW_SPI || tag==PinOwner::DMX_INPUT) { #ifdef WLED_DEBUG if (gpio < 255) { // 255 (-1) is the "not defined GPIO" if (!isPinOk(gpio, output)) { DEBUG_PRINTF_P(PSTR("PIN ALLOC: FAIL for owner 0x%02X: GPIO %d "), static_cast(tag), gpio); if (output) DEBUG_PRINTLN(F(" cannot be used for i/o on this MCU.")); else DEBUG_PRINTLN(F(" cannot be used as input on this MCU.")); } else { DEBUG_PRINTF_P(PSTR("PIN ALLOC: FAIL GPIO %d - HW I2C & SPI pins have to be allocated using allocateMultiplePins.\n"), gpio); } } #endif return false; } if (isPinAllocated(gpio)) { DEBUG_PRINTF_P(PSTR("PIN ALLOC: FAIL Pin %d already allocated by 0x%02X.\n"), gpio, static_cast(ownerTag[gpio])); return false; } bitWrite(pinAlloc, gpio, true); ownerTag[gpio] = tag; DEBUG_PRINTF_P(PSTR("PIN ALLOC: Pin %d successfully allocated by 0x%02X.\n"), gpio, static_cast(ownerTag[gpio])); DEBUG_PRINTF_P(PSTR("PIN ALLOC: 0x%014llX.\n"), (unsigned long long)pinAlloc); return true; } // if tag is set to PinOwner::None, checks for ANY owner of the pin. // if tag is set to any other value, checks if that tag is the current owner of the pin. bool PinManager::isPinAllocated(byte gpio, PinOwner tag) { if (!isPinOk(gpio, false)) return true; if ((tag != PinOwner::None) && (ownerTag[gpio] != tag)) return false; return bitRead(pinAlloc, gpio); } /* see https://docs.espressif.com/projects/esp-idf/en/latest/esp32s3/api-reference/peripherals/gpio.html * The ESP32-S3 chip features 45 physical GPIO pins (GPIO0 ~ GPIO21 and GPIO26 ~ GPIO48). Each pin can be used as a general-purpose I/O * Strapping pins: GPIO0, GPIO3, GPIO45 and GPIO46 are strapping pins. For more infomation, please refer to ESP32-S3 datasheet. * Serial TX = GPIO43, RX = GPIO44; LED BUILTIN is usually GPIO39 * USB-JTAG: GPIO 19 and 20 are used by USB-JTAG by default. In order to use them as GPIOs, USB-JTAG will be disabled by the drivers. * SPI0/1: GPIO26-32 are usually used for SPI flash and PSRAM and not recommended for other uses. * When using Octal Flash or Octal PSRAM or both, GPIO33~37 are connected to SPIIO4 ~ SPIIO7 and SPIDQS. Therefore, on boards embedded with ESP32-S3R8 / ESP32-S3R8V chip, GPIO33~37 are also not recommended for other uses. * * see https://docs.espressif.com/projects/esp-idf/en/v4.4.2/esp32s3/api-reference/peripherals/adc.html * https://docs.espressif.com/projects/esp-idf/en/latest/esp32s3/api-reference/peripherals/adc_oneshot.html * ADC1: GPIO1 - GPIO10 (channel 0..9) * ADC2: GPIO11 - GPIO20 (channel 0..9) * adc_power_acquire(): Please do not use the interrupt of GPIO36 and GPIO39 when using ADC or Wi-Fi and Bluetooth with sleep mode enabled. As a workaround, call adc_power_acquire() in the APP. * Since the ADC2 module is also used by the Wi-Fi, reading operation of adc2_get_raw() may fail between esp_wifi_start() and esp_wifi_stop(). Use the return code to see whether the reading is successful. */ // Check if supplied GPIO is ok to use bool PinManager::isPinOk(byte gpio, bool output) { if (gpio >= WLED_NUM_PINS) return false; // catch error case, to avoid array out-of-bounds access #ifdef ARDUINO_ARCH_ESP32 if (digitalPinIsValid(gpio)) { #if defined(CONFIG_IDF_TARGET_ESP32C3) // strapping pins: 2, 8, & 9 if (gpio > 11 && gpio < 18) return false; // 11-17 SPI FLASH #if ARDUINO_USB_CDC_ON_BOOT == 1 || ARDUINO_USB_DFU_ON_BOOT == 1 if (gpio > 17 && gpio < 20) return false; // 18-19 USB-JTAG #endif #elif defined(CONFIG_IDF_TARGET_ESP32S3) // 00 to 18 are for general use. Be careful about straping pins GPIO0 and GPIO3 - these may be pulled-up or pulled-down on your board. #if ARDUINO_USB_CDC_ON_BOOT == 1 || ARDUINO_USB_DFU_ON_BOOT == 1 if (gpio > 18 && gpio < 21) return false; // 19 + 20 = USB-JTAG. Not recommended for other uses. #endif if (gpio > 21 && gpio < 33) return false; // 22 to 32: not connected + SPI FLASH #if CONFIG_ESPTOOLPY_FLASHMODE_OPI // 33-37: never available if using _octal_ Flash (opi_opi) if (gpio > 32 && gpio < 38) return false; #endif #if CONFIG_SPIRAM_MODE_OCT // 33-37: not available if using _octal_ PSRAM (qio_opi), but free to use on _quad_ PSRAM (qio_qspi) if (gpio > 32 && gpio < 38) return !psramFound(); #endif // 38 to 48 are for general use. Be careful about straping pins GPIO45 and GPIO46 - these may be pull-up or pulled-down on your board. #elif defined(CONFIG_IDF_TARGET_ESP32S2) // strapping pins: 0, 45 & 46 if (gpio > 21 && gpio < 33) return false; // 22 to 32: not connected + SPI FLASH // JTAG: GPIO39-42 are usually used for inline debugging // GPIO46 is input only and pulled down #else if ((strncmp_P(PSTR("ESP32-U4WDH"), ESP.getChipModel(), 11) == 0) || // this is the correct identifier, but.... (strncmp_P(PSTR("ESP32-PICO-D"), ESP.getChipModel(), 12) == 0)) { // https://github.com/espressif/arduino-esp32/issues/10683 // this chip has 4 MB of internal Flash and different packaging, so available pins are different! if ((gpio > 5 && gpio < 9) || gpio == 11) return false; // U4WDH/PICO-D2 & PICO-D4: GPIO 6, 7, 8, 11 are used for SPI flash; 9 & 10 are free if (gpio == 16 || gpio == 17) return false; // U4WDH/PICO-D?: GPIO 16 and 17 are used for PSRAM } else if (strncmp_P(PSTR("ESP32-PICO-V3"), ESP.getChipModel(), 13) == 0) { if (gpio == 6 || gpio == 11) return false; // PICO-V3: uses GPIO 6 and 11 for flash if (strstr_P(ESP.getChipModel(), PSTR("V3-02")) != nullptr && (gpio == 9 || gpio == 10)) return false; // PICO-V3-02: uses GPIO 9 and 10 for PSRAM; 7, 8 are free } else { // for classic ESP32 (non-mini) modules, these are the SPI flash pins if (gpio > 5 && gpio < 12) return false; //SPI flash pins } if (gpio == 16) return !psramFound(); // PSRAM pins on modules with off-package or in-package PSRAM if (gpio == 17) { if (strncmp_P(PSTR("ESP32-D0WDR2-V3"), ESP.getChipModel(), 15) == 0) { return true; } else { return !psramFound(); // PSRAM pins on modules with in-package PSRAM } } #endif if (output) return digitalPinCanOutput(gpio); else return true; } #else if (gpio < 6) return true; if (gpio < 12) return false; //SPI flash pins if (gpio < 17) return true; #endif return false; } bool PinManager::isReadOnlyPin(byte gpio) { #ifdef ARDUINO_ARCH_ESP32 if (gpio < WLED_NUM_PINS) return (digitalPinIsValid(gpio) && !digitalPinCanOutput(gpio)); #endif return false; } PinOwner PinManager::getPinOwner(byte gpio) { if (!isPinOk(gpio, false)) return PinOwner::None; return ownerTag[gpio]; } #ifdef ARDUINO_ARCH_ESP32 byte PinManager::allocateLedc(byte channels) { if (channels > WLED_MAX_ANALOG_CHANNELS || channels == 0) return 255; unsigned ca = 0; for (unsigned i = 0; i < WLED_MAX_ANALOG_CHANNELS; i++) { if (bitRead(ledcAlloc, i)) { //found occupied pin ca = 0; } else { // if we have PWM CCT bus allocation (2 channels) we need to make sure both channels share the same timer // for phase shifting purposes (otherwise phase shifts may not be accurate) if (channels == 2) { // will skip odd channel for first channel for phase shifting if (ca == 0 && i % 2 == 0) ca++; // even LEDC channels is 1st PWM channel if (ca == 1 && i % 2 == 1) ca++; // odd LEDC channel is 2nd PWM channel } else ca++; } if (ca >= channels) { //enough free channels unsigned in = (i + 1) - ca; for (unsigned j = 0; j < ca; j++) { bitWrite(ledcAlloc, in+j, true); } return in; } } return 255; //not enough consecutive free LEDC channels } void PinManager::deallocateLedc(byte pos, byte channels) { for (unsigned j = pos; j < pos + channels && j < WLED_MAX_ANALOG_CHANNELS; j++) { bitWrite(ledcAlloc, j, false); } } #endif // Convert PinOwner enum to string for allocated pins const char* PinManager::getPinOwnerName(uint8_t gpio) { PinOwner owner = PinManager::getPinOwner(gpio); // returns "none" if allocated by system, unallocated or unavailable switch (owner) { case PinOwner::None: return PinManager::isPinAllocated(gpio) ? "System" : "Unknown"; case PinOwner::Ethernet: return "Ethernet"; case PinOwner::BusDigital: return "LED Digital"; case PinOwner::BusOnOff: return "LED On/Off"; case PinOwner::BusPwm: return "LED PWM"; case PinOwner::Button: return "Button"; case PinOwner::IR: return "IR Receiver"; case PinOwner::Relay: return "Relay"; case PinOwner::SPI_RAM: return "SPI RAM"; case PinOwner::DebugOut: return "Debug"; case PinOwner::DMX: return "DMX Output"; case PinOwner::HW_I2C: return "I2C"; case PinOwner::HW_SPI: return "SPI"; case PinOwner::DMX_INPUT: return "DMX Input"; case PinOwner::HUB75: return "HUB75"; // Usermods - return generic name for now // TODO: Get actual usermod name from UsermodManager default: // Check if it's a usermod (high bit not set) if (static_cast(owner) > 0 && !(static_cast(owner) & 0x80)) { return "Usermod"; } return "Unknown"; } } int PinManager::getButtonIndex(byte gpio) { for (size_t b = 0; b < buttons.size(); b++) { if (buttons[b].pin == gpio && buttons[b].type != BTN_TYPE_NONE) { return b; } } return -1; } bool PinManager::isAnalogPin(byte gpio) { #ifdef ARDUINO_ARCH_ESP32 // Check ADC capability: only ADC1 channels can be used (ADC2 channels are not usable when WiFi is active) #if CONFIG_IDF_TARGET_ESP32 // ESP32: ADC1 channels 0-7 (GPIO 36, 37, 38, 39, 32, 33, 34, 35) int adc_channel = digitalPinToAnalogChannel(gpio); if (adc_channel >= 0 && adc_channel <= 7) return true; #elif CONFIG_IDF_TARGET_ESP32S2 // ESP32-S2: ADC1 channels 0-9 (GPIO 1-10) int adc_channel = digitalPinToAnalogChannel(gpio); if (adc_channel >= 0 && adc_channel <= 9) return true; #elif CONFIG_IDF_TARGET_ESP32S3 // ESP32-S3: ADC1 channels 0-9 (GPIO 1-10) int adc_channel = digitalPinToAnalogChannel(gpio); if (adc_channel >= 0 && adc_channel <= 9) return true; #elif CONFIG_IDF_TARGET_ESP32C3 // ESP32-C3: ADC1 channels 0-4 (GPIO 0-4) int adc_channel = digitalPinToAnalogChannel(gpio); if (adc_channel >= 0 && adc_channel <= 4) return true; #endif #endif return false; // not an analog pin if it doesn't have ADC capability, ESP8266 has only one ADC pin (A0) which is handled separately in button.cpp, so return false for all pins here } ================================================ FILE: wled00/pin_manager.h ================================================ #ifndef WLED_PIN_MANAGER_H #define WLED_PIN_MANAGER_H /* * Registers pins so there is no attempt for two interfaces to use the same pin */ #ifdef ESP8266 #define WLED_NUM_PINS (GPIO_PIN_COUNT+1) // somehow they forgot GPIO 16 (0-16==17) #else #define WLED_NUM_PINS (GPIO_PIN_COUNT) #endif // Pin capability flags - only "special" capabilities useful for debugging (note: touch capability is provided by appendGPIOinfo() via d.touch) #define PIN_CAP_ADC 0x02 // has ADC capability (analog input) #define PIN_CAP_PWM 0x04 // can be used for PWM (analog LED output) -> unused, all pins can use ledc PWM #define PIN_CAP_BOOT 0x08 // bootloader pin #define PIN_CAP_BOOTSTRAP 0x10 // bootstrap pin (strapping pin affecting boot mode) #define PIN_CAP_INPUT_ONLY 0x20 // input only pin (cannot be used as output) typedef struct PinManagerPinType { int8_t pin; bool isOutput; } managed_pin_type; /* * Allows PinManager to "lock" an allocation to a specific * owner, so someone else doesn't accidentally de-allocate * a pin it hasn't allocated. Also enhances debugging. * * RAM Cost: * 17 bytes on ESP8266 * 40 bytes on ESP32 */ enum struct PinOwner : uint8_t { None = 0, // default == legacy == unspecified owner // High bit is set for all built-in pin owners Ethernet = 0x81, BusDigital = 0x82, BusOnOff = 0x83, BusPwm = 0x84, // 'BusP' == PWM output using BusPwm Button = 0x85, // 'Butn' == button from configuration IR = 0x86, // 'IR' == IR receiver pin from configuration Relay = 0x87, // 'Rly' == Relay pin from configuration SPI_RAM = 0x88, // 'SpiR' == SPI RAM DebugOut = 0x89, // 'Dbg' == debug output always IO1 DMX = 0x8A, // 'DMX' == hard-coded to IO2 HW_I2C = 0x8B, // 'I2C' == hardware I2C pins (4&5 on ESP8266, 21&22 on ESP32) HW_SPI = 0x8C, // 'SPI' == hardware (V)SPI pins (13,14&15 on ESP8266, 5,18&23 on ESP32) DMX_INPUT = 0x8D, // 'DMX_INPUT' == DMX input via serial HUB75 = 0x8E, // 'Hub75' == Hub75 driver // Use UserMod IDs from const.h here UM_Unspecified = USERMOD_ID_UNSPECIFIED, // 0x01 UM_Example = USERMOD_ID_EXAMPLE, // 0x02 // Usermod "usermod_v2_example.h" UM_Temperature = USERMOD_ID_TEMPERATURE, // 0x03 // Usermod "usermod_temperature.h" // #define USERMOD_ID_FIXNETSERVICES // 0x04 // Usermod "usermod_Fix_unreachable_netservices.h" -- Does not allocate pins UM_PIR = USERMOD_ID_PIRSWITCH, // 0x05 // Usermod "usermod_PIR_sensor_switch.h" UM_IMU = USERMOD_ID_IMU, // 0x06 // Usermod "usermod_mpu6050_imu.h" -- Interrupt pin UM_FourLineDisplay = USERMOD_ID_FOUR_LINE_DISP, // 0x07 // Usermod "usermod_v2_four_line_display.h -- May use "standard" HW_I2C pins UM_RotaryEncoderUI = USERMOD_ID_ROTARY_ENC_UI, // 0x08 // Usermod "usermod_v2_rotary_encoder_ui.h" // #define USERMOD_ID_AUTO_SAVE // 0x09 // Usermod "usermod_v2_auto_save.h" -- Does not allocate pins // #define USERMOD_ID_DHT // 0x0A // Usermod "usermod_dht.h" -- Statically allocates pins, not compatible with pinManager? // #define USERMOD_ID_VL53L0X // 0x0C // Usermod "usermod_vl53l0x_gestures.h" -- Uses "standard" HW_I2C pins UM_MultiRelay = USERMOD_ID_MULTI_RELAY, // 0x0D // Usermod "usermod_multi_relay.h" UM_AnimatedStaircase = USERMOD_ID_ANIMATED_STAIRCASE, // 0x0E // Usermod "Animated_Staircase.h" UM_Battery = USERMOD_ID_BATTERY, // // #define USERMOD_ID_RTC // 0x0F // Usermod "usermod_rtc.h" -- Uses "standard" HW_I2C pins // #define USERMOD_ID_ELEKSTUBE_IPS // 0x10 // Usermod "usermod_elekstube_ips.h" -- Uses quite a few pins ... see Hardware.h and User_Setup.h // #define USERMOD_ID_SN_PHOTORESISTOR // 0x11 // Usermod "usermod_sn_photoresistor.h" -- Uses hard-coded pin (PHOTORESISTOR_PIN == A0), but could be easily updated to use pinManager UM_BH1750 = USERMOD_ID_BH1750, // 0x14 // Usermod "bh1750.h -- Uses "standard" HW_I2C pins UM_RGBRotaryEncoder = USERMOD_RGB_ROTARY_ENCODER, // 0x16 // Usermod "rgb-rotary-encoder.h" UM_QuinLEDAnPenta = USERMOD_ID_QUINLED_AN_PENTA, // 0x17 // Usermod "quinled-an-penta.h" UM_BME280 = USERMOD_ID_BME280, // 0x1E // Usermod "usermod_bme280.h -- Uses "standard" HW_I2C pins UM_Audioreactive = USERMOD_ID_AUDIOREACTIVE, // 0x20 // Usermod "audio_reactive.h" UM_SdCard = USERMOD_ID_SD_CARD, // 0x25 // Usermod "usermod_sd_card.h" UM_PWM_OUTPUTS = USERMOD_ID_PWM_OUTPUTS, // 0x26 // Usermod "usermod_pwm_outputs.h" UM_LDR_DUSK_DAWN = USERMOD_ID_LDR_DUSK_DAWN, // 0x2B // Usermod "usermod_LDR_Dusk_Dawn_v2.h" UM_MAX17048 = USERMOD_ID_MAX17048, // 0x2F // Usermod "usermod_max17048.h" UM_BME68X = USERMOD_ID_BME68X, // 0x31 // Usermod "usermod_bme68x.h -- Uses "standard" HW_I2C pins UM_PIXELS_DICE_TRAY = USERMOD_ID_PIXELS_DICE_TRAY // 0x35 // Usermod "pixels_dice_tray.h" -- Needs compile time specified 6 pins for display including SPI. }; static_assert(0u == static_cast(PinOwner::None), "PinOwner::None must be zero, so default array initialization works as expected"); namespace PinManager { // De-allocates a single pin bool deallocatePin(byte gpio, PinOwner tag); // De-allocates multiple pins but only if all can be deallocated (PinOwner has to be specified) bool deallocateMultiplePins(const uint8_t *pinArray, byte arrayElementCount, PinOwner tag); bool deallocateMultiplePins(const managed_pin_type *pinArray, byte arrayElementCount, PinOwner tag); // Allocates a single pin, with an owner tag. // De-allocation requires the same owner tag (or override) bool allocatePin(byte gpio, bool output, PinOwner tag); // Allocates all the pins, or allocates none of the pins, with owner tag. // Provided to simplify error condition handling in clients // using more than one pin, such as I2C, SPI, rotary encoders, // ethernet, etc.. bool allocateMultiplePins(const managed_pin_type * mptArray, byte arrayElementCount, PinOwner tag ); bool allocateMultiplePins(const int8_t * mptArray, byte arrayElementCount, PinOwner tag, boolean output); [[deprecated("Replaced by three-parameter allocatePin(gpio, output, ownerTag), for improved debugging")]] inline bool allocatePin(byte gpio, bool output = true) { return allocatePin(gpio, output, PinOwner::None); } [[deprecated("Replaced by two-parameter deallocatePin(gpio, ownerTag), for improved debugging")]] inline void deallocatePin(byte gpio) { deallocatePin(gpio, PinOwner::None); } // will return true for reserved pins bool isPinAllocated(byte gpio, PinOwner tag = PinOwner::None); // will return false for reserved pins bool isPinOk(byte gpio, bool output = true); bool isReadOnlyPin(byte gpio); int getButtonIndex(byte gpio); // returns button index if pin is used for button, otherwise -1 bool isAnalogPin(byte gpio); // returns true if pin has ADC capability, otherwise false PinOwner getPinOwner(byte gpio); const char* getPinOwnerName(uint8_t gpio); #ifdef ARDUINO_ARCH_ESP32 byte allocateLedc(byte channels); void deallocateLedc(byte pos, byte channels); #endif }; //extern PinManager pinManager; #endif ================================================ FILE: wled00/playlist.cpp ================================================ #include "wled.h" /* * Handles playlists, timed sequences of presets */ typedef struct PlaylistEntry { uint8_t preset; //ID of the preset to apply uint16_t dur; //Duration of the entry (in tenths of seconds) uint16_t tr; //Duration of the transition TO this entry (in tenths of seconds) } ple; static byte playlistRepeat = 1; //how many times to repeat the playlist (0 = infinitely) static byte playlistEndPreset = 0; //what preset to apply after playlist end (0 = stay on last preset) static byte playlistOptions = 0; //bit 0: shuffle playlist after each iteration. bits 1-7 TBD static PlaylistEntry *playlistEntries = nullptr; static byte playlistLen; //number of playlist entries static int8_t playlistIndex = -1; static uint16_t playlistEntryDur = 0; //duration of the current entry in tenths of seconds //values we need to keep about the parent playlist while inside sub-playlist static int16_t parentPlaylistIndex = -1; static byte parentPlaylistRepeat = 0; static byte parentPlaylistPresetId = 0; //for re-loading void shufflePlaylist() { int currentIndex = playlistLen; PlaylistEntry temporaryValue; // While there remain elements to shuffle... while (currentIndex--) { // Pick a random element... int randomIndex = random(0, currentIndex); // And swap it with the current element. temporaryValue = playlistEntries[currentIndex]; playlistEntries[currentIndex] = playlistEntries[randomIndex]; playlistEntries[randomIndex] = temporaryValue; } DEBUG_PRINTLN(F("Playlist shuffle.")); } void unloadPlaylist() { if (playlistEntries != nullptr) { delete[] playlistEntries; playlistEntries = nullptr; } currentPlaylist = playlistIndex = -1; playlistLen = playlistEntryDur = playlistOptions = 0; DEBUG_PRINTLN(F("Playlist unloaded.")); } int16_t loadPlaylist(JsonObject playlistObj, byte presetId) { if (currentPlaylist > 0 && parentPlaylistPresetId > 0) return -1; // we are already in nested playlist, do nothing if (currentPlaylist > 0) { parentPlaylistIndex = playlistIndex; parentPlaylistRepeat = playlistRepeat; parentPlaylistPresetId = currentPlaylist; } unloadPlaylist(); JsonArray presets = playlistObj["ps"]; playlistLen = presets.size(); if (playlistLen == 0) return -1; if (playlistLen > 100) playlistLen = 100; playlistEntries = new(std::nothrow) PlaylistEntry[playlistLen]; if (playlistEntries == nullptr) return -1; byte it = 0; for (int ps : presets) { if (it >= playlistLen) break; playlistEntries[it].preset = ps; it++; } it = 0; JsonArray durations = playlistObj["dur"]; if (durations.isNull()) { playlistEntries[0].dur = playlistObj["dur"] | 100; //10 seconds as fallback it = 1; } else { for (int dur : durations) { if (it >= playlistLen) break; playlistEntries[it].dur = constrain(dur, 0, 65530); it++; } } if (it > 0) // should never happen but just in case for (int i = it; i < playlistLen; i++) playlistEntries[i].dur = playlistEntries[it -1].dur; it = 0; JsonArray tr = playlistObj[F("transition")]; if (tr.isNull()) { playlistEntries[0].tr = playlistObj[F("transition")] | (transitionDelay / 100); it = 1; } else { for (int transition : tr) { if (it >= playlistLen) break; playlistEntries[it].tr = transition; it++; } } for (int i = it; i < playlistLen; i++) playlistEntries[i].tr = playlistEntries[it -1].tr; int rep = playlistObj[F("repeat")]; bool shuffle = false; if (rep < 0) { //support negative values as infinite + shuffle rep = 0; shuffle = true; } playlistRepeat = rep; if (playlistRepeat > 0) playlistRepeat++; //add one extra repetition immediately since it will be deducted on first start playlistEndPreset = playlistObj["end"] | 0; // if end preset is 255 restore original preset (if any running) upon playlist end if (playlistEndPreset == 255 && currentPreset > 0) { playlistEndPreset = currentPreset; playlistOptions |= PL_OPTION_RESTORE; // for async save operation } if (playlistEndPreset > 250) playlistEndPreset = 0; shuffle = shuffle || playlistObj["r"]; if (shuffle) playlistOptions |= PL_OPTION_SHUFFLE; if (parentPlaylistPresetId == 0 && parentPlaylistIndex > -1) { // we are re-loading playlist when returning from nested playlist playlistIndex = parentPlaylistIndex; playlistRepeat = parentPlaylistRepeat; parentPlaylistIndex = -1; parentPlaylistRepeat = 0; } else if (rep == 0) { // endless playlist will never return to parent so erase parent information if it was called from it parentPlaylistPresetId = 0; parentPlaylistIndex = -1; parentPlaylistRepeat = 0; } currentPlaylist = presetId; DEBUG_PRINTLN(F("Playlist loaded.")); return currentPlaylist; } void handlePlaylist() { static unsigned long presetCycledTime = 0; if (currentPlaylist < 0 || playlistEntries == nullptr) return; if ((playlistEntryDur < UINT16_MAX && millis() - presetCycledTime > 100 * playlistEntryDur) || doAdvancePlaylist) { presetCycledTime = millis(); if (bri == 0 || nightlightActive) return; ++playlistIndex %= playlistLen; // -1 at 1st run (limit to playlistLen) // playlist roll-over if (!playlistIndex) { if (playlistRepeat == 1) { //stop if all repetitions are done unloadPlaylist(); if (parentPlaylistPresetId > 0) { applyPresetFromPlaylist(parentPlaylistPresetId); // reload previous playlist (unfortunately asynchronous) parentPlaylistPresetId = 0; // reset previous playlist but do not reset Index or Repeat (they will be loaded & reset in loadPlaylist()) } else if (playlistEndPreset) applyPresetFromPlaylist(playlistEndPreset); return; } if (playlistRepeat > 1) playlistRepeat--; // decrease repeat count on each index reset if not an endless playlist // playlistRepeat == 0: endless loop if (playlistOptions & PL_OPTION_SHUFFLE) shufflePlaylist(); // shuffle playlist and start over } jsonTransitionOnce = true; strip.setTransition(playlistEntries[playlistIndex].tr * 100); playlistEntryDur = playlistEntries[playlistIndex].dur > 0 ? playlistEntries[playlistIndex].dur : UINT16_MAX; applyPresetFromPlaylist(playlistEntries[playlistIndex].preset); doAdvancePlaylist = false; } } void serializePlaylist(JsonObject sObj) { JsonObject playlist = sObj.createNestedObject(F("playlist")); JsonArray ps = playlist.createNestedArray("ps"); JsonArray dur = playlist.createNestedArray("dur"); JsonArray transition = playlist.createNestedArray(F("transition")); playlist[F("repeat")] = (playlistIndex < 0 && playlistRepeat > 0) ? playlistRepeat - 1 : playlistRepeat; // remove added repetition count (if not yet running) playlist["end"] = playlistOptions & PL_OPTION_RESTORE ? 255 : playlistEndPreset; playlist["r"] = playlistOptions & PL_OPTION_SHUFFLE; for (int i=0; ito(); DEBUG_PRINTLN(F("Serialize current state")); if (playlistSave) { serializePlaylist(sObj); if (includeBri) sObj["on"] = true; } else { serializeState(sObj, true, includeBri, segBounds, selectedOnly); } if (saveName) sObj["n"] = saveName; else sObj["n"] = F("Unkonwn preset"); // should not happen, but just in case... if (quickLoad && quickLoad[0]) sObj[F("ql")] = quickLoad; if (saveLedmap >= 0) sObj[F("ledmap")] = saveLedmap; /* #ifdef WLED_DEBUG DEBUG_PRINTLN(F("Serialized preset")); serializeJson(*pDoc,Serial); DEBUG_PRINTLN(); #endif */ #if defined(ARDUINO_ARCH_ESP32) if (!persist) { p_free(tmpRAMbuffer); size_t len = measureJson(*pDoc) + 1; // if possible use SPI RAM on ESP32 tmpRAMbuffer = (char*)p_malloc(len); if (tmpRAMbuffer!=nullptr) { serializeJson(*pDoc, tmpRAMbuffer, len); } else { writeObjectToFileUsingId(getPresetsFileName(persist), presetToSave, pDoc); } } else #endif writeObjectToFileUsingId(getPresetsFileName(persist), presetToSave, pDoc); if (persist) presetsModifiedTime = toki.second(); //unix time releaseJSONBufferLock(); updateFSInfo(); // clean up saveLedmap = -1; presetToSave = 0; p_free(saveName); p_free(quickLoad); saveName = nullptr; quickLoad = nullptr; playlistSave = false; } bool getPresetName(byte index, String& name) { if (!requestJSONBufferLock(JSON_LOCK_PRESET_NAME)) return false; bool presetExists = false; if (readObjectFromFileUsingId(getPresetsFileName(), index, pDoc)) { JsonObject fdo = pDoc->as(); if (fdo["n"]) { name = (const char*)(fdo["n"]); presetExists = true; } } releaseJSONBufferLock(); return presetExists; } void initPresetsFile() { char fileName[33]; strncpy_P(fileName, getPresetsFileName(), 32); fileName[32] = 0; //use PROGMEM safe copy as FS.open() does not if (WLED_FS.exists(fileName)) return; StaticJsonDocument<64> doc; JsonObject sObj = doc.to(); sObj.createNestedObject("0"); File f = WLED_FS.open(fileName, "w"); if (!f) { errorFlag = ERR_FS_GENERAL; return; } serializeJson(doc, f); f.close(); } bool applyPresetFromPlaylist(byte index) { DEBUG_PRINTF_P(PSTR("Request to apply preset: %d\n"), index); presetToApply = index; callModeToApply = CALL_MODE_DIRECT_CHANGE; return true; } bool applyPreset(byte index, byte callMode) { unloadPlaylist(); // applying a preset unloads the playlist (#3827) DEBUG_PRINTF_P(PSTR("Request to apply preset: %u\n"), index); presetToApply = index; callModeToApply = callMode; return true; } // apply preset or fallback to a effect and palette if it doesn't exist void applyPresetWithFallback(uint8_t index, uint8_t callMode, uint8_t effectID, uint8_t paletteID) { applyPreset(index, callMode); //these two will be overwritten if preset exists in handlePresets() effectCurrent = effectID; effectPalette = paletteID; } void handlePresets() { byte presetErrFlag = ERR_NONE; if (presetToSave) { strip.suspend(); doSaveState(); strip.resume(); return; } if (presetToApply == 0 || !requestJSONBufferLock(JSON_LOCK_PRESET_LOAD)) return; // no preset waiting to apply, or JSON buffer is already allocated, return to loop until free bool changePreset = false; uint8_t tmpPreset = presetToApply; // store temporary since deserializeState() may call applyPreset() uint8_t tmpMode = callModeToApply; JsonObject fdo; presetToApply = 0; //clear request for preset callModeToApply = 0; DEBUG_PRINTF_P(PSTR("Applying preset: %u\n"), (unsigned)tmpPreset); #if defined(ARDUINO_ARCH_ESP32S2) || defined(ARDUINO_ARCH_ESP32C3) unsigned long maxWait = millis() + strip.getFrameTime(); while (strip.isUpdating() && millis() < maxWait) delay(1); // wait for strip to finish updating, accessing FS during sendout causes glitches #endif #ifdef ARDUINO_ARCH_ESP32 if (tmpPreset==255 && tmpRAMbuffer!=nullptr) { deserializeJson(*pDoc,tmpRAMbuffer); } else #endif { presetErrFlag = readObjectFromFileUsingId(getPresetsFileName(tmpPreset < 255), tmpPreset, pDoc) ? ERR_NONE : ERR_FS_PLOAD; } fdo = pDoc->as(); // only reset errorflag if previous error was preset-related if ((errorFlag == ERR_NONE) || (errorFlag == ERR_FS_PLOAD)) errorFlag = presetErrFlag; //HTTP API commands const char* httpwin = fdo["win"]; if (httpwin) { String apireq = "win"; // reduce flash string usage apireq += F("&IN&"); // internal call apireq += httpwin; handleSet(nullptr, apireq, false); // may call applyPreset() via PL= setValuesFromFirstSelectedSeg(); // fills legacy values changePreset = true; } else { if (!fdo["seg"].isNull() || !fdo["on"].isNull() || !fdo["bri"].isNull() || !fdo["nl"].isNull() || !fdo["ps"].isNull() || !fdo[F("playlist")].isNull()) changePreset = true; if (!(tmpMode == CALL_MODE_BUTTON_PRESET && fdo["ps"].is() && strchr(fdo["ps"].as(),'~') != strrchr(fdo["ps"].as(),'~'))) fdo.remove("ps"); // remove load request for presets to prevent recursive crash (if not called by button and contains preset cycling string "1~5~") deserializeState(fdo, CALL_MODE_NO_NOTIFY, tmpPreset); // may change presetToApply by calling applyPreset() } if (!errorFlag && tmpPreset < 255 && changePreset) currentPreset = tmpPreset; #if defined(ARDUINO_ARCH_ESP32) //Aircoookie recommended not to delete buffer if (tmpPreset==255 && tmpRAMbuffer!=nullptr) { p_free(tmpRAMbuffer); tmpRAMbuffer = nullptr; } #endif releaseJSONBufferLock(); if (changePreset) notify(tmpMode); // force UDP notification stateUpdated(tmpMode); // was colorUpdated() if anything breaks updateInterfaces(tmpMode); } //called from handleSet(PS=) [network callback (sObj is empty), IR (irrational), deserializeState, UDP] and deserializeState() [network callback (filedoc!=nullptr)] void savePreset(byte index, const char* pname, JsonObject sObj) { if (!saveName) saveName = static_cast(p_malloc(33)); if (!quickLoad) quickLoad = static_cast(p_malloc(9)); if (!saveName || !quickLoad) return; if (index == 0 || (index > 250 && index < 255)) return; if (pname) strlcpy(saveName, pname, 33); else { if (sObj["n"].is()) strlcpy(saveName, sObj["n"].as(), 33); else sprintf_P(saveName, PSTR("Preset %d"), index); } DEBUG_PRINTF_P(PSTR("Saving preset (%d) %s\n"), index, saveName); presetToSave = index; playlistSave = false; if (sObj[F("ql")].is()) strlcpy(quickLoad, sObj[F("ql")].as(), 9); // client limits QL to 2 chars, buffer for 8 bytes to allow unicode else quickLoad[0] = 0; const char *bootPS = PSTR("bootps"); if (!sObj[FPSTR(bootPS)].isNull()) { bootPreset = sObj[FPSTR(bootPS)] | bootPreset; sObj.remove(FPSTR(bootPS)); configNeedsWrite = true; } if (sObj.size()==0 || sObj["o"].isNull()) { // no "o" means not a playlist or custom API call, saving of state is async (not immediately) includeBri = sObj["ib"].as() || sObj.size()==0 || index==255; // temporary preset needs brightness segBounds = sObj["sb"].as() || sObj.size()==0 || index==255; // temporary preset needs bounds selectedOnly = sObj[F("sc")].as(); saveLedmap = sObj[F("ledmap")] | -1; } else { // this is a playlist or API call if (sObj[F("playlist")].isNull()) { // we will save API call immediately (often causes presets.json corruption) presetToSave = 0; if (index <= 250) { // cannot save API calls to temporary preset (255) sObj.remove("o"); sObj.remove("v"); sObj.remove("time"); sObj.remove(F("error")); sObj.remove(F("psave")); if (sObj["n"].isNull()) sObj["n"] = saveName; initPresetsFile(); // just in case if someone deleted presets.json using /edit writeObjectToFileUsingId(getPresetsFileName(), index, pDoc); presetsModifiedTime = toki.second(); //unix time updateFSInfo(); } p_free(saveName); p_free(quickLoad); saveName = nullptr; quickLoad = nullptr; } else { // store playlist // WARNING: playlist will be loaded in json.cpp after this call and will have repeat counter increased by 1 it will also be randomised if selected includeBri = true; // !sObj["on"].isNull(); playlistSave = true; } } } void deletePreset(byte index) { StaticJsonDocument<24> empty; writeObjectToFileUsingId(getPresetsFileName(), index, &empty); presetsModifiedTime = toki.second(); //unix time updateFSInfo(); } ================================================ FILE: wled00/remote.cpp ================================================ #include "wled.h" #ifndef WLED_DISABLE_ESPNOW #define ESPNOW_BUSWAIT_TIMEOUT 24 // one frame timeout to wait for bus to finish updating #define NIGHT_MODE_DEACTIVATED -1 #define NIGHT_MODE_BRIGHTNESS 5 #define WIZMOTE_BUTTON_ON 1 #define WIZMOTE_BUTTON_OFF 2 #define WIZMOTE_BUTTON_NIGHT 3 #define WIZMOTE_BUTTON_ONE 16 #define WIZMOTE_BUTTON_TWO 17 #define WIZMOTE_BUTTON_THREE 18 #define WIZMOTE_BUTTON_FOUR 19 #define WIZMOTE_BUTTON_FIVE 20 #define WIZMOTE_BUTTON_SIX 21 #define WIZMOTE_BUTTON_SEVEN 22 #define WIZMOTE_BUTTON_BRIGHT_UP 9 #define WIZMOTE_BUTTON_BRIGHT_DOWN 8 #define WIZ_SMART_BUTTON_ON 100 #define WIZ_SMART_BUTTON_OFF 101 #define WIZ_SMART_BUTTON_BRIGHT_UP 102 #define WIZ_SMART_BUTTON_BRIGHT_DOWN 103 // This is kind of an esoteric strucure because it's pulled from the "Wizmote" // product spec. That remote is used as the baseline for behavior and availability // since it's broadly commercially available and works out of the box as a drop-in typedef struct WizMoteMessageStructure { uint8_t program; // 0x91 for ON button, 0x81 for all others uint8_t seq[4]; // Incremetal sequence number 32 bit unsigned integer LSB first uint8_t dt1; // Button Data Type (0x20) uint8_t button; // Identifies which button is being pressed uint8_t dt2; // Battery Level Data Type (0x01) uint8_t batLevel; // Battery Level 0-100 uint8_t byte10; // Unknown, maybe checksum uint8_t byte11; // Unknown, maybe checksum uint8_t byte12; // Unknown, maybe checksum uint8_t byte13; // Unknown, maybe checksum } message_structure_t; static uint32_t last_seq = UINT32_MAX; static int brightnessBeforeNightMode = NIGHT_MODE_DEACTIVATED; static int16_t ESPNowButton = -1; // set in callback if new button value is received // Pulled from the IR Remote logic but reduced to 10 steps with a constant of 3 static const byte brightnessSteps[] = { 6, 9, 14, 22, 33, 50, 75, 113, 170, 255 }; static const size_t numBrightnessSteps = sizeof(brightnessSteps) / sizeof(byte); inline bool nightModeActive() { return brightnessBeforeNightMode != NIGHT_MODE_DEACTIVATED; } static void activateNightMode() { if (nightModeActive()) return; brightnessBeforeNightMode = bri; bri = NIGHT_MODE_BRIGHTNESS; stateUpdated(CALL_MODE_BUTTON); } static bool resetNightMode() { if (!nightModeActive()) return false; bri = brightnessBeforeNightMode; brightnessBeforeNightMode = NIGHT_MODE_DEACTIVATED; stateUpdated(CALL_MODE_BUTTON); return true; } // increment `bri` to the next `brightnessSteps` value static void brightnessUp() { if (nightModeActive()) return; // dumb incremental search is efficient enough for so few items for (unsigned index = 0; index < numBrightnessSteps; ++index) { if (brightnessSteps[index] > bri) { bri = brightnessSteps[index]; break; } } stateUpdated(CALL_MODE_BUTTON); } // decrement `bri` to the next `brightnessSteps` value static void brightnessDown() { if (nightModeActive()) return; // dumb incremental search is efficient enough for so few items for (int index = numBrightnessSteps - 1; index >= 0; --index) { if (brightnessSteps[index] < bri) { bri = brightnessSteps[index]; break; } } stateUpdated(CALL_MODE_BUTTON); } static void setOn() { resetNightMode(); if (!bri) { toggleOnOff(); stateUpdated(CALL_MODE_BUTTON); } } static void setOff() { resetNightMode(); if (bri) { toggleOnOff(); stateUpdated(CALL_MODE_BUTTON); } } static void presetWithFallback(uint8_t presetID, uint8_t effectID, uint8_t paletteID) { resetNightMode(); applyPresetWithFallback(presetID, CALL_MODE_BUTTON_PRESET, effectID, paletteID); } // this function follows the same principle as decodeIRJson() static bool remoteJson(int button) { char objKey[10]; bool parsed = false; if (!requestJSONBufferLock(JSON_LOCK_REMOTE)) return false; sprintf_P(objKey, PSTR("\"%d\":"), button); unsigned long start = millis(); while (strip.isUpdating() && millis()-start < ESPNOW_BUSWAIT_TIMEOUT) yield(); // wait for strip to finish updating, accessing FS during sendout causes glitches // attempt to read command from remote.json readObjectFromFile(PSTR("/remote.json"), objKey, pDoc); JsonObject fdo = pDoc->as(); if (fdo.isNull()) { // the received button does not exist //if (!WLED_FS.exists(F("/remote.json"))) errorFlag = ERR_FS_RMLOAD; //warn if file itself doesn't exist releaseJSONBufferLock(); return parsed; } String cmdStr = fdo["cmd"].as(); JsonObject jsonCmdObj = fdo["cmd"]; //object if (jsonCmdObj.isNull()) // we could also use: fdo["cmd"].is() { if (cmdStr.startsWith("!")) { // call limited set of C functions if (cmdStr.startsWith(F("!incBri"))) { brightnessUp(); parsed = true; } else if (cmdStr.startsWith(F("!decBri"))) { brightnessDown(); parsed = true; } else if (cmdStr.startsWith(F("!presetF"))) { //!presetFallback uint8_t p1 = fdo["PL"] | 1; uint8_t p2 = fdo["FX"] | hw_random8(strip.getModeCount() -1); uint8_t p3 = fdo["FP"] | 0; presetWithFallback(p1, p2, p3); parsed = true; } } else { // HTTP API command String apireq = "win"; apireq += '&'; // reduce flash string usage //if (cmdStr.indexOf("~") || fdo["rpt"]) lastValidCode = code; // repeatable action if (!cmdStr.startsWith(apireq)) cmdStr = apireq + cmdStr; // if no "win&" prefix if (!irApplyToAllSelected && cmdStr.indexOf(F("SS="))<0) { char tmp[10]; sprintf_P(tmp, PSTR("&SS=%d"), strip.getMainSegmentId()); cmdStr += tmp; } fdo.clear(); // clear JSON buffer (it is no longer needed) handleSet(nullptr, cmdStr, false); // no stateUpdated() call here stateUpdated(CALL_MODE_BUTTON); parsed = true; } } else { // command is JSON object (TODO: currently will not handle irApplyToAllSelected correctly) deserializeState(jsonCmdObj, CALL_MODE_BUTTON); parsed = true; } releaseJSONBufferLock(); return parsed; } // Callback function that will be executed when data is received from a linked remote void handleWiZdata(uint8_t *incomingData, size_t len) { message_structure_t *incoming = reinterpret_cast(incomingData); if (len != sizeof(message_structure_t)) { DEBUG_PRINTF_P(PSTR("Unknown incoming ESP Now message received of length %u\n"), len); return; } uint32_t cur_seq = incoming->seq[0] | (incoming->seq[1] << 8) | (incoming->seq[2] << 16) | (incoming->seq[3] << 24); if (cur_seq == last_seq) { return; } DEBUG_PRINT(F("Incoming ESP Now Packet [")); DEBUG_PRINT(cur_seq); DEBUG_PRINT(F("] from sender [")); DEBUG_PRINT(last_signal_src); DEBUG_PRINT(F("] button: ")); DEBUG_PRINTLN(incoming->button); ESPNowButton = incoming->button; // save state, do not process in callback (can cause glitches) last_seq = cur_seq; } // process ESPNow button data (acesses FS, should not be called while update to avoid glitches) void handleRemote() { if(ESPNowButton >= 0) { if (!remoteJson(ESPNowButton)) switch (ESPNowButton) { case WIZMOTE_BUTTON_ON : setOn(); break; case WIZMOTE_BUTTON_OFF : setOff(); break; case WIZMOTE_BUTTON_ONE : presetWithFallback(1, FX_MODE_STATIC, 0); break; case WIZMOTE_BUTTON_TWO : presetWithFallback(2, FX_MODE_BREATH, 0); break; case WIZMOTE_BUTTON_THREE : presetWithFallback(3, FX_MODE_FIRE_FLICKER, 0); break; case WIZMOTE_BUTTON_FOUR : presetWithFallback(4, FX_MODE_RAINBOW, 0); break; case WIZMOTE_BUTTON_FIVE : presetWithFallback(5, FX_MODE_CANDLE, 0); break; case WIZMOTE_BUTTON_SIX : presetWithFallback(6, FX_MODE_RANDOM_COLOR, 0); break; case WIZMOTE_BUTTON_SEVEN : presetWithFallback(7, FX_MODE_FADE, 0); break; case WIZMOTE_BUTTON_NIGHT : activateNightMode(); break; case WIZMOTE_BUTTON_BRIGHT_UP : brightnessUp(); break; case WIZMOTE_BUTTON_BRIGHT_DOWN : brightnessDown(); break; case WIZ_SMART_BUTTON_ON : setOn(); break; case WIZ_SMART_BUTTON_OFF : setOff(); break; case WIZ_SMART_BUTTON_BRIGHT_UP : brightnessUp(); break; case WIZ_SMART_BUTTON_BRIGHT_DOWN : brightnessDown(); break; default: break; } } ESPNowButton = -1; } #else void handleRemote() {} #endif ================================================ FILE: wled00/set.cpp ================================================ #include "wled.h" /* * Receives client input */ //called upon POST settings form submit void handleSettingsSet(AsyncWebServerRequest *request, byte subPage) { if (subPage == SUBPAGE_PINREQ) { checkSettingsPIN(request->arg(F("PIN")).c_str()); return; } //0: menu 1: wifi 2: leds 3: ui 4: sync 5: time 6: sec 7: DMX 8: usermods 9: N/A 10: 2D if (subPage < 1 || subPage > 10 || !correctPIN) return; //WIFI SETTINGS if (subPage == SUBPAGE_WIFI) { unsigned cnt = 0; for (size_t n = 0; n < WLED_MAX_WIFI_COUNT; n++) { char cs[4] = "CS"; cs[2] = 48+n; cs[3] = 0; //client SSID char pw[4] = "PW"; pw[2] = 48+n; pw[3] = 0; //client password char bs[4] = "BS"; bs[2] = 48+n; bs[3] = 0; //BSSID char ip[5] = "IP"; ip[2] = 48+n; ip[4] = 0; //IP address char gw[5] = "GW"; gw[2] = 48+n; gw[4] = 0; //GW address char sn[5] = "SN"; sn[2] = 48+n; sn[4] = 0; //subnet mask #ifdef WLED_ENABLE_WPA_ENTERPRISE char et[4] = "ET"; et[2] = 48+n; et[3] = 0; //WiFi encryption type char ea[4] = "EA"; ea[2] = 48+n; ea[3] = 0; //enterprise anonymous identity char ei[4] = "EI"; ei[2] = 48+n; ei[3] = 0; //enterprise identity #endif if (request->hasArg(cs)) { if (n >= multiWiFi.size()) multiWiFi.emplace_back(); // expand vector by one char oldSSID[33]; strcpy(oldSSID, multiWiFi[n].clientSSID); char oldPass[65]; strcpy(oldPass, multiWiFi[n].clientPass); uint8_t oldBSSID[6]; memcpy(oldBSSID, multiWiFi[n].bssid, 6); // save old BSSID strlcpy(multiWiFi[n].clientSSID, request->arg(cs).c_str(), 33); if (strlen(oldSSID) == 0 || strncmp(multiWiFi[n].clientSSID, oldSSID, 32) != 0) { forceReconnect = true; } if (!isAsterisksOnly(request->arg(pw).c_str(), 65)) { strlcpy(multiWiFi[n].clientPass, request->arg(pw).c_str(), 65); forceReconnect = true; } fillStr2MAC(multiWiFi[n].bssid, request->arg(bs).c_str()); if (memcmp(oldBSSID, multiWiFi[n].bssid, 6) != 0) { // check if BSSID changed forceReconnect = true; } for (size_t i = 0; i < 4; i++) { ip[3] = 48+i; gw[3] = 48+i; sn[3] = 48+i; multiWiFi[n].staticIP[i] = request->arg(ip).toInt(); multiWiFi[n].staticGW[i] = request->arg(gw).toInt(); multiWiFi[n].staticSN[i] = request->arg(sn).toInt(); } #ifdef WLED_ENABLE_WPA_ENTERPRISE byte oldType = multiWiFi[n].encryptionType; char oldAnon[65]; strcpy(oldAnon, multiWiFi[n].enterpriseAnonIdentity); char oldIden[65]; strcpy(oldIden, multiWiFi[n].enterpriseIdentity); if (request->hasArg(et) && request->hasArg(ea) && request->hasArg(ei)) { multiWiFi[n].encryptionType = request->arg(et).toInt(); strlcpy(multiWiFi[n].enterpriseAnonIdentity, request->arg(ea).c_str(), 65); strlcpy(multiWiFi[n].enterpriseIdentity, request->arg(ei).c_str(), 65); } else { // No enterprise settings provided, default to PSK multiWiFi[n].encryptionType = WIFI_ENCRYPTION_TYPE_PSK; } if (multiWiFi[n].encryptionType == WIFI_ENCRYPTION_TYPE_PSK) { // PSK - Clear the anonymous identity and identity fields multiWiFi[n].enterpriseAnonIdentity[0] = '\0'; multiWiFi[n].enterpriseIdentity[0] = '\0'; } forceReconnect |= oldType != multiWiFi[n].encryptionType; if (strncmp(multiWiFi[n].enterpriseAnonIdentity, oldAnon, 64) != 0) { forceReconnect = true; } if (strncmp(multiWiFi[n].enterpriseIdentity, oldIden, 64) != 0) { forceReconnect = true; } #endif cnt++; } } // remove unused if (cnt < multiWiFi.size()) { cnt = multiWiFi.size() - cnt; while (cnt--) multiWiFi.pop_back(); multiWiFi.shrink_to_fit(); // release memory } if (request->hasArg(F("D0"))) { dnsAddress = IPAddress(request->arg(F("D0")).toInt(),request->arg(F("D1")).toInt(),request->arg(F("D2")).toInt(),request->arg(F("D3")).toInt()); } strlcpy(cmDNS, request->arg(F("CM")).c_str(), 33); apBehavior = request->arg(F("AB")).toInt(); char oldSSID[33]; strcpy(oldSSID, apSSID); strlcpy(apSSID, request->arg(F("AS")).c_str(), 33); if (!strcmp(oldSSID, apSSID) && apActive) forceReconnect = true; apHide = request->hasArg(F("AH")); int passlen = request->arg(F("AP")).length(); if (passlen == 0 || (passlen > 7 && !isAsterisksOnly(request->arg(F("AP")).c_str(), 65))) { strlcpy(apPass, request->arg(F("AP")).c_str(), 65); forceReconnect = true; } int t = request->arg(F("AC")).toInt(); if (t != apChannel) forceReconnect = true; if (t > 0 && t < 14) apChannel = t; #ifdef ARDUINO_ARCH_ESP32 int tx = request->arg(F("TX")).toInt(); txPower = min(max(tx, (int)WIFI_POWER_2dBm), (int)WIFI_POWER_19_5dBm); #endif force802_3g = request->hasArg(F("FG")); noWifiSleep = request->hasArg(F("WS")); #ifndef WLED_DISABLE_ESPNOW bool oldESPNow = enableESPNow; enableESPNow = request->hasArg(F("RE")); if (oldESPNow != enableESPNow) forceReconnect = true; linked_remotes.clear(); // clear old remotes for (size_t n = 0; n < 10; n++) { char rm[4]; snprintf(rm, sizeof(rm), "RM%d", n); // "RM0" to "RM9" if (request->hasArg(rm)) { const String& arg = request->arg(rm); if (arg.isEmpty()) continue; std::array mac{}; strlcpy(mac.data(), request->arg(rm).c_str(), 13); strlwr(mac.data()); if (mac[0] != '\0') { linked_remotes.emplace_back(mac); } } } #endif #if defined(ARDUINO_ARCH_ESP32) && defined(WLED_USE_ETHERNET) ethernetType = request->arg(F("ETH")).toInt(); initEthernet(); #endif } //LED SETTINGS if (subPage == SUBPAGE_LEDS) { int t = 0; if (rlyPin>=0 && PinManager::isPinAllocated(rlyPin, PinOwner::Relay)) { PinManager::deallocatePin(rlyPin, PinOwner::Relay); } #ifndef WLED_DISABLE_INFRARED if (irPin>=0 && PinManager::isPinAllocated(irPin, PinOwner::IR)) { deInitIR(); PinManager::deallocatePin(irPin, PinOwner::IR); } #endif for (const auto &button : buttons) { if (button.pin >= 0 && PinManager::isPinAllocated(button.pin, PinOwner::Button)) { PinManager::deallocatePin(button.pin, PinOwner::Button); #ifdef SOC_TOUCH_VERSION_2 // ESP32 S2 and S3 have a function to check touch state, detach interrupt if (digitalPinToTouchChannel(button.pin) >= 0) // if touch capable pin touchDetachInterrupt(button.pin); // if not assigned previously, this will do nothing #endif } } unsigned colorOrder, type, skip, awmode, channelSwap, maPerLed, driverType; unsigned length, start, maMax; uint8_t pins[OUTPUT_MAX_PINS] = {255, 255, 255, 255, 255}; String text; // this will set global ABL max current used when per-port ABL is not used unsigned ablMilliampsMax = request->arg(F("MA")).toInt(); BusManager::setMilliampsMax(ablMilliampsMax); strip.autoSegments = request->hasArg(F("MS")); strip.correctWB = request->hasArg(F("CCT")); strip.cctFromRgb = request->hasArg(F("CR")); cctICused = request->hasArg(F("IC")); uint8_t cctBlending = request->arg(F("CB")).toInt(); Bus::setCCTBlend(cctBlending); Bus::setGlobalAWMode(request->arg(F("AW")).toInt()); strip.setTargetFps(request->arg(F("FR")).toInt()); bool busesChanged = false; for (int s = 0; s < 36; s++) { // theoretical limit is 36 : "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ" int offset = s < 10 ? '0' : 'A' - 10; char lp[4] = "L0"; lp[2] = offset+s; lp[3] = 0; //ascii 0-9 //strip data pin char lc[4] = "LC"; lc[2] = offset+s; lc[3] = 0; //strip length char co[4] = "CO"; co[2] = offset+s; co[3] = 0; //strip color order char lt[4] = "LT"; lt[2] = offset+s; lt[3] = 0; //strip type char ls[4] = "LS"; ls[2] = offset+s; ls[3] = 0; //strip start LED char cv[4] = "CV"; cv[2] = offset+s; cv[3] = 0; //strip reverse char sl[4] = "SL"; sl[2] = offset+s; sl[3] = 0; //skip first N LEDs char rf[4] = "RF"; rf[2] = offset+s; rf[3] = 0; //refresh required char aw[4] = "AW"; aw[2] = offset+s; aw[3] = 0; //auto white mode char wo[4] = "WO"; wo[2] = offset+s; wo[3] = 0; //channel swap char sp[4] = "SP"; sp[2] = offset+s; sp[3] = 0; //bus clock speed (DotStar & PWM) char la[4] = "LA"; la[2] = offset+s; la[3] = 0; //LED mA char ma[4] = "MA"; ma[2] = offset+s; ma[3] = 0; //max mA char ld[4] = "LD"; ld[2] = offset+s; ld[3] = 0; //driver type (RMT=0, I2S=1) char hs[4] = "HS"; hs[2] = offset+s; hs[3] = 0; //hostname (for network types, custom text for others) if (!request->hasArg(lp)) { DEBUG_PRINTF_P(PSTR("# of buses: %d\n"), s+1); break; } for (int i = 0; i < 5; i++) { lp[1] = '0'+i; if (!request->hasArg(lp)) break; pins[i] = (request->arg(lp).length() > 0) ? request->arg(lp).toInt() : 255; } type = request->arg(lt).toInt(); skip = request->arg(sl).toInt(); colorOrder = request->arg(co).toInt(); start = (request->hasArg(ls)) ? request->arg(ls).toInt() : t; if (request->hasArg(lc) && request->arg(lc).toInt() > 0) { t += length = request->arg(lc).toInt(); } else { break; // no parameter } awmode = request->arg(aw).toInt(); uint16_t freq = request->arg(sp).toInt(); if (Bus::isPWM(type)) { switch (freq) { case 0 : freq = WLED_PWM_FREQ/2; break; case 1 : freq = WLED_PWM_FREQ*2/3; break; default: case 2 : freq = WLED_PWM_FREQ; break; case 3 : freq = WLED_PWM_FREQ*2; break; case 4 : freq = WLED_PWM_FREQ*10/3; break; // uint16_t max (19531 * 3.333) } } else if (Bus::is2Pin(type)) { switch (freq) { default: case 0 : freq = 1000; break; case 1 : freq = 2000; break; case 2 : freq = 5000; break; case 3 : freq = 10000; break; case 4 : freq = 20000; break; } } else { freq = 0; } channelSwap = Bus::hasWhite(type) ? request->arg(wo).toInt() : 0; if (Bus::isOnOff(type) || Bus::isPWM(type) || Bus::isVirtual(type)) { // analog and virtual maPerLed = 0; maMax = 0; } else { maPerLed = request->arg(la).toInt(); maMax = request->arg(ma).toInt() * request->hasArg(F("PPL")); // if PP-ABL is disabled maMax (per bus) must be 0 } type |= request->hasArg(rf) << 7; // off refresh override driverType = request->arg(ld).toInt(); // 0=RMT (default), 1=I2S text = request->arg(hs).substring(0,31); // actual finalization is done in WLED::loop() (removing old busses and adding new) // this may happen even before this loop is finished so we do "doInitBusses" after the loop busConfigs.emplace_back(type, pins, start, length, colorOrder | (channelSwap<<4), request->hasArg(cv), skip, awmode, freq, maPerLed, maMax, driverType, text); busesChanged = true; } //doInitBusses = busesChanged; // we will do that below to ensure all input data is processed // we will not bother with pre-allocating ColorOrderMappings vector BusManager::getColorOrderMap().reset(); for (int s = 0; s < WLED_MAX_COLOR_ORDER_MAPPINGS; s++) { int offset = s < 10 ? '0' : 'A' - 10; char xs[4] = "XS"; xs[2] = offset+s; xs[3] = 0; //start LED char xc[4] = "XC"; xc[2] = offset+s; xc[3] = 0; //strip length char xo[4] = "XO"; xo[2] = offset+s; xo[3] = 0; //color order char xw[4] = "XW"; xw[2] = offset+s; xw[3] = 0; //W swap if (request->hasArg(xs)) { start = request->arg(xs).toInt(); length = request->arg(xc).toInt(); colorOrder = request->arg(xo).toInt() & 0x0F; colorOrder |= (request->arg(xw).toInt() & 0x0F) << 4; // add W swap information if (!BusManager::getColorOrderMap().add(start, length, colorOrder)) break; } } // update other pins #ifndef WLED_DISABLE_INFRARED int hw_ir_pin = request->arg(F("IR")).toInt(); if (PinManager::allocatePin(hw_ir_pin,false, PinOwner::IR)) { irPin = hw_ir_pin; } else { irPin = -1; } irEnabled = request->arg(F("IT")).toInt(); initIR(); #endif irApplyToAllSelected = !request->hasArg(F("MSO")); int hw_rly_pin = request->arg(F("RL")).toInt(); if (PinManager::allocatePin(hw_rly_pin,true, PinOwner::Relay)) { rlyPin = hw_rly_pin; } else { rlyPin = -1; } rlyMde = (bool)request->hasArg(F("RM")); rlyOpenDrain = (bool)request->hasArg(F("RO")); disablePullUp = (bool)request->hasArg(F("IP")); touchThreshold = request->arg(F("TT")).toInt(); for (int i = 0; i < WLED_MAX_BUTTONS; i++) { int offset = i < 10 ? '0' : 'A' - 10; char bt[4] = "BT"; bt[2] = offset+i; bt[3] = 0; // button pin (use A,B,C,... if WLED_MAX_BUTTONS>10) char be[4] = "BE"; be[2] = offset+i; be[3] = 0; // button type (use A,B,C,... if WLED_MAX_BUTTONS>10) int hw_btn_pin = request->hasArg(bt) ? request->arg(bt).toInt() : -1; if (i >= buttons.size()) buttons.emplace_back(hw_btn_pin, request->arg(be).toInt()); // add button to vector else { buttons[i].pin = hw_btn_pin; buttons[i].type = request->arg(be).toInt(); } if (buttons[i].pin >= 0 && PinManager::allocatePin(buttons[i].pin, false, PinOwner::Button)) { #ifdef ARDUINO_ARCH_ESP32 // ESP32 only: check that button pin is a valid gpio if ((buttons[i].type == BTN_TYPE_ANALOG) || (buttons[i].type == BTN_TYPE_ANALOG_INVERTED)) { if (digitalPinToAnalogChannel(buttons[i].pin) < 0) { // not an ADC analog pin DEBUG_PRINTF_P(PSTR("PIN ALLOC error: GPIO%d for analog button #%d is not an analog pin!\n"), buttons[i].pin, i); PinManager::deallocatePin(buttons[i].pin, PinOwner::Button); buttons[i].type = BTN_TYPE_NONE; } else { analogReadResolution(12); // see #4040 } } else if ((buttons[i].type == BTN_TYPE_TOUCH || buttons[i].type == BTN_TYPE_TOUCH_SWITCH)) { if (digitalPinToTouchChannel(buttons[i].pin) < 0) { // not a touch pin DEBUG_PRINTF_P(PSTR("PIN ALLOC error: GPIO%d for touch button #%d is not an touch pin!\n"), buttons[i].pin, i); PinManager::deallocatePin(buttons[i].pin, PinOwner::Button); buttons[i].type = BTN_TYPE_NONE; } #ifdef SOC_TOUCH_VERSION_2 // ESP32 S2 and S3 have a fucntion to check touch state but need to attach an interrupt to do so else touchAttachInterrupt(buttons[i].pin, touchButtonISR, touchThreshold << 4); // threshold on Touch V2 is much higher (1500 is a value given by Espressif example, I measured changes of over 5000) #endif } else #endif { // regular buttons and switches if (disablePullUp) { pinMode(buttons[i].pin, INPUT); } else { #ifdef ESP32 pinMode(buttons[i].pin, buttons[i].type==BTN_TYPE_PUSH_ACT_HIGH ? INPUT_PULLDOWN : INPUT_PULLUP); #else pinMode(buttons[i].pin, INPUT_PULLUP); #endif } } } else { buttons[i].pin = -1; buttons[i].type = BTN_TYPE_NONE; } } // we should remove all unused buttons from the vector for (int i = buttons.size()-1; i > 0; i--) { if (buttons[i].pin < 0 && buttons[i].type == BTN_TYPE_NONE) { buttons.erase(buttons.begin() + i); // remove button from vector } } briS = request->arg(F("CA")).toInt(); turnOnAtBoot = request->hasArg(F("BO")); t = request->arg(F("BP")).toInt(); if (t <= 250) bootPreset = t; gammaCorrectBri = request->hasArg(F("GB")); gammaCorrectCol = request->hasArg(F("GC")); gammaCorrectVal = request->arg(F("GV")).toFloat(); if (gammaCorrectVal < 0.1f || gammaCorrectVal > 3) { gammaCorrectVal = 1.0f; // no gamma correction gammaCorrectBri = false; gammaCorrectCol = false; } NeoGammaWLEDMethod::calcGammaTable(gammaCorrectVal); // fill look-up tables t = request->arg(F("TD")).toInt(); if (t >= 0) transitionDelayDefault = t; t = request->arg(F("TP")).toInt(); randomPaletteChangeTime = MIN(255,MAX(1,t)); useHarmonicRandomPalette = request->hasArg(F("TH")); nightlightTargetBri = request->arg(F("TB")).toInt(); t = request->arg(F("TL")).toInt(); if (t > 0) nightlightDelayMinsDefault = t; nightlightDelayMins = nightlightDelayMinsDefault; nightlightMode = request->arg(F("TW")).toInt(); t = request->arg(F("PB")).toInt(); if (t >= 0 && t < 4) paletteBlend = t; t = request->arg(F("BF")).toInt(); if (t > 0) briMultiplier = t; doInitBusses = busesChanged; } //UI if (subPage == SUBPAGE_UI) { strlcpy(serverDescription, request->arg(F("DS")).c_str(), 33); simplifiedUI = request->hasArg(F("SU")); DEBUG_PRINTLN(F("Enumerating ledmaps")); enumerateLedmaps(); DEBUG_PRINTLN(F("Loading custom palettes")); loadCustomPalettes(); // (re)load all custom palettes } //SYNC if (subPage == SUBPAGE_SYNC) { int t = request->arg(F("UP")).toInt(); if (t > 0) udpPort = t; t = request->arg(F("U2")).toInt(); if (t > 0) udpPort2 = t; #ifndef WLED_DISABLE_ESPNOW useESPNowSync = request->hasArg(F("EN")); #endif syncGroups = request->arg(F("GS")).toInt(); receiveGroups = request->arg(F("GR")).toInt(); receiveNotificationBrightness = request->hasArg(F("RB")); receiveNotificationColor = request->hasArg(F("RC")); receiveNotificationEffects = request->hasArg(F("RX")); receiveNotificationPalette = request->hasArg(F("RP")); receiveSegmentOptions = request->hasArg(F("SO")); receiveSegmentBounds = request->hasArg(F("SG")); sendNotifications = request->hasArg(F("SS")); notifyDirect = request->hasArg(F("SD")); notifyButton = request->hasArg(F("SB")); notifyAlexa = request->hasArg(F("SA")); notifyHue = request->hasArg(F("SH")); t = request->arg(F("UR")).toInt(); if ((t>=0) && (t<30)) udpNumRetries = t; nodeListEnabled = request->hasArg(F("NL")); if (!nodeListEnabled) Nodes.clear(); nodeBroadcastEnabled = request->hasArg(F("NB")); receiveDirect = request->hasArg(F("RD")); // UDP realtime useMainSegmentOnly = request->hasArg(F("MO")); realtimeRespectLedMaps = request->hasArg(F("RLM")); e131SkipOutOfSequence = request->hasArg(F("ES")); e131Multicast = request->hasArg(F("EM")); t = request->arg(F("EP")).toInt(); if (t > 0) e131Port = t; t = request->arg(F("EU")).toInt(); if (t >= 0 && t <= 63999) e131Universe = t; t = request->arg(F("DA")).toInt(); if (t >= 0 && t <= 510) DMXAddress = t; t = request->arg(F("XX")).toInt(); if (t >= 0 && t <= 150) DMXSegmentSpacing = t; t = request->arg(F("PY")).toInt(); if (t >= 0 && t <= 200) e131Priority = t; t = request->arg(F("DM")).toInt(); if (t >= DMX_MODE_DISABLED && t <= DMX_MODE_PRESET) DMXMode = t; t = request->arg(F("ET")).toInt(); if (t > 99 && t <= 65000) realtimeTimeoutMs = t; arlsForceMaxBri = request->hasArg(F("FB")); arlsDisableGammaCorrection = request->hasArg(F("RG")); t = request->arg(F("WO")).toInt(); if (t >= -255 && t <= 255) arlsOffset = t; #ifdef WLED_ENABLE_DMX_INPUT dmxInputTransmitPin = request->arg(F("IDMT")).toInt(); dmxInputReceivePin = request->arg(F("IDMR")).toInt(); dmxInputEnablePin = request->arg(F("IDME")).toInt(); dmxInputPort = request->arg(F("IDMP")).toInt(); if(dmxInputPort <= 0 || dmxInputPort > 2) dmxInputPort = 2; #endif #ifndef WLED_DISABLE_ALEXA alexaEnabled = request->hasArg(F("AL")); strlcpy(alexaInvocationName, request->arg(F("AI")).c_str(), 33); t = request->arg(F("AP")).toInt(); if (t >= 0 && t <= 9) alexaNumPresets = t; #endif #ifndef WLED_DISABLE_MQTT mqttEnabled = request->hasArg(F("MQ")); strlcpy(mqttServer, request->arg(F("MS")).c_str(), MQTT_MAX_SERVER_LEN+1); t = request->arg(F("MQPORT")).toInt(); if (t > 0) mqttPort = t; strlcpy(mqttUser, request->arg(F("MQUSER")).c_str(), 41); if (!isAsterisksOnly(request->arg(F("MQPASS")).c_str(), 41)) strlcpy(mqttPass, request->arg(F("MQPASS")).c_str(), 65); strlcpy(mqttClientID, request->arg(F("MQCID")).c_str(), 41); strlcpy(mqttDeviceTopic, request->arg(F("MD")).c_str(), MQTT_MAX_TOPIC_LEN+1); strlcpy(mqttGroupTopic, request->arg(F("MG")).c_str(), MQTT_MAX_TOPIC_LEN+1); buttonPublishMqtt = request->hasArg(F("BM")); retainMqttMsg = request->hasArg(F("RT")); #endif #ifndef WLED_DISABLE_HUESYNC for (int i=0;i<4;i++){ String a = "H"+String(i); hueIP[i] = request->arg(a).toInt(); } t = request->arg(F("HL")).toInt(); if (t > 0) huePollLightId = t; t = request->arg(F("HI")).toInt(); if (t > 50) huePollIntervalMs = t; hueApplyOnOff = request->hasArg(F("HO")); hueApplyBri = request->hasArg(F("HB")); hueApplyColor = request->hasArg(F("HC")); huePollingEnabled = request->hasArg(F("HP")); hueStoreAllowed = true; reconnectHue(); #endif t = request->arg(F("BD")).toInt(); if (t >= 96 && t <= 15000) serialBaud = t; updateBaudRate(serialBaud *100); } //TIME if (subPage == SUBPAGE_TIME) { ntpEnabled = request->hasArg(F("NT")); strlcpy(ntpServerName, request->arg(F("NS")).c_str(), 33); useAMPM = !request->hasArg(F("CF")); currentTimezone = request->arg(F("TZ")).toInt(); utcOffsetSecs = request->arg(F("UO")).toInt(); //start ntp if not already connected if (ntpEnabled && WLED_CONNECTED && !ntpConnected) ntpConnected = ntpUdp.begin(ntpLocalPort); ntpLastSyncTime = NTP_NEVER; // force new NTP query longitude = request->arg(F("LN")).toFloat(); latitude = request->arg(F("LT")).toFloat(); // force a sunrise/sunset re-calculation calculateSunriseAndSunset(); overlayCurrent = request->hasArg(F("OL")) ? 1 : 0; overlayMin = request->arg(F("O1")).toInt(); overlayMax = request->arg(F("O2")).toInt(); analogClock12pixel = request->arg(F("OM")).toInt(); analogClock5MinuteMarks = request->hasArg(F("O5")); analogClockSecondsTrail = request->hasArg(F("OS")); analogClockSolidBlack = request->hasArg(F("OB")); countdownMode = request->hasArg(F("CE")); countdownYear = request->arg(F("CY")).toInt(); countdownMonth = request->arg(F("CI")).toInt(); countdownDay = request->arg(F("CD")).toInt(); countdownHour = request->arg(F("CH")).toInt(); countdownMin = request->arg(F("CM")).toInt(); countdownSec = request->arg(F("CS")).toInt(); setCountdown(); macroAlexaOn = request->arg(F("A0")).toInt(); macroAlexaOff = request->arg(F("A1")).toInt(); macroCountdown = request->arg(F("MC")).toInt(); macroNl = request->arg(F("MN")).toInt(); int ii = 0; for (auto &button : buttons) { char mp[4] = "MP"; mp[2] = (ii<10?'0':'A'-10)+ii; mp[3] = 0; // short char ml[4] = "ML"; ml[2] = (ii<10?'0':'A'-10)+ii; ml[3] = 0; // long char md[4] = "MD"; md[2] = (ii<10?'0':'A'-10)+ii; md[3] = 0; // double //if (!request->hasArg(mp)) break; button.macroButton = request->arg(mp).toInt(); // these will default to 0 if not present button.macroLongPress = request->arg(ml).toInt(); button.macroDoublePress = request->arg(md).toInt(); ii++; } char k[3]; k[2] = 0; for (int i = 0; i<10; i++) { k[1] = i+48;//ascii 0,1,2,3,... k[0] = 'H'; //timer hours timerHours[i] = request->arg(k).toInt(); k[0] = 'N'; //minutes timerMinutes[i] = request->arg(k).toInt(); k[0] = 'T'; //macros timerMacro[i] = request->arg(k).toInt(); k[0] = 'W'; //weekdays timerWeekday[i] = request->arg(k).toInt(); if (i<8) { k[0] = 'M'; //start month timerMonth[i] = request->arg(k).toInt() & 0x0F; timerMonth[i] <<= 4; k[0] = 'P'; //end month timerMonth[i] += (request->arg(k).toInt() & 0x0F); k[0] = 'D'; //start day timerDay[i] = request->arg(k).toInt(); k[0] = 'E'; //end day timerDayEnd[i] = request->arg(k).toInt(); } } } //SECURITY if (subPage == SUBPAGE_SEC) { if (request->hasArg(F("RS"))) //complete factory reset { WLED_FS.format(); serveMessage(request, 200, F("All Settings erased."), F("Connect to WLED-AP to setup again"),255); doReboot = true; // may reboot immediately on dual-core system (race condition) which is desireable in this case } if (request->hasArg(F("PIN"))) { const char *pin = request->arg(F("PIN")).c_str(); unsigned pinLen = strlen(pin); if (pinLen == 4 || pinLen == 0) { unsigned numZeros = 0; for (unsigned i = 0; i < pinLen; i++) numZeros += (pin[i] == '0'); if (numZeros < pinLen || pinLen == 0) { // ignore 0000 input (placeholder) strlcpy(settingsPIN, pin, 5); } settingsPIN[4] = 0; } } bool pwdCorrect = !otaLock; //always allow access if ota not locked if (request->hasArg(F("OP"))) { if (otaLock && strcmp(otaPass,request->arg(F("OP")).c_str()) == 0) { // brute force protection: do not unlock even if correct if last save was less than 3 seconds ago if (millis() - lastEditTime > PIN_RETRY_COOLDOWN) pwdCorrect = true; } if (!otaLock && request->arg(F("OP")).length() > 0) { strlcpy(otaPass,request->arg(F("OP")).c_str(), 33); // set new OTA password } } if (pwdCorrect) //allow changes if correct pwd or no ota active { otaLock = request->hasArg(F("NO")); wifiLock = request->hasArg(F("OW")); #ifndef WLED_DISABLE_OTA aOtaEnabled = request->hasArg(F("AO")); #endif otaSameSubnet = request->hasArg(F("SU")); } } #ifdef WLED_ENABLE_DMX // include only if DMX is enabled if (subPage == SUBPAGE_DMX) { int t = request->arg(F("PU")).toInt(); if (t >= 0 && t <= 63999) e131ProxyUniverse = t; t = request->arg(F("CN")).toInt(); if (t>0 && t<16) { DMXChannels = t; } t = request->arg(F("CS")).toInt(); if (t>0 && t<513) { DMXStart = t; } t = request->arg(F("CG")).toInt(); if (t>0 && t<513) { DMXGap = t; } t = request->arg(F("SL")).toInt(); if (t>=0 && t < MAX_LEDS) { DMXStartLED = t; } for (int i=0; i<15; i++) { String argname = "CH" + String((i+1)); t = request->arg(argname).toInt(); DMXFixtureMap[i] = t; } } #endif //USERMODS if (subPage == SUBPAGE_UM) { if (!requestJSONBufferLock(JSON_LOCK_SETTINGS)) { request->deferResponse(); return; } // global I2C & SPI pins int8_t hw_sda_pin = !request->arg(F("SDA")).length() ? -1 : (int)request->arg(F("SDA")).toInt(); int8_t hw_scl_pin = !request->arg(F("SCL")).length() ? -1 : (int)request->arg(F("SCL")).toInt(); if (i2c_sda != hw_sda_pin || i2c_scl != hw_scl_pin) { // only if pins changed uint8_t old_i2c[2] = { static_cast(i2c_scl), static_cast(i2c_sda) }; PinManager::deallocateMultiplePins(old_i2c, 2, PinOwner::HW_I2C); // just in case deallocation of old pins PinManagerPinType i2c[2] = { { hw_sda_pin, true }, { hw_scl_pin, true } }; if (hw_sda_pin >= 0 && hw_scl_pin >= 0 && PinManager::allocateMultiplePins(i2c, 2, PinOwner::HW_I2C)) { i2c_sda = hw_sda_pin; i2c_scl = hw_scl_pin; // no bus re-initialisation as usermods do not get any notification //Wire.begin(i2c_sda, i2c_scl); } else { // there is no Wire.end() DEBUG_PRINTLN(F("Could not allocate I2C pins.")); i2c_sda = -1; i2c_scl = -1; } } int8_t hw_mosi_pin = !request->arg(F("MOSI")).length() ? -1 : (int)request->arg(F("MOSI")).toInt(); int8_t hw_miso_pin = !request->arg(F("MISO")).length() ? -1 : (int)request->arg(F("MISO")).toInt(); int8_t hw_sclk_pin = !request->arg(F("SCLK")).length() ? -1 : (int)request->arg(F("SCLK")).toInt(); #ifdef ESP8266 // cannot change pins on ESP8266 if (hw_mosi_pin >= 0 && hw_mosi_pin != HW_PIN_DATASPI) hw_mosi_pin = HW_PIN_DATASPI; if (hw_miso_pin >= 0 && hw_miso_pin != HW_PIN_MISOSPI) hw_mosi_pin = HW_PIN_MISOSPI; if (hw_sclk_pin >= 0 && hw_sclk_pin != HW_PIN_CLOCKSPI) hw_sclk_pin = HW_PIN_CLOCKSPI; #endif if (spi_mosi != hw_mosi_pin || spi_miso != hw_miso_pin || spi_sclk != hw_sclk_pin) { // only if pins changed uint8_t old_spi[3] = { static_cast(spi_mosi), static_cast(spi_miso), static_cast(spi_sclk) }; PinManager::deallocateMultiplePins(old_spi, 3, PinOwner::HW_SPI); // just in case deallocation of old pins PinManagerPinType spi[3] = { { hw_mosi_pin, true }, { hw_miso_pin, true }, { hw_sclk_pin, true } }; if (hw_mosi_pin >= 0 && hw_sclk_pin >= 0 && PinManager::allocateMultiplePins(spi, 3, PinOwner::HW_SPI)) { spi_mosi = hw_mosi_pin; spi_miso = hw_miso_pin; spi_sclk = hw_sclk_pin; // no bus re-initialisation as usermods do not get any notification //SPI.end(); #ifdef ESP32 //SPI.begin(spi_sclk, spi_miso, spi_mosi); #else //SPI.begin(); #endif } else { //SPI.end(); DEBUG_PRINTLN(F("Could not allocate SPI pins.")); spi_mosi = -1; spi_miso = -1; spi_sclk = -1; } } JsonObject um = pDoc->createNestedObject("um"); size_t args = request->args(); unsigned j=0; for (size_t i=0; iargName(i); String value = request->arg(i); // POST request parameters are combined as _ int umNameEnd = name.indexOf(":"); if (umNameEnd<1) continue; // parameter does not contain ":" or on 1st place -> wrong JsonObject mod = um[name.substring(0,umNameEnd)]; // get a usermod JSON object if (mod.isNull()) { mod = um.createNestedObject(name.substring(0,umNameEnd)); // if it does not exist create it } DEBUG_PRINT(name.substring(0,umNameEnd)); DEBUG_PRINT(":"); name = name.substring(umNameEnd+1); // remove mod name from string // if the resulting name still contains ":" this means nested object JsonObject subObj; int umSubObj = name.indexOf(":"); DEBUG_PRINTF_P(PSTR("(%d):"),umSubObj); if (umSubObj>0) { subObj = mod[name.substring(0,umSubObj)]; if (subObj.isNull()) subObj = mod.createNestedObject(name.substring(0,umSubObj)); name = name.substring(umSubObj+1); // remove nested object name from string } else { subObj = mod; } DEBUG_PRINT(name); // check if parameters represent array if (name.endsWith("[]")) { name.replace("[]",""); value.replace(",","."); // just in case conversion if (!subObj[name].is()) { JsonArray ar = subObj.createNestedArray(name); if (value.indexOf(".") >= 0) ar.add(value.toFloat()); // we do have a float else ar.add(value.toInt()); // we may have an int j=0; } else { if (value.indexOf(".") >= 0) subObj[name].add(value.toFloat()); // we do have a float else subObj[name].add(value.toInt()); // we may have an int j++; } DEBUG_PRINTF_P(PSTR("[%d] = %s\n"), j, value.c_str()); } else { // we are using a hidden field with the same name as our parameter (!before the actual parameter!) // to describe the type of parameter (text,float,int), for boolean parameters the first field contains "off" // so checkboxes have one or two fields (first is always "false", existence of second depends on checkmark and may be "true") if (subObj[name].isNull()) { // the first occurrence of the field describes the parameter type (used in next loop) if (value == "false") subObj[name] = false; // checkboxes may have only one field else subObj[name] = value; } else { String type = subObj[name].as(); // get previously stored value as a type if (subObj[name].is()) subObj[name] = true; // checkbox/boolean else if (type == "number") { value.replace(",","."); // just in case conversion if (value.indexOf(".") >= 0) subObj[name] = value.toFloat(); // we do have a float else subObj[name] = value.toInt(); // we may have an int } else if (type == "int") subObj[name] = value.toInt(); else subObj[name] = value; // text fields } DEBUG_PRINTF_P(PSTR(" = %s\n"), value.c_str()); } } UsermodManager::readFromConfig(um); // force change of usermod parameters DEBUG_PRINTLN(F("Done re-init UsermodManager::")); releaseJSONBufferLock(); } #ifndef WLED_DISABLE_2D //2D panels if (subPage == SUBPAGE_2D) { strip.isMatrix = request->arg(F("SOMP")).toInt(); strip.panel.clear(); if (strip.isMatrix) { unsigned panels = constrain(request->arg(F("MPC")).toInt(), 1, WLED_MAX_PANELS); strip.panel.reserve(panels); // pre-allocate memory for (unsigned i=0; ihasArg(pO)) break; pO[l] = 'B'; p.bottomStart = request->arg(pO).toInt(); pO[l] = 'R'; p.rightStart = request->arg(pO).toInt(); pO[l] = 'V'; p.vertical = request->arg(pO).toInt(); pO[l] = 'S'; p.serpentine = request->hasArg(pO); pO[l] = 'X'; p.xOffset = request->arg(pO).toInt(); pO[l] = 'Y'; p.yOffset = request->arg(pO).toInt(); pO[l] = 'W'; p.width = request->arg(pO).toInt(); pO[l] = 'H'; p.height = request->arg(pO).toInt(); strip.panel.push_back(p); } } strip.panel.shrink_to_fit(); // release unused memory // we are changing matrix/ledmap geometry which *will* affect existing segments // since we are not in loop() context we must make sure that effects are not running. credit @blazonchek for properly fixing #4911 strip.suspend(); strip.waitForIt(); strip.deserializeMap(); // (re)load default ledmap (will also setUpMatrix() if ledmap does not exist) strip.makeAutoSegments(true); // force re-creation of segments strip.resume(); } #endif lastEditTime = millis(); // do not save if factory reset or LED settings (which are saved after LED re-init) configNeedsWrite = subPage != SUBPAGE_LEDS && !(subPage == SUBPAGE_SEC && doReboot); if (subPage == SUBPAGE_UM) doReboot = request->hasArg(F("RBT")); // prevent race condition on dual core system (set reboot here, after configNeedsWrite has been set) #ifndef WLED_DISABLE_ALEXA if (subPage == SUBPAGE_SYNC) alexaInit(); #endif } //HTTP API request parser bool handleSet(AsyncWebServerRequest *request, const String& req, bool apply) { if (!(req.indexOf("win") >= 0)) return false; int pos = 0; DEBUG_PRINTF_P(PSTR("API req: %s\n"), req.c_str()); //segment select (sets main segment) pos = req.indexOf(F("SM=")); if (pos > 0 && !realtimeMode) { strip.setMainSegmentId(getNumVal(req, pos)); } byte selectedSeg = strip.getFirstSelectedSegId(); bool singleSegment = false; pos = req.indexOf(F("SS=")); if (pos > 0) { unsigned t = getNumVal(req, pos); if (t < strip.getSegmentsNum()) { selectedSeg = t; singleSegment = true; } } Segment& selseg = strip.getSegment(selectedSeg); pos = req.indexOf(F("SV=")); //segment selected if (pos > 0) { unsigned t = getNumVal(req, pos); if (t == 2) for (unsigned i = 0; i < strip.getSegmentsNum(); i++) strip.getSegment(i).selected = false; // unselect other segments selseg.selected = t; } // temporary values, write directly to segments, globals are updated by setValuesFromFirstSelectedSeg() uint32_t col0 = selseg.colors[0]; uint32_t col1 = selseg.colors[1]; uint32_t col2 = selseg.colors[2]; byte colIn[4] = {R(col0), G(col0), B(col0), W(col0)}; byte colInSec[4] = {R(col1), G(col1), B(col1), W(col1)}; byte effectIn = selseg.mode; byte speedIn = selseg.speed; byte intensityIn = selseg.intensity; byte paletteIn = selseg.palette; byte custom1In = selseg.custom1; byte custom2In = selseg.custom2; byte custom3In = selseg.custom3; byte check1In = selseg.check1; byte check2In = selseg.check2; byte check3In = selseg.check3; uint16_t startI = selseg.start; uint16_t stopI = selseg.stop; uint16_t startY = selseg.startY; uint16_t stopY = selseg.stopY; uint8_t grpI = selseg.grouping; uint16_t spcI = selseg.spacing; pos = req.indexOf(F("&S=")); //segment start if (pos > 0) { startI = std::abs(getNumVal(req, pos)); } pos = req.indexOf(F("S2=")); //segment stop if (pos > 0) { stopI = std::abs(getNumVal(req, pos)); } pos = req.indexOf(F("GP=")); //segment grouping if (pos > 0) { grpI = std::max(1,getNumVal(req, pos)); } pos = req.indexOf(F("SP=")); //segment spacing if (pos > 0) { spcI = std::max(0,getNumVal(req, pos)); } strip.suspend(); // must suspend strip operations before changing geometry selseg.setGeometry(startI, stopI, grpI, spcI, UINT16_MAX, startY, stopY, selseg.map1D2D); strip.resume(); pos = req.indexOf(F("RV=")); //Segment reverse if (pos > 0) selseg.reverse = req.charAt(pos+3) != '0'; pos = req.indexOf(F("MI=")); //Segment mirror if (pos > 0) selseg.mirror = req.charAt(pos+3) != '0'; pos = req.indexOf(F("SB=")); //Segment brightness/opacity if (pos > 0) { byte segbri = getNumVal(req, pos); selseg.setOption(SEG_OPTION_ON, segbri); // use transition if (segbri) { selseg.setOpacity(segbri); } } pos = req.indexOf(F("SW=")); //segment power if (pos > 0) { switch (getNumVal(req, pos)) { case 0: selseg.setOption(SEG_OPTION_ON, false); break; // use transition case 1: selseg.setOption(SEG_OPTION_ON, true); break; // use transition default: selseg.setOption(SEG_OPTION_ON, !selseg.on); break; // use transition } } pos = req.indexOf(F("PS=")); //saves current in preset if (pos > 0) savePreset(getNumVal(req, pos)); pos = req.indexOf(F("P1=")); //sets first preset for cycle if (pos > 0) presetCycMin = getNumVal(req, pos); pos = req.indexOf(F("P2=")); //sets last preset for cycle if (pos > 0) presetCycMax = getNumVal(req, pos); //apply preset if (updateVal(req.c_str(), "PL=", presetCycCurr, presetCycMin, presetCycMax)) { applyPreset(presetCycCurr); } pos = req.indexOf(F("NP")); //advances to next preset in a playlist if (pos > 0) doAdvancePlaylist = true; //set brightness updateVal(req.c_str(), "&A=", bri); bool col0Changed = false, col1Changed = false, col2Changed = false; //set colors col0Changed |= updateVal(req.c_str(), "&R=", colIn[0]); col0Changed |= updateVal(req.c_str(), "&G=", colIn[1]); col0Changed |= updateVal(req.c_str(), "&B=", colIn[2]); col0Changed |= updateVal(req.c_str(), "&W=", colIn[3]); col1Changed |= updateVal(req.c_str(), "R2=", colInSec[0]); col1Changed |= updateVal(req.c_str(), "G2=", colInSec[1]); col1Changed |= updateVal(req.c_str(), "B2=", colInSec[2]); col1Changed |= updateVal(req.c_str(), "W2=", colInSec[3]); #ifdef WLED_ENABLE_LOXONE //lox parser pos = req.indexOf(F("LX=")); // Lox primary color if (pos > 0) { int lxValue = getNumVal(req, pos); if (parseLx(lxValue, colIn)) { bri = 255; nightlightActive = false; //always disable nightlight when toggling col0Changed = true; } } pos = req.indexOf(F("LY=")); // Lox secondary color if (pos > 0) { int lxValue = getNumVal(req, pos); if(parseLx(lxValue, colInSec)) { bri = 255; nightlightActive = false; //always disable nightlight when toggling col1Changed = true; } } #endif //set hue pos = req.indexOf(F("HU=")); if (pos > 0) { uint16_t temphue = getNumVal(req, pos); byte tempsat = 255; pos = req.indexOf(F("SA=")); if (pos > 0) { tempsat = getNumVal(req, pos); } byte sec = req.indexOf(F("H2")); colorHStoRGB(temphue, tempsat, (sec>0) ? colInSec : colIn); col0Changed |= (!sec); col1Changed |= sec; } //set white spectrum (kelvin) pos = req.indexOf(F("&K=")); if (pos > 0) { byte sec = req.indexOf(F("K2")); colorKtoRGB(getNumVal(req, pos), (sec>0) ? colInSec : colIn); col0Changed |= (!sec); col1Changed |= sec; } //set color from HEX or 32bit DEC pos = req.indexOf(F("CL=")); if (pos > 0) { colorFromDecOrHexString(colIn, (char*)req.substring(pos + 3).c_str()); col0Changed = true; } pos = req.indexOf(F("C2=")); if (pos > 0) { colorFromDecOrHexString(colInSec, (char*)req.substring(pos + 3).c_str()); col1Changed = true; } pos = req.indexOf(F("C3=")); if (pos > 0) { byte tmpCol[4]; colorFromDecOrHexString(tmpCol, (char*)req.substring(pos + 3).c_str()); col2 = RGBW32(tmpCol[0], tmpCol[1], tmpCol[2], tmpCol[3]); selseg.setColor(2, col2); // defined above (SS= or main) col2Changed = true; } //set to random hue SR=0->1st SR=1->2nd pos = req.indexOf(F("SR")); if (pos > 0) { byte sec = getNumVal(req, pos); setRandomColor(sec? colInSec : colIn); col0Changed |= (!sec); col1Changed |= sec; } // apply colors to selected segment, and all selected segments if applicable if (col0Changed) { col0 = RGBW32(colIn[0], colIn[1], colIn[2], colIn[3]); selseg.setColor(0, col0); } if (col1Changed) { col1 = RGBW32(colInSec[0], colInSec[1], colInSec[2], colInSec[3]); selseg.setColor(1, col1); } //swap 2nd & 1st pos = req.indexOf(F("SC")); if (pos > 0) { std::swap(col0,col1); col0Changed = col1Changed = true; } bool fxModeChanged = false, speedChanged = false, intensityChanged = false, paletteChanged = false; bool custom1Changed = false, custom2Changed = false, custom3Changed = false, check1Changed = false, check2Changed = false, check3Changed = false; // set effect parameters if (updateVal(req.c_str(), "FX=", effectIn, 0, strip.getModeCount()-1)) { if (request != nullptr) unloadPlaylist(); // unload playlist if changing FX using web request fxModeChanged = true; } speedChanged = updateVal(req.c_str(), "SX=", speedIn); intensityChanged = updateVal(req.c_str(), "IX=", intensityIn); paletteChanged = updateVal(req.c_str(), "FP=", paletteIn, 0, getPaletteCount()-1); custom1Changed = updateVal(req.c_str(), "X1=", custom1In); custom2Changed = updateVal(req.c_str(), "X2=", custom2In); custom3Changed = updateVal(req.c_str(), "X3=", custom3In); check1Changed = updateVal(req.c_str(), "M1=", check1In); check2Changed = updateVal(req.c_str(), "M2=", check2In); check3Changed = updateVal(req.c_str(), "M3=", check3In); stateChanged |= (fxModeChanged || speedChanged || intensityChanged || paletteChanged || custom1Changed || custom2Changed || custom3Changed || check1Changed || check2Changed || check3Changed); // apply to main and all selected segments to prevent #1618. for (unsigned i = 0; i < strip.getSegmentsNum(); i++) { Segment& seg = strip.getSegment(i); if (i != selectedSeg && (singleSegment || !seg.isActive() || !seg.isSelected())) continue; // skip non main segments if not applying to all if (fxModeChanged) seg.setMode(effectIn, req.indexOf(F("FXD="))>0); // apply defaults if FXD= is specified if (speedChanged) seg.speed = speedIn; if (intensityChanged) seg.intensity = intensityIn; if (paletteChanged) seg.setPalette(paletteIn); if (col0Changed) seg.setColor(0, col0); if (col1Changed) seg.setColor(1, col1); if (col2Changed) seg.setColor(2, col2); if (custom1Changed) seg.custom1 = custom1In; if (custom2Changed) seg.custom2 = custom2In; if (custom3Changed) seg.custom3 = custom3In; if (check1Changed) seg.check1 = (bool)check1In; if (check2Changed) seg.check2 = (bool)check2In; if (check3Changed) seg.check3 = (bool)check3In; } //set advanced overlay pos = req.indexOf(F("OL=")); if (pos > 0) { overlayCurrent = getNumVal(req, pos); } //apply macro (deprecated, added for compatibility with pre-0.11 automations) pos = req.indexOf(F("&M=")); if (pos > 0) { applyPreset(getNumVal(req, pos) + 16); } //toggle send UDP direct notifications pos = req.indexOf(F("SN=")); if (pos > 0) notifyDirect = (req.charAt(pos+3) != '0'); //toggle receive UDP direct notifications pos = req.indexOf(F("RN=")); if (pos > 0) receiveGroups = (req.charAt(pos+3) != '0') ? receiveGroups | 1 : receiveGroups & 0xFE; //receive live data via UDP/Hyperion pos = req.indexOf(F("RD=")); if (pos > 0) receiveDirect = (req.charAt(pos+3) != '0'); //main toggle on/off (parse before nightlight, #1214) pos = req.indexOf(F("&T=")); if (pos > 0) { nightlightActive = false; //always disable nightlight when toggling switch (getNumVal(req, pos)) { case 0: if (bri != 0){briLast = bri; bri = 0;} break; //off, only if it was previously on case 1: if (bri == 0) bri = briLast; break; //on, only if it was previously off default: toggleOnOff(); //toggle } } //toggle nightlight mode bool aNlDef = false; if (req.indexOf(F("&ND")) > 0) aNlDef = true; pos = req.indexOf(F("NL=")); if (pos > 0) { if (req.charAt(pos+3) == '0') { nightlightActive = false; } else { nightlightActive = true; if (!aNlDef) nightlightDelayMins = getNumVal(req, pos); else nightlightDelayMins = nightlightDelayMinsDefault; nightlightStartTime = millis(); } } else if (aNlDef) { nightlightActive = true; nightlightDelayMins = nightlightDelayMinsDefault; nightlightStartTime = millis(); } //set nightlight target brightness pos = req.indexOf(F("NT=")); if (pos > 0) { nightlightTargetBri = getNumVal(req, pos); nightlightActiveOld = false; //re-init } //toggle nightlight fade pos = req.indexOf(F("NF=")); if (pos > 0) { nightlightMode = getNumVal(req, pos); nightlightActiveOld = false; //re-init } if (nightlightMode > NL_MODE_SUN) nightlightMode = NL_MODE_SUN; pos = req.indexOf(F("TT=")); if (pos > 0) transitionDelay = getNumVal(req, pos); strip.setTransition(transitionDelay); //set time (unix timestamp) pos = req.indexOf(F("ST=")); if (pos > 0) { setTimeFromAPI(getNumVal(req, pos)); } //set countdown goal (unix timestamp) pos = req.indexOf(F("CT=")); if (pos > 0) { countdownTime = getNumVal(req, pos); if (countdownTime - toki.second() > 0) countdownOverTriggered = false; } pos = req.indexOf(F("LO=")); if (pos > 0) { realtimeOverride = getNumVal(req, pos); if (realtimeOverride > 2) realtimeOverride = REALTIME_OVERRIDE_ALWAYS; if (realtimeMode && useMainSegmentOnly) { strip.getMainSegment().freeze = !realtimeOverride; realtimeOverride = REALTIME_OVERRIDE_NONE; // ignore request for override if using main segment only } } pos = req.indexOf(F("RB")); if (pos > 0) doReboot = true; // clock mode, 0: normal, 1: countdown pos = req.indexOf(F("NM=")); if (pos > 0) countdownMode = (req.charAt(pos+3) != '0'); pos = req.indexOf(F("U0=")); //user var 0 if (pos > 0) { userVar0 = getNumVal(req, pos); } pos = req.indexOf(F("U1=")); //user var 1 if (pos > 0) { userVar1 = getNumVal(req, pos); } // you can add more if you need // global colPri[], effectCurrent, ... are updated in stateChanged() if (!apply) return true; // when called by JSON API, do not call colorUpdated() here pos = req.indexOf(F("&NN")); //do not send UDP notifications this time stateUpdated((pos > 0) ? CALL_MODE_NO_NOTIFY : CALL_MODE_DIRECT_CHANGE); // internal call, does not send XML response pos = req.indexOf(F("IN")); if ((request != nullptr) && (pos < 1)) { auto response = request->beginResponseStream("text/xml"); XML_response(*response); request->send(response); } return true; } ================================================ FILE: wled00/src/dependencies/dmx/ESPDMX.cpp ================================================ // - - - - - // ESPDMX - A Arduino library for sending and receiving DMX using the builtin serial hardware port. // ESPDMX.cpp: Library implementation file // // Copyright (C) 2015 Rick // This work is licensed under a GNU style license. // // Last change: Marcel Seerig // // Documentation and samples are available at https://github.com/Rickgg/ESP-Dmx // - - - - - /* ----- LIBRARIES ----- */ #if defined(ESP8266) || defined(CONFIG_IDF_TARGET_ESP32C3) || defined(CONFIG_IDF_TARGET_ESP32S2) #include #include "ESPDMX.h" #define dmxMaxChannel 512 #define defaultMax 32 #define DMXSPEED 250000 #define DMXFORMAT SERIAL_8N2 #define BREAKSPEED 83333 #define BREAKFORMAT SERIAL_8N1 bool dmxStarted = false; int sendPin = 2; //default on ESP8266 //DMX value array and size. Entry 0 will hold startbyte, so we need 512+1 elements uint8_t dmxDataStore[dmxMaxChannel+1] = {}; int channelSize; void DMXESPSerial::init() { channelSize = defaultMax; Serial1.begin(DMXSPEED); pinMode(sendPin, OUTPUT); dmxStarted = true; } // Set up the DMX-Protocol void DMXESPSerial::init(int chanQuant) { if (chanQuant > dmxMaxChannel || chanQuant <= 0) { chanQuant = defaultMax; } channelSize = chanQuant; Serial1.begin(DMXSPEED); pinMode(sendPin, OUTPUT); dmxStarted = true; } // Function to read DMX data uint8_t DMXESPSerial::read(int Channel) { if (dmxStarted == false) init(); if (Channel < 1) Channel = 1; if (Channel > dmxMaxChannel) Channel = dmxMaxChannel; return(dmxDataStore[Channel]); } // Function to send DMX data void DMXESPSerial::write(int Channel, uint8_t value) { if (dmxStarted == false) init(); if (Channel < 1) Channel = 1; if (Channel > channelSize) Channel = channelSize; if (value < 0) value = 0; if (value > 255) value = 255; dmxDataStore[Channel] = value; } void DMXESPSerial::end() { channelSize = 0; Serial1.end(); dmxStarted = false; } void DMXESPSerial::update() { if (dmxStarted == false) init(); //Send break digitalWrite(sendPin, HIGH); Serial1.begin(BREAKSPEED, BREAKFORMAT); Serial1.write(0); Serial1.flush(); delay(1); Serial1.end(); //send data Serial1.begin(DMXSPEED, DMXFORMAT); digitalWrite(sendPin, LOW); Serial1.write(dmxDataStore, channelSize); Serial1.flush(); delay(1); Serial1.end(); } // Function to update the DMX bus #endif ================================================ FILE: wled00/src/dependencies/dmx/ESPDMX.h ================================================ // - - - - - // ESPDMX - A Arduino library for sending and receiving DMX using the builtin serial hardware port. // ESPDMX.cpp: Library implementation file // // Copyright (C) 2015 Rick // This work is licensed under a GNU style license. // // Last change: Marcel Seerig // // Documentation and samples are available at https://github.com/Rickgg/ESP-Dmx // - - - - - #include #ifndef ESPDMX_h #define ESPDMX_h // ---- Methods ---- class DMXESPSerial { public: void init(); void init(int MaxChan); uint8_t read(int Channel); void write(int channel, uint8_t value); void update(); void end(); }; #endif ================================================ FILE: wled00/src/dependencies/dmx/LICENSE.md ================================================ SparkFun License Information ============================ SparkFun uses two different licenses for our files — one for hardware and one for code. Hardware --------- **SparkFun hardware is released under [Creative Commons Share-alike 4.0 International](http://creativecommons.org/licenses/by-sa/4.0/).** Note: This is a human-readable summary of (and not a substitute for) the [license](http://creativecommons.org/licenses/by-sa/4.0/legalcode). You are free to: Share — copy and redistribute the material in any medium or format Adapt — remix, transform, and build upon the material for any purpose, even commercially. The licensor cannot revoke these freedoms as long as you follow the license terms. Under the following terms: Attribution — You must give appropriate credit, provide a link to the license, and indicate if changes were made. You may do so in any reasonable manner, but not in any way that suggests the licensor endorses you or your use. ShareAlike — If you remix, transform, or build upon the material, you must distribute your contributions under the same license as the original. No additional restrictions — You may not apply legal terms or technological measures that legally restrict others from doing anything the license permits. Notices: You do not have to comply with the license for elements of the material in the public domain or where your use is permitted by an applicable exception or limitation. No warranties are given. The license may not give you all of the permissions necessary for your intended use. For example, other rights such as publicity, privacy, or moral rights may limit how you use the material. Code -------- **SparkFun code, firmware, and software is released under the MIT License(http://opensource.org/licenses/MIT).** The MIT License (MIT) Copyright (c) 2016 SparkFun Electronics Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: wled00/src/dependencies/dmx/SparkFunDMX.cpp ================================================ /****************************************************************************** SparkFunDMX.h Arduino Library for the SparkFun ESP32 LED to DMX Shield Andy England @ SparkFun Electronics 7/22/2019 Development environment specifics: Arduino IDE 1.6.4 This code is released under the [MIT License](http://opensource.org/licenses/MIT). Please review the LICENSE.md file included with this example. If you have any questions or concerns with licensing, please contact techsupport@sparkfun.com. Distributed as-is; no warranty is given. ******************************************************************************/ /* ----- LIBRARIES ----- */ #if defined(ARDUINO_ARCH_ESP32) #include #if !defined(CONFIG_IDF_TARGET_ESP32C3) && !defined(CONFIG_IDF_TARGET_ESP32S2) #include "SparkFunDMX.h" #include #define dmxMaxChannel 512 #define defaultMax 32 #define DMXSPEED 250000 #define DMXFORMAT SERIAL_8N2 #define BREAKSPEED 83333 #define BREAKFORMAT SERIAL_8N1 static const int enablePin = -1; // disable the enable pin because it is not needed static const int rxPin = -1; // disable the receiving pin because it is not needed - softhack007: Pin=-1 means "use default" not "disable" static const int txPin = 2; // transmit DMX data over this pin (default is pin 2) //DMX value array and size. Entry 0 will hold startbyte, so we need 512+1 elements static uint8_t dmxData[dmxMaxChannel+1] = { 0 }; static int chanSize = 0; #if !defined(DMX_SEND_ONLY) static int currentChannel = 0; #endif // Some new MCUs (-S2, -C3) don't have HardwareSerial(2) #if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(4, 2, 0) #if SOC_UART_NUM < 3 #error DMX output is not possible on your MCU, as it does not have HardwareSerial(2) #endif #endif static HardwareSerial DMXSerial(2); /* Interrupt Timer for DMX Receive */ #if !defined(DMX_SEND_ONLY) static hw_timer_t * timer = NULL; static portMUX_TYPE timerMux = portMUX_INITIALIZER_UNLOCKED; #endif static volatile int _interruptCounter = 0; static volatile bool _startCodeDetected = false; #if !defined(DMX_SEND_ONLY) /* Start Code is detected by 21 low interrupts */ void IRAM_ATTR onTimer() { if ((rxPin >= 0) && (digitalRead(rxPin) == 1)) { _interruptCounter = 0; //If the RX Pin is high, we are not in an interrupt } else { _interruptCounter++; } if (_interruptCounter > 9) { portENTER_CRITICAL_ISR(&timerMux); _startCodeDetected = true; DMXSerial.begin(DMXSPEED, DMXFORMAT, rxPin, txPin); portEXIT_CRITICAL_ISR(&timerMux); _interruptCounter = 0; } } void SparkFunDMX::initRead(int chanQuant) { timer = timerBegin(0, 1, true); timerAttachInterrupt(timer, &onTimer, true); timerAlarmWrite(timer, 320, true); timerAlarmEnable(timer); _READWRITE = _READ; if (chanQuant > dmxMaxChannel || chanQuant <= 0) { chanQuant = defaultMax; } chanSize = chanQuant; if (enablePin >= 0) { pinMode(enablePin, OUTPUT); digitalWrite(enablePin, LOW); } if (rxPin >= 0) pinMode(rxPin, INPUT); } #endif // Set up the DMX-Protocol void SparkFunDMX::initWrite (int chanQuant) { _READWRITE = _WRITE; if (chanQuant > dmxMaxChannel || chanQuant <= 0) { chanQuant = defaultMax; } chanSize = chanQuant + 1; //Add 1 for start code DMXSerial.begin(DMXSPEED, DMXFORMAT, rxPin, txPin); if (enablePin >= 0) { pinMode(enablePin, OUTPUT); digitalWrite(enablePin, HIGH); } } #if !defined(DMX_SEND_ONLY) // Function to read DMX data uint8_t SparkFunDMX::read(int Channel) { if (Channel > chanSize) Channel = chanSize; return(dmxData[Channel - 1]); //subtract one to account for start byte } #endif // Function to send DMX data void SparkFunDMX::write(int Channel, uint8_t value) { if (Channel < 0) Channel = 0; if (Channel > chanSize) chanSize = Channel; dmxData[0] = 0; dmxData[Channel] = value; //add one to account for start byte } void SparkFunDMX::update() { if (_READWRITE == _WRITE) { //Send DMX break digitalWrite(txPin, HIGH); DMXSerial.begin(BREAKSPEED, BREAKFORMAT, rxPin, txPin);//Begin the Serial port DMXSerial.write(0); DMXSerial.flush(); delay(1); DMXSerial.end(); //Send DMX data DMXSerial.begin(DMXSPEED, DMXFORMAT, rxPin, txPin);//Begin the Serial port DMXSerial.write(dmxData, chanSize); DMXSerial.flush(); DMXSerial.end();//clear our DMX array, end the Hardware Serial port } #if !defined(DMX_SEND_ONLY) else if (_READWRITE == _READ)//In a perfect world, this function ends serial communication upon packet completion and attaches RX to a CHANGE interrupt so the start code can be read again { if (_startCodeDetected == true) { while (DMXSerial.available()) { dmxData[currentChannel++] = DMXSerial.read(); } if (currentChannel > chanSize) //Set the channel counter back to 0 if we reach the known end size of our packet { portENTER_CRITICAL(&timerMux); _startCodeDetected = false; DMXSerial.flush(); DMXSerial.end(); portEXIT_CRITICAL(&timerMux); currentChannel = 0; } } } #endif } // Function to update the DMX bus #endif #endif ================================================ FILE: wled00/src/dependencies/dmx/SparkFunDMX.h ================================================ /****************************************************************************** SparkFunDMX.h Arduino Library for the SparkFun ESP32 LED to DMX Shield Andy England @ SparkFun Electronics 7/22/2019 Development environment specifics: Arduino IDE 1.6.4 This code is released under the [MIT License](http://opensource.org/licenses/MIT). Please review the LICENSE.md file included with this example. If you have any questions or concerns with licensing, please contact techsupport@sparkfun.com. Distributed as-is; no warranty is given. ******************************************************************************/ #include #ifndef SparkFunDMX_h #define SparkFunDMX_h #define DMX_SEND_ONLY // this disables DMX sending features, and saves us two GPIO pins // ---- Methods ---- class SparkFunDMX { public: void initWrite(int maxChan); #if !defined(DMX_SEND_ONLY) void initRead(int maxChan); uint8_t read(int Channel); #endif void write(int channel, uint8_t value); void update(); private: const uint8_t _startCodeValue = 0xFF; const bool _READ = true; const bool _WRITE = false; bool _READWRITE; }; #endif ================================================ FILE: wled00/src/dependencies/e131/ESPAsyncE131.cpp ================================================ /* * ESPAsyncE131.cpp * * Project: ESPAsyncE131 - Asynchronous E.131 (sACN) library for Arduino ESP8266 and ESP32 * Copyright (c) 2019 Shelby Merrick * http://www.forkineye.com * * This program is provided free for you to use in any way that you wish, * subject to the laws and regulations where you are using it. Due diligence * is strongly suggested before using this code. Please give credit where due. * * The Author makes no warranty of any kind, express or implied, with regard * to this program or the documentation contained in this document. The * Author shall not be liable in any event for incidental or consequential * damages in connection with, or arising out of, the furnishing, performance * or use of these programs. * */ #include "ESPAsyncE131.h" #include "../network/Network.h" #include // E1.17 ACN Packet Identifier const byte ESPAsyncE131::ACN_ID[12] = { 0x41, 0x53, 0x43, 0x2d, 0x45, 0x31, 0x2e, 0x31, 0x37, 0x00, 0x00, 0x00 }; // Art-Net Packet Identifier const byte ESPAsyncE131::ART_ID[8] = { 0x41, 0x72, 0x74, 0x2d, 0x4e, 0x65, 0x74, 0x00 }; // Constructor ESPAsyncE131::ESPAsyncE131(e131_packet_callback_function callback) { _callback = callback; } ///////////////////////////////////////////////////////// // // Public begin() members // ///////////////////////////////////////////////////////// bool ESPAsyncE131::begin(bool multicast, uint16_t port, uint16_t universe, uint8_t n) { bool success = false; if (multicast) { success = initMulticast(port, universe, n); } else { success = initUnicast(port); } return success; } ///////////////////////////////////////////////////////// // // Private init() members // ///////////////////////////////////////////////////////// bool ESPAsyncE131::initUnicast(uint16_t port) { bool success = false; if (udp.listen(port)) { udp.onPacket(std::bind(&ESPAsyncE131::parsePacket, this, std::placeholders::_1)); success = true; } return success; } bool ESPAsyncE131::initMulticast(uint16_t port, uint16_t universe, uint8_t n) { bool success = false; IPAddress address = IPAddress(239, 255, ((universe >> 8) & 0xff), ((universe >> 0) & 0xff)); if (udp.listenMulticast(address, port)) { ip4_addr_t ifaddr; ip4_addr_t multicast_addr; ifaddr.addr = static_cast(Network.localIP()); for (uint8_t i = 1; i < n; i++) { multicast_addr.addr = static_cast(IPAddress(239, 255, (((universe + i) >> 8) & 0xff), (((universe + i) >> 0) & 0xff))); igmp_joingroup(&ifaddr, &multicast_addr); } udp.onPacket(std::bind(&ESPAsyncE131::parsePacket, this, std::placeholders::_1)); success = true; } return success; } ///////////////////////////////////////////////////////// // // Packet parsing - Private // ///////////////////////////////////////////////////////// void ESPAsyncE131::parsePacket(AsyncUDPPacket _packet) { bool error = false; uint8_t protocol = P_E131; e131_packet_t *sbuff = reinterpret_cast(_packet.data()); //E1.31 packet identifier ("ACS-E1.17") if (memcmp(sbuff->acn_id, ESPAsyncE131::ACN_ID, sizeof(sbuff->acn_id))) protocol = P_ARTNET; if (protocol == P_ARTNET) { if (memcmp(sbuff->art_id, ESPAsyncE131::ART_ID, sizeof(sbuff->art_id))) error = true; //not "Art-Net" if (sbuff->art_opcode != ARTNET_OPCODE_OPDMX && sbuff->art_opcode != ARTNET_OPCODE_OPPOLL) error = true; //not a DMX or poll packet } else { //E1.31 error handling if (htonl(sbuff->root_vector) != ESPAsyncE131::VECTOR_ROOT) error = true; if (htonl(sbuff->frame_vector) != ESPAsyncE131::VECTOR_FRAME) error = true; if (sbuff->dmp_vector != ESPAsyncE131::VECTOR_DMP) error = true; if (sbuff->property_values[0] != 0) error = true; } if (error && _packet.localPort() == DDP_DEFAULT_PORT) { //DDP packet error = false; protocol = P_DDP; } if (!error) { _callback(sbuff, _packet.remoteIP(), protocol); } } ================================================ FILE: wled00/src/dependencies/e131/ESPAsyncE131.h ================================================ /* * ESPAsyncE131.h * * Project: ESPAsyncE131 - Asynchronous E.131 (sACN) library for Arduino ESP8266 and ESP32 * Copyright (c) 2019 Shelby Merrick * http://www.forkineye.com * * Project: ESPAsyncDDP - Asynchronous DDP library for Arduino ESP8266 and ESP32 * Copyright (c) 2019 Daniel Kulp * * This program is provided free for you to use in any way that you wish, * subject to the laws and regulations where you are using it. Due diligence * is strongly suggested before using this code. Please give credit where due. * * The Author makes no warranty of any kind, express or implied, with regard * to this program or the documentation contained in this document. The * Author shall not be liable in any event for incidental or consequential * damages in connection with, or arising out of, the furnishing, performance * or use of these programs. */ /* * Inspired by https://github.com/hideakitai/ArtNet for ArtNet support */ #ifndef ESPASYNCE131_H_ #define ESPASYNCE131_H_ #ifdef ESP32 #include #include #elif defined (ESP8266) #include #include #include #else #error Platform not supported #endif #include #include #include #if LWIP_VERSION_MAJOR == 1 typedef struct ip_addr ip4_addr_t; #endif // Defaults #define E131_DEFAULT_PORT 5568 #define ARTNET_DEFAULT_PORT 6454 #define DDP_DEFAULT_PORT 4048 #define DDP_PUSH_FLAG 0x01 #define DDP_TIMECODE_FLAG 0x10 #define DDP_TYPE_RGB24 0x0B // 00 001 011 (RGB , 8 bits per channel, 3 channels) #define DDP_TYPE_RGBW32 0x1B // 00 011 011 (RGBW, 8 bits per channel, 4 channels) #define ARTNET_OPCODE_OPDMX 0x5000 #define ARTNET_OPCODE_OPPOLL 0x2000 #define ARTNET_OPCODE_OPPOLLREPLY 0x2100 #define P_E131 0 #define P_ARTNET 1 #define P_DDP 2 // E1.31 Packet Offsets #define E131_ROOT_PREAMBLE_SIZE 0 #define E131_ROOT_POSTAMBLE_SIZE 2 #define E131_ROOT_ID 4 #define E131_ROOT_FLENGTH 16 #define E131_ROOT_VECTOR 18 #define E131_ROOT_CID 22 #define E131_FRAME_FLENGTH 38 #define E131_FRAME_VECTOR 40 #define E131_FRAME_SOURCE 44 #define E131_FRAME_PRIORITY 108 #define E131_FRAME_RESERVED 109 #define E131_FRAME_SEQ 111 #define E131_FRAME_OPT 112 #define E131_FRAME_UNIVERSE 113 #define E131_DMP_FLENGTH 115 #define E131_DMP_VECTOR 117 #define E131_DMP_TYPE 118 #define E131_DMP_ADDR_FIRST 119 #define E131_DMP_ADDR_INC 121 #define E131_DMP_COUNT 123 #define E131_DMP_DATA 125 // E1.31 Packet Structure typedef union { struct { //E1.31 packet // Root Layer uint16_t preamble_size; uint16_t postamble_size; uint8_t acn_id[12]; uint16_t root_flength; uint32_t root_vector; uint8_t cid[16]; // Frame Layer uint16_t frame_flength; uint32_t frame_vector; uint8_t source_name[64]; uint8_t priority; uint16_t reserved; uint8_t sequence_number; uint8_t options; uint16_t universe; // DMP Layer uint16_t dmp_flength; uint8_t dmp_vector; uint8_t type; uint16_t first_address; uint16_t address_increment; uint16_t property_value_count; uint8_t property_values[513]; } __attribute__((packed)); struct { //Art-Net packet uint8_t art_id[8]; uint16_t art_opcode; uint16_t art_protocol_ver; uint8_t art_sequence_number; uint8_t art_physical; uint16_t art_universe; uint16_t art_length; uint8_t art_data[512]; } __attribute__((packed)); struct { //DDP Header uint8_t flags; uint8_t sequenceNum; uint8_t dataType; uint8_t destination; uint32_t channelOffset; uint16_t dataLen; uint8_t data[1]; } __attribute__((packed)); /*struct { //DDP Time code Header (unsupported) uint8_t flags; uint8_t sequenceNum; uint8_t dataType; uint8_t destination; uint32_t channelOffset; uint16_t dataLen; uint32_t timeCode; uint8_t data[1]; } __attribute__((packed));*/ uint8_t raw[1458]; } e131_packet_t; typedef union { struct { uint8_t reply_id[8]; uint16_t reply_opcode; uint8_t reply_ip[4]; uint16_t reply_port; uint8_t reply_version_h; uint8_t reply_version_l; uint8_t reply_net_sw; uint8_t reply_sub_sw; uint8_t reply_oem_h; uint8_t reply_oem_l; uint8_t reply_ubea_ver; uint8_t reply_status_1; uint16_t reply_esta_man; uint8_t reply_short_name[18]; uint8_t reply_long_name[64]; uint8_t reply_node_report[64]; uint8_t reply_num_ports_h; uint8_t reply_num_ports_l; uint8_t reply_port_types[4]; uint8_t reply_good_input[4]; uint8_t reply_good_output_a[4]; uint8_t reply_sw_in[4]; uint8_t reply_sw_out[4]; uint8_t reply_sw_video; uint8_t reply_sw_macro; uint8_t reply_sw_remote; uint8_t reply_spare[3]; uint8_t reply_style; uint8_t reply_mac[6]; uint8_t reply_bind_ip[4]; uint8_t reply_bind_index; uint8_t reply_status_2; uint8_t reply_good_output_b[4]; uint8_t reply_status_3; uint8_t reply_filler[21]; } __attribute__((packed)); uint8_t raw[239]; } ArtPollReply; // new packet callback typedef void (*e131_packet_callback_function) (e131_packet_t* p, IPAddress clientIP, byte protocol); class ESPAsyncE131 { private: // Constants for packet validation static const uint8_t ACN_ID[]; static const uint8_t ART_ID[]; static const uint32_t VECTOR_ROOT = 4; static const uint32_t VECTOR_FRAME = 2; static const uint8_t VECTOR_DMP = 2; AsyncUDP udp; // AsyncUDP // Internal Initializers bool initUnicast(uint16_t port); bool initMulticast(uint16_t port, uint16_t universe, uint8_t n = 1); // Packet parser callback void parsePacket(AsyncUDPPacket _packet); e131_packet_callback_function _callback = nullptr; public: ESPAsyncE131(e131_packet_callback_function callback); // Generic UDP listener, no physical or IP configuration bool begin(bool multicast, uint16_t port = E131_DEFAULT_PORT, uint16_t universe = 1, uint8_t n = 1); }; // Class to track e131 package priority class E131Priority { private: uint8_t priority; time_t setupTime; uint8_t seconds; public: E131Priority(uint8_t timeout=3) { seconds = timeout; set(0); }; // Set priority (+ remember time) void set(uint8_t prio) { setupTime = time(0); priority = prio; } // Get priority (+ reset & return 0 if older timeout) uint8_t get() { if (time(0) > setupTime + seconds) priority = 0; return priority; } }; #endif // ESPASYNCE131_H_ ================================================ FILE: wled00/src/dependencies/espalexa/Espalexa.h ================================================ #ifndef Espalexa_h #define Espalexa_h /* * Alexa Voice On/Off/Brightness/Color Control. Emulates a Philips Hue bridge to Alexa. * * This was put together from these two excellent projects: * https://github.com/kakopappa/arduino-esp8266-alexa-wemo-switch * https://github.com/probonopd/ESP8266HueEmulator */ /* * @title Espalexa library * @version 2.7.1 * @author Christian Schwinne * @license MIT * @contributors d-999 */ #include "Arduino.h" //you can use these defines for library config in your sketch. Just use them before #include //#define ESPALEXA_ASYNC //in case this is unwanted in your application (will disable the /espalexa value page) //#define ESPALEXA_NO_SUBPAGE #ifndef ESPALEXA_MAXDEVICES #define ESPALEXA_MAXDEVICES 10 //this limit only has memory reasons, set it higher should you need to, max 128 #endif //#define ESPALEXA_DEBUG #ifdef ESPALEXA_ASYNC #ifdef ARDUINO_ARCH_ESP32 #include #else #include #endif #include #else #ifdef ARDUINO_ARCH_ESP32 #include #include //if you get an error here please update to ESP32 arduino core 1.0.0 #else #include #include #endif #endif #include #include "../network/Network.h" #ifdef ESPALEXA_DEBUG #pragma message "Espalexa 2.7.1 debug mode" #define EA_DEBUG(x) Serial.print (x) #define EA_DEBUGLN(x) Serial.println (x) #else #define EA_DEBUG(x) #define EA_DEBUGLN(x) #endif #include "EspalexaDevice.h" #define DEVICE_UNIQUE_ID_LENGTH 12 class Espalexa { private: //private member vars #ifdef ESPALEXA_ASYNC AsyncWebServer* serverAsync; AsyncWebServerRequest* server; //this saves many #defines String body = ""; #elif defined ARDUINO_ARCH_ESP32 WebServer* server; #else ESP8266WebServer* server; #endif uint8_t currentDeviceCount = 0; bool discoverable = true; bool udpConnected = false; EspalexaDevice* devices[ESPALEXA_MAXDEVICES] = {}; //Keep in mind that Device IDs go from 1 to DEVICES, cpp arrays from 0 to DEVICES-1!! WiFiUDP espalexaUdp; IPAddress ipMulti; uint32_t mac24; //bottom 24 bits of mac String escapedMac=""; //lowercase mac address //private member functions const char* modeString(EspalexaColorMode m) { if (m == EspalexaColorMode::xy) return "xy"; if (m == EspalexaColorMode::hs) return "hs"; return "ct"; } const char* typeString(EspalexaDeviceType t) { switch (t) { case EspalexaDeviceType::dimmable: return PSTR("Dimmable light"); case EspalexaDeviceType::whitespectrum: return PSTR("Color temperature light"); case EspalexaDeviceType::color: return PSTR("Color light"); case EspalexaDeviceType::extendedcolor: return PSTR("Extended color light"); default: return ""; } } const char* modelidString(EspalexaDeviceType t) { switch (t) { case EspalexaDeviceType::dimmable: return "LWB010"; case EspalexaDeviceType::whitespectrum: return "LWT010"; case EspalexaDeviceType::color: return "LST001"; case EspalexaDeviceType::extendedcolor: return "LCT015"; default: return ""; } } void encodeLightId(uint8_t idx, char* out) { String mymac = WiFi.macAddress(); sprintf_P(out, PSTR("%02X:%s:AB-%02X"), idx, mymac.c_str(), idx); } // construct 'globally unique' Json dict key fitting into signed int inline int encodeLightKey(uint8_t idx) { //return idx +1; static_assert(ESPALEXA_MAXDEVICES <= 128, ""); return (mac24<<7) | idx; } // get device index from Json key uint8_t decodeLightKey(int key) { //return key -1; return (((uint32_t)key>>7) == mac24) ? (key & 127U) : 255U; } //device JSON string: color+temperature device emulates LCT015, dimmable device LWB010, (TODO: on/off Plug 01, color temperature device LWT010, color device LST001) void deviceJsonString(EspalexaDevice* dev, char* buf, size_t maxBuf) // softhack007 "size" parameter added, to avoid buffer overrun { char buf_lightid[27]; encodeLightId(dev->getId() + 1, buf_lightid); char buf_col[80] = ""; //color support if (static_cast(dev->getType()) > 2) //TODO: %f is not working for some reason on ESP8266 in v0.11.0 (was fine in 0.10.2). Need to investigate //sprintf_P(buf_col,PSTR(",\"hue\":%u,\"sat\":%u,\"effect\":\"none\",\"xy\":[%f,%f]") // ,dev->getHue(), dev->getSat(), dev->getX(), dev->getY()); snprintf_P(buf_col, sizeof(buf_col), PSTR(",\"hue\":%u,\"sat\":%u,\"effect\":\"none\",\"xy\":[%s,%s]"),dev->getHue(), dev->getSat(), ((String)dev->getX()).c_str(), ((String)dev->getY()).c_str()); char buf_ct[16] = ""; //white spectrum support if (static_cast(dev->getType()) > 1 && dev->getType() != EspalexaDeviceType::color) snprintf(buf_ct, sizeof(buf_ct), ",\"ct\":%u", dev->getCt()); char buf_cm[20] = ""; if (static_cast(dev->getType()) > 1) snprintf(buf_cm, sizeof(buf_cm), PSTR("\",\"colormode\":\"%s"), modeString(dev->getColorMode())); snprintf_P(buf, maxBuf, PSTR("{\"state\":{\"on\":%s,\"bri\":%u%s%s,\"alert\":\"none%s\",\"mode\":\"homeautomation\",\"reachable\":true}," "\"type\":\"%s\",\"name\":\"%s\",\"modelid\":\"%s\",\"manufacturername\":\"Philips\",\"productname\":\"E%u" "\",\"uniqueid\":\"%s\",\"swversion\":\"espalexa-2.7.0\"}") , (dev->getValue())?"true":"false", dev->getLastValue()-1, buf_col, buf_ct, buf_cm, typeString(dev->getType()), dev->getName().c_str(), modelidString(dev->getType()), static_cast(dev->getType()), buf_lightid); } //Espalexa status page /espalexa #ifndef ESPALEXA_NO_SUBPAGE void servePage() { EA_DEBUGLN("HTTP Req espalexa ...\n"); String res = "Hello from Espalexa!\r\n\r\n"; for (int i=0; igetName() + "): " + String(dev->getValue()) + " (" + typeString(dev->getType()); if (static_cast(dev->getType()) > 1) //color support { res += ", colormode=" + String(modeString(dev->getColorMode())) + ", r=" + String(dev->getR()) + ", g=" + String(dev->getG()) + ", b=" + String(dev->getB()); res +=", ct=" + String(dev->getCt()) + ", hue=" + String(dev->getHue()) + ", sat=" + String(dev->getSat()) + ", x=" + String(dev->getX()) + ", y=" + String(dev->getY()); } res += ")\r\n"; } res += "\r\nFree Heap: " + (String)ESP.getFreeHeap(); res += "\r\nUptime: " + (String)millis(); res += "\r\n\r\nEspalexa library v2.7.0 by Christian Schwinne 2021"; server->send(200, "text/plain", res); } #endif //not found URI (only if internal webserver is used) void serveNotFound() { EA_DEBUGLN("Not-Found HTTP call:"); #ifndef ESPALEXA_ASYNC EA_DEBUGLN("URI: " + server->uri()); EA_DEBUGLN("Body: " + server->arg(0)); if(!handleAlexaApiCall(server->uri(), server->arg(0))) #else EA_DEBUGLN("URI: " + server->url()); EA_DEBUGLN("Body: " + body); if(!handleAlexaApiCall(server)) #endif server->send(404, "text/plain", "Not Found (espalexa)"); } //send description.xml device property page void serveDescription() { EA_DEBUGLN("# Responding to description.xml ... #\n"); IPAddress localIP = Network.localIP(); char s[16]; snprintf(s, sizeof(s), "%d.%d.%d.%d", localIP[0], localIP[1], localIP[2], localIP[3]); char buf[1024]; snprintf_P(buf, sizeof(buf), PSTR("" "" "10" "http://%s:80/" "" "urn:schemas-upnp-org:device:Basic:1" "Espalexa (%s:80)" "Royal Philips Electronics" "http://www.philips.com" "Philips hue Personal Wireless Lighting" "Philips hue bridge 2012" "929000226503" "http://www.meethue.com" "%s" "uuid:2f402f80-da50-11e1-9b23-%s" "index.html" "" ""),s,s,escapedMac.c_str(),escapedMac.c_str()); server->send(200, "text/xml", buf); EA_DEBUGLN("Send setup.xml"); EA_DEBUGLN(buf); } //init the server void startHttpServer() { #ifdef ESPALEXA_ASYNC if (serverAsync == nullptr) { serverAsync = new AsyncWebServer(80); serverAsync->onNotFound([=](AsyncWebServerRequest *request){server = request; serveNotFound();}); } serverAsync->onRequestBody([=](AsyncWebServerRequest *request, uint8_t *data, size_t len, size_t index, size_t total){ char b[len +1]; b[len] = 0; memcpy(b, data, len); body = b; //save the body so we can use it for the API call EA_DEBUG("Received body: "); EA_DEBUGLN(body); }); #ifndef ESPALEXA_NO_SUBPAGE serverAsync->on("/espalexa", HTTP_GET, [=](AsyncWebServerRequest *request){server = request; servePage();}); #endif serverAsync->on("/description.xml", HTTP_GET, [=](AsyncWebServerRequest *request){server = request; serveDescription();}); serverAsync->begin(); #else if (server == nullptr) { #ifdef ARDUINO_ARCH_ESP32 server = new WebServer(80); #else server = new ESP8266WebServer(80); #endif server->onNotFound([=](){serveNotFound();}); } #ifndef ESPALEXA_NO_SUBPAGE server->on("/espalexa", HTTP_GET, [=](){servePage();}); #endif server->on("/description.xml", HTTP_GET, [=](){serveDescription();}); server->begin(); #endif } //respond to UDP SSDP M-SEARCH void respondToSearch() { IPAddress localIP = Network.localIP(); char s[16]; sprintf(s, "%d.%d.%d.%d", localIP[0], localIP[1], localIP[2], localIP[3]); char buf[1024]; snprintf_P(buf, sizeof(buf), PSTR("HTTP/1.1 200 OK\r\n" "EXT:\r\n" "CACHE-CONTROL: max-age=100\r\n" // SSDP_INTERVAL "LOCATION: http://%s:80/description.xml\r\n" "SERVER: FreeRTOS/6.0.5, UPnP/1.0, IpBridge/1.17.0\r\n" // _modelName, _modelNumber "hue-bridgeid: %s\r\n" "ST: urn:schemas-upnp-org:device:basic:1\r\n" // _deviceType "USN: uuid:2f402f80-da50-11e1-9b23-%s::upnp:rootdevice\r\n" // _uuid::_deviceType "\r\n"),s,escapedMac.c_str(),escapedMac.c_str()); espalexaUdp.beginPacket(espalexaUdp.remoteIP(), espalexaUdp.remotePort()); #ifdef ARDUINO_ARCH_ESP32 espalexaUdp.write((uint8_t*)buf, strlen(buf)); #else espalexaUdp.write(buf); #endif espalexaUdp.endPacket(); } public: Espalexa(){} //initialize interfaces #ifdef ESPALEXA_ASYNC bool begin(AsyncWebServer* externalServer = nullptr) #elif defined ARDUINO_ARCH_ESP32 bool begin(WebServer* externalServer = nullptr) #else bool begin(ESP8266WebServer* externalServer = nullptr) #endif { EA_DEBUGLN("Espalexa Begin..."); EA_DEBUG("MAXDEVICES "); EA_DEBUGLN(ESPALEXA_MAXDEVICES); escapedMac = WiFi.macAddress(); escapedMac.replace(":", ""); escapedMac.toLowerCase(); String macSubStr = escapedMac.substring(6, 12); mac24 = strtol(macSubStr.c_str(), 0, 16); #ifdef ESPALEXA_ASYNC serverAsync = externalServer; #else server = externalServer; #endif #ifdef ARDUINO_ARCH_ESP32 udpConnected = espalexaUdp.beginMulticast(IPAddress(239, 255, 255, 250), 1900); #else udpConnected = espalexaUdp.beginMulticast(Network.localIP(), IPAddress(239, 255, 255, 250), 1900); #endif if (udpConnected){ startHttpServer(); EA_DEBUGLN("Done"); return true; } EA_DEBUGLN("Failed"); return false; } // get device count, function only in WLED version of Espalexa uint8_t getDeviceCount() { return currentDeviceCount; } //service loop void loop() { #ifndef ESPALEXA_ASYNC if (server == nullptr) return; //only if begin() was not called server->handleClient(); #endif if (!udpConnected) return; int packetSize = espalexaUdp.parsePacket(); if (packetSize < 1) return; //no new udp packet EA_DEBUGLN("Got UDP!"); unsigned char packetBuffer[packetSize+1]; //buffer to hold incoming udp packet espalexaUdp.read(packetBuffer, packetSize); packetBuffer[packetSize] = 0; espalexaUdp.flush(); if (!discoverable) return; //do not reply to M-SEARCH if not discoverable const char* request = (const char *) packetBuffer; if (strstr(request, "M-SEARCH") == nullptr) return; EA_DEBUGLN(request); if (strstr(request, "ssdp:disc") != nullptr && //short for "ssdp:discover" (strstr(request, "upnp:rootd") != nullptr || //short for "upnp:rootdevice" strstr(request, "ssdp:all") != nullptr || strstr(request, "asic:1") != nullptr )) //short for "device:basic:1" { EA_DEBUGLN("Responding search req..."); respondToSearch(); } } // Function only in WLED version of Espalexa, does not actually release memory for names void removeAllDevices() { currentDeviceCount=0; return; } // returns device index or 0 on failure uint8_t addDevice(EspalexaDevice* d) { EA_DEBUG("Adding device "); EA_DEBUGLN((currentDeviceCount+1)); if (currentDeviceCount >= ESPALEXA_MAXDEVICES) return 0; if (d == nullptr) return 0; d->setId(currentDeviceCount); devices[currentDeviceCount] = d; return ++currentDeviceCount; } //brightness-only callback uint8_t addDevice(String deviceName, BrightnessCallbackFunction callback, uint8_t initialValue = 0) { EA_DEBUG("Constructing device "); EA_DEBUGLN((currentDeviceCount+1)); if (currentDeviceCount >= ESPALEXA_MAXDEVICES) return 0; EspalexaDevice* d = new EspalexaDevice(deviceName, callback, initialValue); return addDevice(d); } //brightness-only callback uint8_t addDevice(String deviceName, ColorCallbackFunction callback, uint8_t initialValue = 0) { EA_DEBUG("Constructing device "); EA_DEBUGLN((currentDeviceCount+1)); if (currentDeviceCount >= ESPALEXA_MAXDEVICES) return 0; EspalexaDevice* d = new EspalexaDevice(deviceName, callback, initialValue); return addDevice(d); } uint8_t addDevice(String deviceName, DeviceCallbackFunction callback, EspalexaDeviceType t = EspalexaDeviceType::dimmable, uint8_t initialValue = 0) { EA_DEBUG("Constructing device "); EA_DEBUGLN((currentDeviceCount+1)); if (currentDeviceCount >= ESPALEXA_MAXDEVICES) return 0; EspalexaDevice* d = new EspalexaDevice(deviceName, callback, t, initialValue); return addDevice(d); } void renameDevice(uint8_t id, const String& deviceName) { unsigned int index = id - 1; if (index < currentDeviceCount) devices[index]->setName(deviceName); } //basic implementation of Philips hue api functions needed for basic Alexa control #ifdef ESPALEXA_ASYNC bool handleAlexaApiCall(AsyncWebServerRequest* request) { server = request; //copy request reference String req = request->url(); //body from global variable EA_DEBUGLN(request->contentType()); if (request->hasParam("body", true)) // This is necessary, otherwise ESP crashes if there is no body { EA_DEBUG("BodyMethod2"); body = request->getParam("body", true)->value(); } EA_DEBUG("FinalBody: "); EA_DEBUGLN(body); #else bool handleAlexaApiCall(String req, String body) { #endif EA_DEBUG("URL: "); EA_DEBUGLN(req); EA_DEBUGLN("AlexaApiCall"); if (req.indexOf("api") <0) return false; //return if not an API call EA_DEBUGLN("ok"); if (body.indexOf("devicetype") > 0) //client wants a hue api username, we don't care and give static { EA_DEBUGLN("devType"); body = ""; server->send(200, "application/json", F("[{\"success\":{\"username\":\"2BLEDHardQrI3WHYTHoMcXHgEspsM8ZZRpSKtBGr\"}}]")); return true; } if ((req.indexOf("state") > 0) && (body.length() > 0)) //client wants to control light { uint32_t devId = req.substring(req.indexOf("lights")+7).toInt(); EA_DEBUG("ls"); EA_DEBUGLN(devId); unsigned idx = decodeLightKey(devId); EA_DEBUGLN(idx); char buf[50]; snprintf_P(buf,sizeof(buf),PSTR("[{\"success\":{\"/lights/%u/state/\": true}}]"),devId); server->send(200, "application/json", buf); if (idx >= currentDeviceCount) return true; //return if invalid ID EspalexaDevice* dev = devices[idx]; dev->setPropertyChanged(EspalexaDeviceProperty::none); if (body.indexOf("false")>0) //OFF command { dev->setValue(0); dev->setPropertyChanged(EspalexaDeviceProperty::off); dev->doCallback(); return true; } if (body.indexOf("true") >0) //ON command { dev->setValue(dev->getLastValue()); dev->setPropertyChanged(EspalexaDeviceProperty::on); } if (body.indexOf("bri") >0) //BRIGHTNESS command { uint8_t briL = body.substring(body.indexOf("bri") +5).toInt(); if (briL == 255) { dev->setValue(255); } else { dev->setValue(briL+1); } dev->setPropertyChanged(EspalexaDeviceProperty::bri); } if (body.indexOf("xy") >0) //COLOR command (XY mode) { dev->setColorXY(body.substring(body.indexOf("[") +1).toFloat(), body.substring(body.indexOf(",0") +1).toFloat()); dev->setPropertyChanged(EspalexaDeviceProperty::xy); } if (body.indexOf("hue") >0) //COLOR command (HS mode) { dev->setColor(body.substring(body.indexOf("hue") +5).toInt(), body.substring(body.indexOf("sat") +5).toInt()); dev->setPropertyChanged(EspalexaDeviceProperty::hs); } if (body.indexOf("ct") >0) //COLOR TEMP command (white spectrum) { dev->setColor(body.substring(body.indexOf("ct") +4).toInt()); dev->setPropertyChanged(EspalexaDeviceProperty::ct); } dev->doCallback(); #ifdef ESPALEXA_DEBUG if (dev->getLastChangedProperty() == EspalexaDeviceProperty::none) EA_DEBUGLN("STATE REQ WITHOUT BODY (likely Content-Type issue #6)"); #endif return true; } int pos = req.indexOf("lights"); if (pos > 0) //client wants light info { int devId = req.substring(pos+7).toInt(); EA_DEBUG("l"); EA_DEBUGLN(devId); if (devId == 0) //client wants all lights { EA_DEBUGLN("lAll"); String jsonTemp = "{"; for (int i = 0; isend(200, "application/json", jsonTemp); } else //client wants one light (devId) { EA_DEBUGLN(devId); unsigned int idx = decodeLightKey(devId); if (idx >= currentDeviceCount) idx = 0; //send first device if invalid if (currentDeviceCount == 0) { server->send(200, "application/json", "{}"); return true; } char buf[512]; deviceJsonString(devices[idx], buf, sizeof(buf)-1); server->send(200, "application/json", buf); } return true; } //we don't care about other api commands at this time and send empty JSON server->send(200, "application/json", "{}"); return true; } //set whether Alexa can discover any devices void setDiscoverable(bool d) { discoverable = d; } //get EspalexaDevice at specific index EspalexaDevice* getDevice(uint8_t index) { if (index >= currentDeviceCount) return nullptr; return devices[index]; } //is an unique device ID String getEscapedMac() { return escapedMac; } //convert brightness (0-255) to percentage uint8_t toPercent(uint8_t bri) { uint16_t perc = bri * 100; return perc / 255; } ~Espalexa(){} //note: Espalexa is NOT meant to be destructed }; #endif ================================================ FILE: wled00/src/dependencies/espalexa/EspalexaDevice.cpp ================================================ //EspalexaDevice Class #include "EspalexaDevice.h" // debug macros #ifdef ESPALEXA_DEBUG #define EA_DEBUG(x) Serial.print (x) #define EA_DEBUGLN(x) Serial.println (x) #else #define EA_DEBUG(x) #define EA_DEBUGLN(x) #endif EspalexaDevice::EspalexaDevice(){} EspalexaDevice::EspalexaDevice(String deviceName, BrightnessCallbackFunction gnCallback, uint8_t initialValue) { //constructor for dimmable device _deviceName = deviceName; _callback = gnCallback; _val = initialValue; _val_last = _val; _type = EspalexaDeviceType::dimmable; } EspalexaDevice::EspalexaDevice(String deviceName, ColorCallbackFunction gnCallback, uint8_t initialValue) { //constructor for color device _deviceName = deviceName; _callbackCol = gnCallback; _val = initialValue; _val_last = _val; _type = EspalexaDeviceType::extendedcolor; } EspalexaDevice::EspalexaDevice(String deviceName, DeviceCallbackFunction gnCallback, EspalexaDeviceType t, uint8_t initialValue) { //constructor for general device _deviceName = deviceName; _callbackDev = gnCallback; _type = t; if (t == EspalexaDeviceType::onoff) _type = EspalexaDeviceType::dimmable; //on/off is broken, so make dimmable device instead if (t == EspalexaDeviceType::whitespectrum) _mode = EspalexaColorMode::ct; _val = initialValue; _val_last = _val; } EspalexaDevice::~EspalexaDevice(){/*nothing to destruct*/} uint8_t EspalexaDevice::getId() { return _id; } EspalexaColorMode EspalexaDevice::getColorMode() { return _mode; } EspalexaDeviceType EspalexaDevice::getType() { return _type; } String EspalexaDevice::getName() { return _deviceName; } EspalexaDeviceProperty EspalexaDevice::getLastChangedProperty() { return _changed; } uint8_t EspalexaDevice::getValue() { return _val; } bool EspalexaDevice::getState() { return _val; } uint8_t EspalexaDevice::getPercent() { uint16_t perc = _val * 100; return perc / 255; } uint8_t EspalexaDevice::getDegrees() { return getPercent(); } uint16_t EspalexaDevice::getHue() { return _hue; } uint8_t EspalexaDevice::getSat() { return _sat; } float EspalexaDevice::getX() { return _x; } float EspalexaDevice::getY() { return _y; } uint16_t EspalexaDevice::getCt() { if (_ct == 0) return 500; return _ct; } uint32_t EspalexaDevice::getKelvin() { if (_ct == 0) return 2000; return 1000000/_ct; } uint32_t EspalexaDevice::getRGB() { if (_rgb != 0) return _rgb; //color has not changed byte rgb[4]{0, 0, 0, 0}; if (_mode == EspalexaColorMode::none) return 0; if (_mode == EspalexaColorMode::ct) { //TODO tweak a bit to match hue lamp characteristics //based on https://gist.github.com/paulkaplan/5184275 float temp = (_ct != 0) ? (10000/ _ct) : 2; //kelvins = 1,000,000/mired (and that /100) softhack007: avoid division by zero - using "2" as substitute float r, g, b; #ifdef ESPALEXA_DEBUG if (_ct == 0) {EA_DEBUGLN(F("EspalexaDevice::getRGB() Warning: ct = 0!"));} #endif if (temp <= 66) { r = 255; g = temp; g = 99.470802 * logf(g) - 161.119568; if (temp <= 19) { b = 0; } else { b = temp-10; b = 138.517731 * logf(b) - 305.044793; } } else { r = temp - 60; r = 329.698727 * pow(r, -0.13320476); g = temp - 60; g = 288.12217 * pow(g, -0.07551485 ); b = 255; } rgb[0] = (byte)constrain(r,0.1,255.1); rgb[1] = (byte)constrain(g,0.1,255.1); rgb[2] = (byte)constrain(b,0.1,255.1); } else if (_mode == EspalexaColorMode::hs) { float h = ((float)_hue)/65535.0; float s = ((float)_sat)/255.0; byte i = floor(h*6); float f = h * 6-i; float p = 255 * (1-s); float q = 255 * (1-f*s); float t = 255 * (1-(1-f)*s); switch (i%6) { case 0: rgb[0]=255,rgb[1]=t,rgb[2]=p;break; case 1: rgb[0]=q,rgb[1]=255,rgb[2]=p;break; case 2: rgb[0]=p,rgb[1]=255,rgb[2]=t;break; case 3: rgb[0]=p,rgb[1]=q,rgb[2]=255;break; case 4: rgb[0]=t,rgb[1]=p,rgb[2]=255;break; case 5: rgb[0]=255,rgb[1]=p,rgb[2]=q; } } else if (_mode == EspalexaColorMode::xy) { //Source: https://www.developers.meethue.com/documentation/color-conversions-rgb-xy float z = 1.0f - _x - _y; float X = (1.0f / _y) * _x; float Z = (1.0f / _y) * z; float r = (int)255*(X * 1.656492f - 0.354851f - Z * 0.255038f); float g = (int)255*(-X * 0.707196f + 1.655397f + Z * 0.036152f); float b = (int)255*(X * 0.051713f - 0.121364f + Z * 1.011530f); if (r > b && r > g && r > 1.0f) { // red is too big g = g / r; b = b / r; r = 1.0f; } else if (g > b && g > r && g > 1.0f) { // green is too big r = r / g; b = b / g; g = 1.0f; } else if (b > r && b > g && b > 1.0f) { // blue is too big r = r / b; g = g / b; b = 1.0f; } // Apply gamma correction r = r <= 0.0031308f ? 12.92f * r : (1.0f + 0.055f) * powf(r, (1.0f / 2.4f)) - 0.055f; g = g <= 0.0031308f ? 12.92f * g : (1.0f + 0.055f) * powf(g, (1.0f / 2.4f)) - 0.055f; b = b <= 0.0031308f ? 12.92f * b : (1.0f + 0.055f) * powf(b, (1.0f / 2.4f)) - 0.055f; if (r > b && r > g) { // red is biggest if (r > 1.0f) { g = g / r; b = b / r; r = 1.0f; } } else if (g > b && g > r) { // green is biggest if (g > 1.0f) { r = r / g; b = b / g; g = 1.0f; } } else if (b > r && b > g) { // blue is biggest if (b > 1.0f) { r = r / b; g = g / b; b = 1.0f; } } rgb[0] = 255.0*r; rgb[1] = 255.0*g; rgb[2] = 255.0*b; } _rgb = ((rgb[0] << 16) | (rgb[1] << 8) | (rgb[2])); return _rgb; } //white channel for RGBW lights. Always 0 unless colormode is ct uint8_t EspalexaDevice::getW() { return (getRGB() >> 24) & 0xFF; } uint8_t EspalexaDevice::getR() { return (getRGB() >> 16) & 0xFF; } uint8_t EspalexaDevice::getG() { return (getRGB() >> 8) & 0xFF; } uint8_t EspalexaDevice::getB() { return getRGB() & 0xFF; } uint8_t EspalexaDevice::getLastValue() { if (_val_last == 0) return 255; return _val_last; } void EspalexaDevice::setPropertyChanged(EspalexaDeviceProperty p) { _changed = p; } void EspalexaDevice::setId(uint8_t id) { _id = id; } //you need to re-discover the device for the Alexa name to change void EspalexaDevice::setName(String name) { _deviceName = name; } void EspalexaDevice::setValue(uint8_t val) { if (_val != 0) { _val_last = _val; } if (val != 0) { _val_last = val; } _val = val; } void EspalexaDevice::setState(bool onoff) { if (onoff) { setValue(_val_last); } else { setValue(0); } } void EspalexaDevice::setPercent(uint8_t perc) { uint16_t val = perc * 255; val /= 100; if (val > 255) val = 255; setValue(val); } void EspalexaDevice::setColorXY(float x, float y) { _x = x; _y = y; _rgb = 0; _mode = EspalexaColorMode::xy; } void EspalexaDevice::setColor(uint16_t hue, uint8_t sat) { _hue = hue; _sat = sat; _rgb = 0; _mode = EspalexaColorMode::hs; } void EspalexaDevice::setColor(uint16_t ct) { _ct = ct; _rgb = 0; _mode =EspalexaColorMode::ct; } void EspalexaDevice::setColor(uint8_t r, uint8_t g, uint8_t b) { float X = r * 0.664511f + g * 0.154324f + b * 0.162028f; float Y = r * 0.283881f + g * 0.668433f + b * 0.047685f; float Z = r * 0.000088f + g * 0.072310f + b * 0.986039f; if ((r+g+b) > 0) { // softhack007: avoid division by zero _x = X / (X + Y + Z); _y = Y / (X + Y + Z); } else { _x = _y = 0.5f;} // softhack007: use default values in case of "black" _rgb = ((r << 16) | (g << 8) | b); _mode = EspalexaColorMode::xy; } void EspalexaDevice::doCallback() { if (_callback != nullptr) {_callback(_val); return;} if (_callbackDev != nullptr) {_callbackDev(this); return;} if (_callbackCol != nullptr) _callbackCol(_val, getRGB()); } ================================================ FILE: wled00/src/dependencies/espalexa/EspalexaDevice.h ================================================ #ifndef EspalexaDevice_h #define EspalexaDevice_h #include "Arduino.h" #include class EspalexaDevice; typedef std::function BrightnessCallbackFunction; typedef std::function DeviceCallbackFunction; typedef std::function ColorCallbackFunction; enum class EspalexaColorMode : uint8_t { none = 0, ct = 1, hs = 2, xy = 3 }; enum class EspalexaDeviceType : uint8_t { onoff = 0, dimmable = 1, whitespectrum = 2, color = 3, extendedcolor = 4 }; enum class EspalexaDeviceProperty : uint8_t { none = 0, on = 1, off = 2, bri = 3, hs = 4, ct = 5, xy = 6 }; class EspalexaDevice { private: String _deviceName; BrightnessCallbackFunction _callback = nullptr; DeviceCallbackFunction _callbackDev = nullptr; ColorCallbackFunction _callbackCol = nullptr; uint8_t _val, _val_last, _sat = 0; uint16_t _hue = 0, _ct = 0; float _x = 0.5f, _y = 0.5f; uint32_t _rgb = 0; uint8_t _id = 0; EspalexaDeviceType _type; EspalexaDeviceProperty _changed = EspalexaDeviceProperty::none; EspalexaColorMode _mode = EspalexaColorMode::xy; public: EspalexaDevice(); ~EspalexaDevice(); EspalexaDevice(String deviceName, BrightnessCallbackFunction bcb, uint8_t initialValue =0); EspalexaDevice(String deviceName, DeviceCallbackFunction dcb, EspalexaDeviceType t =EspalexaDeviceType::dimmable, uint8_t initialValue =0); EspalexaDevice(String deviceName, ColorCallbackFunction ccb, uint8_t initialValue =0); String getName(); uint8_t getId(); EspalexaDeviceProperty getLastChangedProperty(); uint8_t getValue(); uint8_t getLastValue(); //last value that was not off (1-255) bool getState(); uint8_t getPercent(); uint8_t getDegrees(); uint16_t getHue(); uint8_t getSat(); uint16_t getCt(); uint32_t getKelvin(); float getX(); float getY(); uint32_t getRGB(); uint8_t getR(); uint8_t getG(); uint8_t getB(); uint8_t getW(); EspalexaColorMode getColorMode(); EspalexaDeviceType getType(); void setId(uint8_t id); void setPropertyChanged(EspalexaDeviceProperty p); void setValue(uint8_t bri); void setState(bool onoff); void setPercent(uint8_t perc); void setName(String name); void setColor(uint16_t ct); void setColor(uint16_t hue, uint8_t sat); void setColorXY(float x, float y); void setColor(uint8_t r, uint8_t g, uint8_t b); void doCallback(); }; #endif ================================================ FILE: wled00/src/dependencies/espalexa/LICENSE ================================================ MIT License Copyright (c) 2017 Christian Schwinne Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: wled00/src/dependencies/json/ArduinoJson-v6.h ================================================ // ArduinoJson - https://arduinojson.org // Copyright Benoit Blanchon 2014-2021 // MIT License #pragma once #ifdef __cplusplus #if __cplusplus >= 201103L # define ARDUINOJSON_HAS_LONG_LONG 1 # define ARDUINOJSON_HAS_RVALUE_REFERENCES 1 #else # define ARDUINOJSON_HAS_LONG_LONG 0 # define ARDUINOJSON_HAS_RVALUE_REFERENCES 0 #endif #ifndef ARDUINOJSON_HAS_NULLPTR # if __cplusplus >= 201103L # define ARDUINOJSON_HAS_NULLPTR 1 # else # define ARDUINOJSON_HAS_NULLPTR 0 # endif #endif #if defined(_MSC_VER) && !ARDUINOJSON_HAS_LONG_LONG # define ARDUINOJSON_HAS_INT64 1 #else # define ARDUINOJSON_HAS_INT64 0 #endif #ifndef ARDUINOJSON_EMBEDDED_MODE # if defined(ARDUINO) /* Arduino*/ \ || defined(__IAR_SYSTEMS_ICC__) /* IAR Embedded Workbench */ \ || defined(__XC) /* MPLAB XC compiler */ \ || defined(__ARMCC_VERSION) /* Keil ARM Compiler */ \ || defined(__AVR) /* Atmel AVR8/GNU C Compiler */ # define ARDUINOJSON_EMBEDDED_MODE 1 # else # define ARDUINOJSON_EMBEDDED_MODE 0 # endif #endif #if !defined(ARDUINOJSON_ENABLE_STD_STREAM) && defined(__has_include) # if __has_include() && \ __has_include() && \ !defined(min) && \ !defined(max) # define ARDUINOJSON_ENABLE_STD_STREAM 1 # else # define ARDUINOJSON_ENABLE_STD_STREAM 0 # endif #endif #if !defined(ARDUINOJSON_ENABLE_STD_STRING) && defined(__has_include) # if __has_include() && !defined(min) && !defined(max) # define ARDUINOJSON_ENABLE_STD_STRING 1 # else # define ARDUINOJSON_ENABLE_STD_STRING 0 # endif #endif #ifndef ARDUINOJSON_ENABLE_STRING_VIEW # ifdef __has_include # if __has_include() && __cplusplus >= 201703L # define ARDUINOJSON_ENABLE_STRING_VIEW 1 # endif # endif #endif #ifndef ARDUINOJSON_ENABLE_STRING_VIEW # define ARDUINOJSON_ENABLE_STRING_VIEW 0 #endif #if ARDUINOJSON_EMBEDDED_MODE # ifndef ARDUINOJSON_USE_DOUBLE # define ARDUINOJSON_USE_DOUBLE 0 # endif # ifndef ARDUINOJSON_USE_LONG_LONG # define ARDUINOJSON_USE_LONG_LONG 0 # endif # ifndef ARDUINOJSON_ENABLE_STD_STRING # define ARDUINOJSON_ENABLE_STD_STRING 0 # endif # ifndef ARDUINOJSON_ENABLE_STD_STREAM # define ARDUINOJSON_ENABLE_STD_STREAM 0 # endif # ifndef ARDUINOJSON_DEFAULT_NESTING_LIMIT # define ARDUINOJSON_DEFAULT_NESTING_LIMIT 10 # endif # ifndef ARDUINOJSON_SLOT_OFFSET_SIZE # if defined(__SIZEOF_POINTER__) && __SIZEOF_POINTER__ == 2 # define ARDUINOJSON_SLOT_OFFSET_SIZE 1 # else # define ARDUINOJSON_SLOT_OFFSET_SIZE 2 # endif # endif #else // ARDUINOJSON_EMBEDDED_MODE # ifndef ARDUINOJSON_USE_DOUBLE # define ARDUINOJSON_USE_DOUBLE 1 # endif # ifndef ARDUINOJSON_USE_LONG_LONG # if ARDUINOJSON_HAS_LONG_LONG || ARDUINOJSON_HAS_INT64 # define ARDUINOJSON_USE_LONG_LONG 1 # else # define ARDUINOJSON_USE_LONG_LONG 0 # endif # endif # ifndef ARDUINOJSON_ENABLE_STD_STRING # define ARDUINOJSON_ENABLE_STD_STRING 1 # endif # ifndef ARDUINOJSON_ENABLE_STD_STREAM # define ARDUINOJSON_ENABLE_STD_STREAM 1 # endif # ifndef ARDUINOJSON_DEFAULT_NESTING_LIMIT # define ARDUINOJSON_DEFAULT_NESTING_LIMIT 50 # endif # ifndef ARDUINOJSON_SLOT_OFFSET_SIZE # define ARDUINOJSON_SLOT_OFFSET_SIZE 4 # endif #endif // ARDUINOJSON_EMBEDDED_MODE #ifdef ARDUINO #include # ifndef ARDUINOJSON_ENABLE_ARDUINO_STRING # define ARDUINOJSON_ENABLE_ARDUINO_STRING 1 # endif # ifndef ARDUINOJSON_ENABLE_ARDUINO_STREAM # define ARDUINOJSON_ENABLE_ARDUINO_STREAM 1 # endif # ifndef ARDUINOJSON_ENABLE_ARDUINO_PRINT # define ARDUINOJSON_ENABLE_ARDUINO_PRINT 1 # endif #else // ARDUINO # ifndef ARDUINOJSON_ENABLE_ARDUINO_STRING # define ARDUINOJSON_ENABLE_ARDUINO_STRING 0 # endif # ifndef ARDUINOJSON_ENABLE_ARDUINO_STREAM # define ARDUINOJSON_ENABLE_ARDUINO_STREAM 0 # endif # ifndef ARDUINOJSON_ENABLE_ARDUINO_PRINT # define ARDUINOJSON_ENABLE_ARDUINO_PRINT 0 # endif #endif // ARDUINO #ifndef ARDUINOJSON_ENABLE_PROGMEM # if defined(PROGMEM) && defined(pgm_read_byte) && defined(pgm_read_dword) && \ defined(pgm_read_ptr) && defined(pgm_read_float) # define ARDUINOJSON_ENABLE_PROGMEM 1 # else # define ARDUINOJSON_ENABLE_PROGMEM 0 # endif #endif #ifndef ARDUINOJSON_DECODE_UNICODE # define ARDUINOJSON_DECODE_UNICODE 1 #endif #ifndef ARDUINOJSON_ENABLE_COMMENTS # define ARDUINOJSON_ENABLE_COMMENTS 0 #endif #ifndef ARDUINOJSON_ENABLE_NAN # define ARDUINOJSON_ENABLE_NAN 0 #endif #ifndef ARDUINOJSON_ENABLE_INFINITY # define ARDUINOJSON_ENABLE_INFINITY 0 #endif #ifndef ARDUINOJSON_POSITIVE_EXPONENTIATION_THRESHOLD # define ARDUINOJSON_POSITIVE_EXPONENTIATION_THRESHOLD 1e7 #endif #ifndef ARDUINOJSON_NEGATIVE_EXPONENTIATION_THRESHOLD # define ARDUINOJSON_NEGATIVE_EXPONENTIATION_THRESHOLD 1e-5 #endif #ifndef ARDUINOJSON_LITTLE_ENDIAN # if defined(_MSC_VER) || \ (defined(__BYTE_ORDER__) && \ __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__) || \ defined(__LITTLE_ENDIAN__) || defined(__i386) || defined(__x86_64) # define ARDUINOJSON_LITTLE_ENDIAN 1 # else # define ARDUINOJSON_LITTLE_ENDIAN 0 # endif #endif #ifndef ARDUINOJSON_ENABLE_ALIGNMENT # if defined(__AVR) # define ARDUINOJSON_ENABLE_ALIGNMENT 0 # else # define ARDUINOJSON_ENABLE_ALIGNMENT 1 # endif #endif #ifndef ARDUINOJSON_TAB # define ARDUINOJSON_TAB " " #endif #ifndef ARDUINOJSON_ENABLE_STRING_DEDUPLICATION # define ARDUINOJSON_ENABLE_STRING_DEDUPLICATION 1 #endif #ifndef ARDUINOJSON_STRING_BUFFER_SIZE # define ARDUINOJSON_STRING_BUFFER_SIZE 32 #endif #ifndef ARDUINOJSON_DEBUG # ifdef __PLATFORMIO_BUILD_DEBUG__ # define ARDUINOJSON_DEBUG 1 # else # define ARDUINOJSON_DEBUG 0 # endif #endif #if ARDUINOJSON_HAS_NULLPTR && defined(nullptr) # error nullptr is defined as a macro. Remove the faulty #define or #undef nullptr #endif #if !ARDUINOJSON_DEBUG # ifdef __clang__ # pragma clang system_header # elif defined __GNUC__ # pragma GCC system_header # endif #endif #define ARDUINOJSON_EXPAND6(a, b, c, d, e, f) a, b, c, d, e, f #define ARDUINOJSON_EXPAND9(a, b, c, d, e, f, g, h, i) a, b, c, d, e, f, g, h, i #define ARDUINOJSON_EXPAND18(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, \ q, r) \ a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r #define ARDUINOJSON_CONCAT_(A, B) A##B #define ARDUINOJSON_CONCAT2(A, B) ARDUINOJSON_CONCAT_(A, B) #define ARDUINOJSON_CONCAT4(A, B, C, D) \ ARDUINOJSON_CONCAT2(ARDUINOJSON_CONCAT2(A, B), ARDUINOJSON_CONCAT2(C, D)) #define ARDUINOJSON_HEX_DIGIT_0000() 0 #define ARDUINOJSON_HEX_DIGIT_0001() 1 #define ARDUINOJSON_HEX_DIGIT_0010() 2 #define ARDUINOJSON_HEX_DIGIT_0011() 3 #define ARDUINOJSON_HEX_DIGIT_0100() 4 #define ARDUINOJSON_HEX_DIGIT_0101() 5 #define ARDUINOJSON_HEX_DIGIT_0110() 6 #define ARDUINOJSON_HEX_DIGIT_0111() 7 #define ARDUINOJSON_HEX_DIGIT_1000() 8 #define ARDUINOJSON_HEX_DIGIT_1001() 9 #define ARDUINOJSON_HEX_DIGIT_1010() A #define ARDUINOJSON_HEX_DIGIT_1011() B #define ARDUINOJSON_HEX_DIGIT_1100() C #define ARDUINOJSON_HEX_DIGIT_1101() D #define ARDUINOJSON_HEX_DIGIT_1110() E #define ARDUINOJSON_HEX_DIGIT_1111() F #define ARDUINOJSON_HEX_DIGIT_(A, B, C, D) ARDUINOJSON_HEX_DIGIT_##A##B##C##D() #define ARDUINOJSON_HEX_DIGIT(A, B, C, D) ARDUINOJSON_HEX_DIGIT_(A, B, C, D) #define ARDUINOJSON_VERSION "6.18.1" #define ARDUINOJSON_VERSION_MAJOR 6 #define ARDUINOJSON_VERSION_MINOR 18 #define ARDUINOJSON_VERSION_REVISION 1 #ifndef ARDUINOJSON_NAMESPACE # define ARDUINOJSON_NAMESPACE \ ARDUINOJSON_CONCAT4( \ ARDUINOJSON_CONCAT4(ArduinoJson, ARDUINOJSON_VERSION_MAJOR, \ ARDUINOJSON_VERSION_MINOR, \ ARDUINOJSON_VERSION_REVISION), \ _, \ ARDUINOJSON_HEX_DIGIT( \ ARDUINOJSON_ENABLE_PROGMEM, ARDUINOJSON_USE_LONG_LONG, \ ARDUINOJSON_USE_DOUBLE, ARDUINOJSON_ENABLE_STRING_DEDUPLICATION), \ ARDUINOJSON_HEX_DIGIT( \ ARDUINOJSON_ENABLE_NAN, ARDUINOJSON_ENABLE_INFINITY, \ ARDUINOJSON_ENABLE_COMMENTS, ARDUINOJSON_DECODE_UNICODE)) #endif #if ARDUINOJSON_DEBUG #include # define ARDUINOJSON_ASSERT(X) assert(X) #else # define ARDUINOJSON_ASSERT(X) ((void)0) #endif #include namespace ARDUINOJSON_NAMESPACE { class MemoryPool; class VariantData; class VariantSlot; class CollectionData { VariantSlot *_head; VariantSlot *_tail; public: VariantData *addElement(MemoryPool *pool); VariantData *getElement(size_t index) const; VariantData *getOrAddElement(size_t index, MemoryPool *pool); void removeElement(size_t index); bool equalsArray(const CollectionData &other) const; template VariantData *addMember(TAdaptedString key, MemoryPool *pool); template VariantData *getMember(TAdaptedString key) const; template VariantData *getOrAddMember(TAdaptedString key, MemoryPool *pool); template void removeMember(TAdaptedString key) { removeSlot(getSlot(key)); } template bool containsKey(const TAdaptedString &key) const; bool equalsObject(const CollectionData &other) const; void clear(); size_t memoryUsage() const; size_t nesting() const; size_t size() const; VariantSlot *addSlot(MemoryPool *); void removeSlot(VariantSlot *slot); bool copyFrom(const CollectionData &src, MemoryPool *pool); VariantSlot *head() const { return _head; } void movePointers(ptrdiff_t stringDistance, ptrdiff_t variantDistance); private: VariantSlot *getSlot(size_t index) const; template VariantSlot *getSlot(TAdaptedString key) const; VariantSlot *getPreviousSlot(VariantSlot *) const; }; inline VariantData *arrayAdd(CollectionData *arr, MemoryPool *pool) { return arr ? arr->addElement(pool) : 0; } template inline typename TVisitor::result_type arrayAccept(const CollectionData *arr, TVisitor &visitor) { if (arr) return visitor.visitArray(*arr); else return visitor.visitNull(); } inline bool arrayEquals(const CollectionData *lhs, const CollectionData *rhs) { if (lhs == rhs) return true; if (!lhs || !rhs) return false; return lhs->equalsArray(*rhs); } #if ARDUINOJSON_ENABLE_ALIGNMENT inline bool isAligned(size_t value) { const size_t mask = sizeof(void *) - 1; size_t addr = value; return (addr & mask) == 0; } inline size_t addPadding(size_t bytes) { const size_t mask = sizeof(void *) - 1; return (bytes + mask) & ~mask; } template struct AddPadding { static const size_t mask = sizeof(void *) - 1; static const size_t value = (bytes + mask) & ~mask; }; #else inline bool isAligned(size_t) { return true; } inline size_t addPadding(size_t bytes) { return bytes; } template struct AddPadding { static const size_t value = bytes; }; #endif template inline bool isAligned(T *ptr) { return isAligned(reinterpret_cast(ptr)); } template inline T *addPadding(T *p) { size_t address = addPadding(reinterpret_cast(p)); return reinterpret_cast(address); } template Y)> struct Max {}; template struct Max { static const size_t value = X; }; template struct Max { static const size_t value = Y; }; } // namespace ARDUINOJSON_NAMESPACE #include #include namespace ARDUINOJSON_NAMESPACE { inline int safe_strcmp(const char* a, const char* b) { if (a == b) return 0; if (!a) return -1; if (!b) return 1; return strcmp(a, b); } inline int safe_strncmp(const char* a, const char* b, size_t n) { if (a == b) return 0; if (!a) return -1; if (!b) return 1; return strncmp(a, b, n); } template struct conditional { typedef TrueType type; }; template struct conditional { typedef FalseType type; }; template struct enable_if {}; template struct enable_if { typedef T type; }; template struct integral_constant { static const T value = v; }; typedef integral_constant true_type; typedef integral_constant false_type; template struct is_array : false_type {}; template struct is_array : true_type {}; template struct is_array : true_type {}; template class is_base_of { protected: // <- to avoid GCC's "all member functions in class are private" typedef char Yes[1]; typedef char No[2]; static Yes &probe(const TBase *); static No &probe(...); public: static const bool value = sizeof(probe(reinterpret_cast(0))) == sizeof(Yes); }; template T declval(); template struct is_class { protected: // <- to avoid GCC's "all member functions in class are private" typedef char Yes[1]; typedef char No[2]; template static Yes &probe(void (U::*)(void)); template static No &probe(...); public: static const bool value = sizeof(probe(0)) == sizeof(Yes); }; template struct is_const : false_type {}; template struct is_const : true_type {}; } // namespace ARDUINOJSON_NAMESPACE #ifdef _MSC_VER # pragma warning(push) # pragma warning(disable : 4244) #endif #ifdef __ICCARM__ #pragma diag_suppress=Pa093 #endif namespace ARDUINOJSON_NAMESPACE { template struct is_convertible { protected: // <- to avoid GCC's "all member functions in class are private" typedef char Yes[1]; typedef char No[2]; static Yes &probe(To); static No &probe(...); public: static const bool value = sizeof(probe(declval())) == sizeof(Yes); }; } // namespace ARDUINOJSON_NAMESPACE #ifdef _MSC_VER # pragma warning(pop) #endif #ifdef __ICCARM__ #pragma diag_default=Pa093 #endif namespace ARDUINOJSON_NAMESPACE { template struct is_same : false_type {}; template struct is_same : true_type {}; template struct remove_cv { typedef T type; }; template struct remove_cv { typedef T type; }; template struct remove_cv { typedef T type; }; template struct remove_cv { typedef T type; }; template struct is_floating_point : integral_constant< bool, // is_same::type>::value || is_same::type>::value> {}; template struct is_integral : integral_constant::type, signed char>::value || is_same::type, unsigned char>::value || is_same::type, signed short>::value || is_same::type, unsigned short>::value || is_same::type, signed int>::value || is_same::type, unsigned int>::value || is_same::type, signed long>::value || is_same::type, unsigned long>::value || #if ARDUINOJSON_HAS_LONG_LONG is_same::type, signed long long>::value || is_same::type, unsigned long long>::value || #endif #if ARDUINOJSON_HAS_INT64 is_same::type, signed __int64>::value || is_same::type, unsigned __int64>::value || #endif is_same::type, char>::value || is_same::type, bool>::value> {}; template struct is_enum { static const bool value = is_convertible::value && !is_class::value && !is_integral::value && !is_floating_point::value; }; template struct is_pointer : false_type {}; template struct is_pointer : true_type {}; template struct is_signed : integral_constant::type, char>::value || is_same::type, signed char>::value || is_same::type, signed short>::value || is_same::type, signed int>::value || is_same::type, signed long>::value || #if ARDUINOJSON_HAS_LONG_LONG is_same::type, signed long long>::value || #endif #if ARDUINOJSON_HAS_INT64 is_same::type, signed __int64>::value || #endif is_same::type, float>::value || is_same::type, double>::value> {}; template struct is_unsigned : integral_constant::type, unsigned char>::value || is_same::type, unsigned short>::value || is_same::type, unsigned int>::value || is_same::type, unsigned long>::value || #if ARDUINOJSON_HAS_INT64 is_same::type, unsigned __int64>::value || #endif #if ARDUINOJSON_HAS_LONG_LONG is_same::type, unsigned long long>::value || #endif is_same::type, bool>::value> {}; template struct type_identity { typedef T type; }; template struct make_unsigned; template <> struct make_unsigned : type_identity {}; template <> struct make_unsigned : type_identity {}; template <> struct make_unsigned : type_identity {}; template <> struct make_unsigned : type_identity {}; template <> struct make_unsigned : type_identity {}; template <> struct make_unsigned : type_identity {}; template <> struct make_unsigned : type_identity {}; template <> struct make_unsigned : type_identity {}; template <> struct make_unsigned : type_identity {}; #if ARDUINOJSON_HAS_LONG_LONG template <> struct make_unsigned : type_identity {}; template <> struct make_unsigned : type_identity {}; #endif #if ARDUINOJSON_HAS_INT64 template <> struct make_unsigned : type_identity {}; template <> struct make_unsigned : type_identity {}; #endif template struct remove_const { typedef T type; }; template struct remove_const { typedef T type; }; template struct remove_reference { typedef T type; }; template struct remove_reference { typedef T type; }; template struct IsString : false_type {}; template struct IsString : IsString {}; template struct IsString : IsString {}; namespace storage_policies { struct store_by_address {}; struct store_by_copy {}; struct decide_at_runtime {}; } // namespace storage_policies class ConstRamStringAdapter { public: ConstRamStringAdapter(const char* str = 0) : _str(str) {} int compare(const char* other) const { return safe_strcmp(_str, other); } bool equals(const char* expected) const { return compare(expected) == 0; } bool isNull() const { return !_str; } size_t size() const { if (!_str) return 0; return strlen(_str); } const char* data() const { return _str; } typedef storage_policies::store_by_address storage_policy; protected: const char* _str; }; template <> struct IsString : true_type {}; template struct IsString : true_type {}; inline ConstRamStringAdapter adaptString(const char* str) { return ConstRamStringAdapter(str); } class RamStringAdapter : public ConstRamStringAdapter { public: RamStringAdapter(const char* str) : ConstRamStringAdapter(str) {} void copyTo(char* p, size_t n) const { memcpy(p, _str, n); } typedef ARDUINOJSON_NAMESPACE::storage_policies::store_by_copy storage_policy; }; template inline RamStringAdapter adaptString(const TChar* str) { return RamStringAdapter(reinterpret_cast(str)); } inline RamStringAdapter adaptString(char* str) { return RamStringAdapter(str); } template struct IsString { static const bool value = sizeof(TChar) == 1; }; template <> struct IsString { static const bool value = false; }; class SizedRamStringAdapter { public: SizedRamStringAdapter(const char* str, size_t n) : _str(str), _size(n) {} int compare(const char* other) const { return safe_strncmp(_str, other, _size); } bool equals(const char* expected) const { return compare(expected) == 0; } bool isNull() const { return !_str; } void copyTo(char* p, size_t n) const { memcpy(p, _str, n); } size_t size() const { return _size; } typedef storage_policies::store_by_copy storage_policy; private: const char* _str; size_t _size; }; template inline SizedRamStringAdapter adaptString(const TChar* str, size_t size) { return SizedRamStringAdapter(reinterpret_cast(str), size); } } // namespace ARDUINOJSON_NAMESPACE #if ARDUINOJSON_ENABLE_STD_STRING #include namespace ARDUINOJSON_NAMESPACE { template class StdStringAdapter { public: StdStringAdapter(const TString& str) : _str(&str) {} void copyTo(char* p, size_t n) const { memcpy(p, _str->c_str(), n); } bool isNull() const { return false; } int compare(const char* other) const { if (!other) return 1; return _str->compare(other); } bool equals(const char* expected) const { if (!expected) return false; return *_str == expected; } size_t size() const { return _str->size(); } typedef storage_policies::store_by_copy storage_policy; private: const TString* _str; }; template struct IsString > : true_type { }; template inline StdStringAdapter > adaptString(const std::basic_string& str) { return StdStringAdapter >( str); } } // namespace ARDUINOJSON_NAMESPACE #endif #if ARDUINOJSON_ENABLE_STRING_VIEW #include namespace ARDUINOJSON_NAMESPACE { class StringViewAdapter { public: StringViewAdapter(std::string_view str) : _str(str) {} void copyTo(char* p, size_t n) const { memcpy(p, _str.data(), n); } bool isNull() const { return false; } int compare(const char* other) const { if (!other) return 1; return _str.compare(other); } bool equals(const char* expected) const { if (!expected) return false; return _str == expected; } size_t size() const { return _str.size(); } typedef storage_policies::store_by_copy storage_policy; private: std::string_view _str; }; template <> struct IsString : true_type {}; inline StringViewAdapter adaptString(const std::string_view& str) { return StringViewAdapter(str); } } // namespace ARDUINOJSON_NAMESPACE #endif #if ARDUINOJSON_ENABLE_ARDUINO_STRING namespace ARDUINOJSON_NAMESPACE { class ArduinoStringAdapter { public: ArduinoStringAdapter(const ::String& str) : _str(&str) {} void copyTo(char* p, size_t n) const { memcpy(p, _str->c_str(), n); } bool isNull() const { return !_str->c_str(); } int compare(const char* other) const { const char* me = _str->c_str(); return safe_strcmp(me, other); } bool equals(const char* expected) const { return compare(expected) == 0; } size_t size() const { return _str->length(); } typedef storage_policies::store_by_copy storage_policy; private: const ::String* _str; }; template <> struct IsString< ::String> : true_type {}; template <> struct IsString< ::StringSumHelper> : true_type {}; inline ArduinoStringAdapter adaptString(const ::String& str) { return ArduinoStringAdapter(str); } } // namespace ARDUINOJSON_NAMESPACE #endif #if ARDUINOJSON_ENABLE_PROGMEM namespace ARDUINOJSON_NAMESPACE { struct pgm_p { pgm_p(const char* p) : address(p) {} const char* address; }; } // namespace ARDUINOJSON_NAMESPACE #ifndef strlen_P inline size_t strlen_P(ARDUINOJSON_NAMESPACE::pgm_p s) { const char* p = s.address; ARDUINOJSON_ASSERT(p != NULL); while (pgm_read_byte(p)) p++; return size_t(p - s.address); } #endif #ifndef strncmp_P inline int strncmp_P(const char* a, ARDUINOJSON_NAMESPACE::pgm_p b, size_t n) { const char* s1 = a; const char* s2 = b.address; ARDUINOJSON_ASSERT(s1 != NULL); ARDUINOJSON_ASSERT(s2 != NULL); while (n-- > 0) { char c1 = *s1++; char c2 = static_cast(pgm_read_byte(s2++)); if (c1 < c2) return -1; if (c1 > c2) return 1; if (c1 == 0 /* and c2 as well */) return 0; } return 0; } #endif #ifndef strcmp_P inline int strcmp_P(const char* a, ARDUINOJSON_NAMESPACE::pgm_p b) { const char* s1 = a; const char* s2 = b.address; ARDUINOJSON_ASSERT(s1 != NULL); ARDUINOJSON_ASSERT(s2 != NULL); for (;;) { char c1 = *s1++; char c2 = static_cast(pgm_read_byte(s2++)); if (c1 < c2) return -1; if (c1 > c2) return 1; if (c1 == 0 /* and c2 as well */) return 0; } } #endif #ifndef memcpy_P inline void* memcpy_P(void* dst, ARDUINOJSON_NAMESPACE::pgm_p src, size_t n) { uint8_t* d = reinterpret_cast(dst); const char* s = src.address; ARDUINOJSON_ASSERT(d != NULL); ARDUINOJSON_ASSERT(s != NULL); while (n-- > 0) { *d++ = pgm_read_byte(s++); } return dst; } #endif namespace ARDUINOJSON_NAMESPACE { class FlashStringAdapter { public: FlashStringAdapter(const __FlashStringHelper* str) : _str(str) {} int compare(const char* other) const { if (!other && !_str) return 0; if (!_str) return -1; if (!other) return 1; return -strcmp_P(other, reinterpret_cast(_str)); } bool equals(const char* expected) const { return compare(expected) == 0; } bool isNull() const { return !_str; } void copyTo(char* p, size_t n) const { memcpy_P(p, reinterpret_cast(_str), n); } size_t size() const { if (!_str) return 0; return strlen_P(reinterpret_cast(_str)); } typedef storage_policies::store_by_copy storage_policy; private: const __FlashStringHelper* _str; }; inline FlashStringAdapter adaptString(const __FlashStringHelper* str) { return FlashStringAdapter(str); } template <> struct IsString : true_type {}; class SizedFlashStringAdapter { public: SizedFlashStringAdapter(const __FlashStringHelper* str, size_t sz) : _str(str), _size(sz) {} int compare(const char* other) const { if (!other && !_str) return 0; if (!_str) return -1; if (!other) return 1; return -strncmp_P(other, reinterpret_cast(_str), _size); } bool equals(const char* expected) const { return compare(expected) == 0; } bool isNull() const { return !_str; } void copyTo(char* p, size_t n) const { memcpy_P(p, reinterpret_cast(_str), n); } size_t size() const { return _size; } typedef storage_policies::store_by_copy storage_policy; private: const __FlashStringHelper* _str; size_t _size; }; inline SizedFlashStringAdapter adaptString(const __FlashStringHelper* str, size_t sz) { return SizedFlashStringAdapter(str, sz); } } // namespace ARDUINOJSON_NAMESPACE #endif namespace ARDUINOJSON_NAMESPACE { template struct int_t; template <> struct int_t<8> { typedef int8_t type; }; template <> struct int_t<16> { typedef int16_t type; }; template <> struct int_t<32> { typedef int32_t type; }; } // namespace ARDUINOJSON_NAMESPACE #ifdef _MSC_VER # pragma warning(push) # pragma warning(disable : 4310) #endif namespace ARDUINOJSON_NAMESPACE { template struct numeric_limits; template struct numeric_limits::value>::type> { static T lowest() { return 0; } static T highest() { return T(-1); } }; template struct numeric_limits< T, typename enable_if::value && is_signed::value>::type> { static T lowest() { return T(T(1) << (sizeof(T) * 8 - 1)); } static T highest() { return T(~lowest()); } }; } // namespace ARDUINOJSON_NAMESPACE #ifdef _MSC_VER # pragma warning(pop) #endif namespace ARDUINOJSON_NAMESPACE { #if ARDUINOJSON_USE_DOUBLE typedef double Float; #else typedef float Float; #endif #if ARDUINOJSON_USE_LONG_LONG typedef int64_t Integer; typedef uint64_t UInt; #else typedef long Integer; typedef unsigned long UInt; #endif } // namespace ARDUINOJSON_NAMESPACE #if ARDUINOJSON_HAS_LONG_LONG && !ARDUINOJSON_USE_LONG_LONG # define ARDUINOJSON_ASSERT_INTEGER_TYPE_IS_SUPPORTED(T) \ static_assert(sizeof(T) <= sizeof(ARDUINOJSON_NAMESPACE::Integer), \ "To use 64-bit integers with ArduinoJson, you must set " \ "ARDUINOJSON_USE_LONG_LONG to 1. See " \ "https://arduinojson.org/v6/api/config/use_long_long/"); #else # define ARDUINOJSON_ASSERT_INTEGER_TYPE_IS_SUPPORTED(T) #endif namespace ARDUINOJSON_NAMESPACE { enum { VALUE_MASK = 0x7F, OWNED_VALUE_BIT = 0x01, VALUE_IS_NULL = 0, VALUE_IS_LINKED_RAW = 0x02, VALUE_IS_OWNED_RAW = 0x03, VALUE_IS_LINKED_STRING = 0x04, VALUE_IS_OWNED_STRING = 0x05, VALUE_IS_BOOLEAN = 0x06, NUMBER_BIT = 0x08, VALUE_IS_UNSIGNED_INTEGER = 0x08, VALUE_IS_SIGNED_INTEGER = 0x0A, VALUE_IS_FLOAT = 0x0C, COLLECTION_MASK = 0x60, VALUE_IS_OBJECT = 0x20, VALUE_IS_ARRAY = 0x40, OWNED_KEY_BIT = 0x80 }; struct RawData { const char *data; size_t size; }; union VariantContent { Float asFloat; bool asBoolean; UInt asUnsignedInteger; Integer asSignedInteger; CollectionData asCollection; const char *asString; struct { const char *data; size_t size; } asRaw; }; typedef int_t::type VariantSlotDiff; class VariantSlot { VariantContent _content; uint8_t _flags; VariantSlotDiff _next; const char* _key; public: VariantData* data() { return reinterpret_cast(&_content); } const VariantData* data() const { return reinterpret_cast(&_content); } VariantSlot* next() { return _next ? this + _next : 0; } const VariantSlot* next() const { return const_cast(this)->next(); } VariantSlot* next(size_t distance) { VariantSlot* slot = this; while (distance--) { if (!slot->_next) return 0; slot += slot->_next; } return slot; } const VariantSlot* next(size_t distance) const { return const_cast(this)->next(distance); } void setNext(VariantSlot* slot) { ARDUINOJSON_ASSERT(!slot || slot - this >= numeric_limits::lowest()); ARDUINOJSON_ASSERT(!slot || slot - this <= numeric_limits::highest()); _next = VariantSlotDiff(slot ? slot - this : 0); } void setNextNotNull(VariantSlot* slot) { ARDUINOJSON_ASSERT(slot != 0); ARDUINOJSON_ASSERT(slot - this >= numeric_limits::lowest()); ARDUINOJSON_ASSERT(slot - this <= numeric_limits::highest()); _next = VariantSlotDiff(slot - this); } void setKey(const char* k, storage_policies::store_by_copy) { ARDUINOJSON_ASSERT(k != NULL); _flags |= OWNED_KEY_BIT; _key = k; } void setKey(const char* k, storage_policies::store_by_address) { ARDUINOJSON_ASSERT(k != NULL); _flags &= VALUE_MASK; _key = k; } const char* key() const { return _key; } bool ownsKey() const { return (_flags & OWNED_KEY_BIT) != 0; } void clear() { _next = 0; _flags = 0; _key = 0; } void movePointers(ptrdiff_t stringDistance, ptrdiff_t variantDistance) { if (_flags & OWNED_KEY_BIT) _key += stringDistance; if (_flags & OWNED_VALUE_BIT) _content.asString += stringDistance; if (_flags & COLLECTION_MASK) _content.asCollection.movePointers(stringDistance, variantDistance); } }; } // namespace ARDUINOJSON_NAMESPACE #define JSON_STRING_SIZE(SIZE) (SIZE + 1) namespace ARDUINOJSON_NAMESPACE { class MemoryPool { public: MemoryPool(char* buf, size_t capa) : _begin(buf), _left(buf), _right(buf ? buf + capa : 0), _end(buf ? buf + capa : 0), _overflowed(false) { ARDUINOJSON_ASSERT(isAligned(_begin)); ARDUINOJSON_ASSERT(isAligned(_right)); ARDUINOJSON_ASSERT(isAligned(_end)); } void* buffer() { return _begin; // NOLINT(clang-analyzer-unix.Malloc) } size_t capacity() const { return size_t(_end - _begin); } size_t size() const { return size_t(_left - _begin + _end - _right); } bool overflowed() const { return _overflowed; } VariantSlot* allocVariant() { return allocRight(); } template const char* saveString(const TAdaptedString& str) { if (str.isNull()) return 0; #if ARDUINOJSON_ENABLE_STRING_DEDUPLICATION const char* existingCopy = findString(str); if (existingCopy) return existingCopy; #endif size_t n = str.size(); char* newCopy = allocString(n + 1); if (newCopy) { str.copyTo(newCopy, n); newCopy[n] = 0; // force null-terminator } return newCopy; } void getFreeZone(char** zoneStart, size_t* zoneSize) const { *zoneStart = _left; *zoneSize = size_t(_right - _left); } const char* saveStringFromFreeZone(size_t len) { #if ARDUINOJSON_ENABLE_STRING_DEDUPLICATION const char* dup = findString(adaptString(_left)); if (dup) return dup; #endif const char* str = _left; _left += len; checkInvariants(); return str; } void markAsOverflowed() { _overflowed = true; } void clear() { _left = _begin; _right = _end; _overflowed = false; } bool canAlloc(size_t bytes) const { return _left + bytes <= _right; } bool owns(void* p) const { return _begin <= p && p < _end; } void* operator new(size_t, void* p) { return p; } ptrdiff_t squash() { char* new_right = addPadding(_left); if (new_right >= _right) return 0; size_t right_size = static_cast(_end - _right); memmove(new_right, _right, right_size); ptrdiff_t bytes_reclaimed = _right - new_right; _right = new_right; _end = new_right + right_size; return bytes_reclaimed; } void movePointers(ptrdiff_t offset) { _begin += offset; _left += offset; _right += offset; _end += offset; } private: void checkInvariants() { ARDUINOJSON_ASSERT(_begin <= _left); ARDUINOJSON_ASSERT(_left <= _right); ARDUINOJSON_ASSERT(_right <= _end); ARDUINOJSON_ASSERT(isAligned(_right)); } #if ARDUINOJSON_ENABLE_STRING_DEDUPLICATION template const char* findString(const TAdaptedString& str) { for (char* next = _begin; next < _left; ++next) { if (str.equals(next)) return next; while (*next) ++next; } return 0; } #endif char* allocString(size_t n) { if (!canAlloc(n)) { _overflowed = true; return 0; } char* s = _left; _left += n; checkInvariants(); return s; } template T* allocRight() { return reinterpret_cast(allocRight(sizeof(T))); } void* allocRight(size_t bytes) { if (!canAlloc(bytes)) { _overflowed = true; return 0; } _right -= bytes; return _right; } char *_begin, *_left, *_right, *_end; bool _overflowed; }; template class SerializedValue { public: explicit SerializedValue(T str) : _str(str) {} operator T() const { return _str; } const char* data() const { return _str.c_str(); } size_t size() const { return _str.length(); } private: T _str; }; template class SerializedValue { public: explicit SerializedValue(TChar* p, size_t n) : _data(p), _size(n) {} operator TChar*() const { return _data; } TChar* data() const { return _data; } size_t size() const { return _size; } private: TChar* _data; size_t _size; }; template inline SerializedValue serialized(T str) { return SerializedValue(str); } template inline SerializedValue serialized(TChar* p) { return SerializedValue(p, adaptString(p).size()); } template inline SerializedValue serialized(TChar* p, size_t n) { return SerializedValue(p, n); } } // namespace ARDUINOJSON_NAMESPACE #if defined(__clang__) # pragma clang diagnostic push # pragma clang diagnostic ignored "-Wconversion" #elif defined(__GNUC__) # if __GNUC__ > 4 || (__GNUC__ == 4 && __GNUC_MINOR__ >= 6) # pragma GCC diagnostic push # endif # pragma GCC diagnostic ignored "-Wconversion" #endif namespace ARDUINOJSON_NAMESPACE { template typename enable_if::value && is_unsigned::value && is_integral::value && sizeof(TOut) <= sizeof(TIn), bool>::type canConvertNumber(TIn value) { return value <= TIn(numeric_limits::highest()); } template typename enable_if::value && is_unsigned::value && is_integral::value && sizeof(TIn) < sizeof(TOut), bool>::type canConvertNumber(TIn) { return true; } template typename enable_if::value && is_floating_point::value, bool>::type canConvertNumber(TIn) { return true; } template typename enable_if::value && is_signed::value && is_integral::value && is_signed::value && sizeof(TOut) < sizeof(TIn), bool>::type canConvertNumber(TIn value) { return value >= TIn(numeric_limits::lowest()) && value <= TIn(numeric_limits::highest()); } template typename enable_if::value && is_signed::value && is_integral::value && is_signed::value && sizeof(TIn) <= sizeof(TOut), bool>::type canConvertNumber(TIn) { return true; } template typename enable_if::value && is_signed::value && is_integral::value && is_unsigned::value && sizeof(TOut) >= sizeof(TIn), bool>::type canConvertNumber(TIn value) { if (value < 0) return false; return TOut(value) <= numeric_limits::highest(); } template typename enable_if::value && is_signed::value && is_integral::value && is_unsigned::value && sizeof(TOut) < sizeof(TIn), bool>::type canConvertNumber(TIn value) { if (value < 0) return false; return value <= TIn(numeric_limits::highest()); } template typename enable_if::value && !is_floating_point::value, bool>::type canConvertNumber(TIn value) { return value >= numeric_limits::lowest() && value <= numeric_limits::highest(); } template TOut convertNumber(TIn value) { return canConvertNumber(value) ? TOut(value) : 0; } } // namespace ARDUINOJSON_NAMESPACE #if defined(__clang__) # pragma clang diagnostic pop #elif defined(__GNUC__) # if __GNUC__ > 4 || (__GNUC__ == 4 && __GNUC_MINOR__ >= 6) # pragma GCC diagnostic pop # endif #endif #if defined(__GNUC__) # if __GNUC__ >= 7 # pragma GCC diagnostic push # pragma GCC diagnostic ignored "-Wmaybe-uninitialized" # pragma GCC diagnostic ignored "-Wuninitialized" # endif #endif namespace ARDUINOJSON_NAMESPACE { class VariantData { VariantContent _content; // must be first to allow cast from array to variant uint8_t _flags; public: void init() { _flags = VALUE_IS_NULL; } template typename TVisitor::result_type accept(TVisitor &visitor) const { switch (type()) { case VALUE_IS_FLOAT: return visitor.visitFloat(_content.asFloat); case VALUE_IS_ARRAY: return visitor.visitArray(_content.asCollection); case VALUE_IS_OBJECT: return visitor.visitObject(_content.asCollection); case VALUE_IS_LINKED_STRING: case VALUE_IS_OWNED_STRING: return visitor.visitString(_content.asString); case VALUE_IS_OWNED_RAW: case VALUE_IS_LINKED_RAW: return visitor.visitRawJson(_content.asRaw.data, _content.asRaw.size); case VALUE_IS_SIGNED_INTEGER: return visitor.visitSignedInteger(_content.asSignedInteger); case VALUE_IS_UNSIGNED_INTEGER: return visitor.visitUnsignedInteger(_content.asUnsignedInteger); case VALUE_IS_BOOLEAN: return visitor.visitBoolean(_content.asBoolean != 0); default: return visitor.visitNull(); } } template T asIntegral() const; template T asFloat() const; const char *asString() const; bool asBoolean() const; CollectionData *asArray() { return isArray() ? &_content.asCollection : 0; } const CollectionData *asArray() const { return const_cast(this)->asArray(); } CollectionData *asObject() { return isObject() ? &_content.asCollection : 0; } const CollectionData *asObject() const { return const_cast(this)->asObject(); } bool copyFrom(const VariantData &src, MemoryPool *pool) { switch (src.type()) { case VALUE_IS_ARRAY: return toArray().copyFrom(src._content.asCollection, pool); case VALUE_IS_OBJECT: return toObject().copyFrom(src._content.asCollection, pool); case VALUE_IS_OWNED_STRING: return setString(RamStringAdapter(src._content.asString), pool); case VALUE_IS_OWNED_RAW: return setOwnedRaw( serialized(src._content.asRaw.data, src._content.asRaw.size), pool); default: setType(src.type()); _content = src._content; return true; } } bool isArray() const { return (_flags & VALUE_IS_ARRAY) != 0; } bool isBoolean() const { return type() == VALUE_IS_BOOLEAN; } bool isCollection() const { return (_flags & COLLECTION_MASK) != 0; } template bool isInteger() const { switch (type()) { case VALUE_IS_UNSIGNED_INTEGER: return canConvertNumber(_content.asUnsignedInteger); case VALUE_IS_SIGNED_INTEGER: return canConvertNumber(_content.asSignedInteger); default: return false; } } bool isFloat() const { return (_flags & NUMBER_BIT) != 0; } bool isString() const { return type() == VALUE_IS_LINKED_STRING || type() == VALUE_IS_OWNED_STRING; } bool isObject() const { return (_flags & VALUE_IS_OBJECT) != 0; } bool isNull() const { return type() == VALUE_IS_NULL; } bool isEnclosed() const { return !isFloat(); } void remove(size_t index) { if (isArray()) _content.asCollection.removeElement(index); } template void remove(TAdaptedString key) { if (isObject()) _content.asCollection.removeMember(key); } void setBoolean(bool value) { setType(VALUE_IS_BOOLEAN); _content.asBoolean = value; } void setFloat(Float value) { setType(VALUE_IS_FLOAT); _content.asFloat = value; } void setLinkedRaw(SerializedValue value) { if (value.data()) { setType(VALUE_IS_LINKED_RAW); _content.asRaw.data = value.data(); _content.asRaw.size = value.size(); } else { setType(VALUE_IS_NULL); } } template bool setOwnedRaw(SerializedValue value, MemoryPool *pool) { const char *dup = pool->saveString(adaptString(value.data(), value.size())); if (dup) { setType(VALUE_IS_OWNED_RAW); _content.asRaw.data = dup; _content.asRaw.size = value.size(); return true; } else { setType(VALUE_IS_NULL); return false; } } template typename enable_if::value>::type setInteger(T value) { setType(VALUE_IS_UNSIGNED_INTEGER); _content.asUnsignedInteger = static_cast(value); } template typename enable_if::value>::type setInteger(T value) { setType(VALUE_IS_SIGNED_INTEGER); _content.asSignedInteger = value; } void setNull() { setType(VALUE_IS_NULL); } void setStringPointer(const char *s, storage_policies::store_by_copy) { ARDUINOJSON_ASSERT(s != 0); setType(VALUE_IS_OWNED_STRING); _content.asString = s; } void setStringPointer(const char *s, storage_policies::store_by_address) { ARDUINOJSON_ASSERT(s != 0); setType(VALUE_IS_LINKED_STRING); _content.asString = s; } template bool setString(TAdaptedString value, MemoryPool *pool) { return storeString(value, pool, typename TAdaptedString::storage_policy()); } CollectionData &toArray() { setType(VALUE_IS_ARRAY); _content.asCollection.clear(); return _content.asCollection; } CollectionData &toObject() { setType(VALUE_IS_OBJECT); _content.asCollection.clear(); return _content.asCollection; } size_t memoryUsage() const { switch (type()) { case VALUE_IS_OWNED_STRING: return strlen(_content.asString) + 1; case VALUE_IS_OWNED_RAW: return _content.asRaw.size; case VALUE_IS_OBJECT: case VALUE_IS_ARRAY: return _content.asCollection.memoryUsage(); default: return 0; } } size_t nesting() const { return isCollection() ? _content.asCollection.nesting() : 0; } size_t size() const { return isCollection() ? _content.asCollection.size() : 0; } VariantData *addElement(MemoryPool *pool) { if (isNull()) toArray(); if (!isArray()) return 0; return _content.asCollection.addElement(pool); } VariantData *getElement(size_t index) const { return isArray() ? _content.asCollection.getElement(index) : 0; } VariantData *getOrAddElement(size_t index, MemoryPool *pool) { if (isNull()) toArray(); if (!isArray()) return 0; return _content.asCollection.getOrAddElement(index, pool); } template VariantData *getMember(TAdaptedString key) const { return isObject() ? _content.asCollection.getMember(key) : 0; } template VariantData *getOrAddMember(TAdaptedString key, MemoryPool *pool) { if (isNull()) toObject(); if (!isObject()) return 0; return _content.asCollection.getOrAddMember(key, pool); } void movePointers(ptrdiff_t stringDistance, ptrdiff_t variantDistance) { if (_flags & OWNED_VALUE_BIT) _content.asString += stringDistance; if (_flags & COLLECTION_MASK) _content.asCollection.movePointers(stringDistance, variantDistance); } uint8_t type() const { return _flags & VALUE_MASK; } private: void setType(uint8_t t) { _flags &= OWNED_KEY_BIT; _flags |= t; } template inline bool storeString(TAdaptedString value, MemoryPool *pool, storage_policies::decide_at_runtime) { if (value.isStatic()) return storeString(value, pool, storage_policies::store_by_address()); else return storeString(value, pool, storage_policies::store_by_copy()); } template inline bool storeString(TAdaptedString value, MemoryPool *, storage_policies::store_by_address) { if (value.isNull()) setNull(); else setStringPointer(value.data(), storage_policies::store_by_address()); return true; } template inline bool storeString(TAdaptedString value, MemoryPool *pool, storage_policies::store_by_copy) { if (value.isNull()) { setNull(); return true; } const char *copy = pool->saveString(value); if (!copy) { setNull(); return false; } setStringPointer(copy, storage_policies::store_by_copy()); return true; } }; } // namespace ARDUINOJSON_NAMESPACE #if defined(__GNUC__) # if __GNUC__ >= 8 # pragma GCC diagnostic pop # endif #endif namespace ARDUINOJSON_NAMESPACE { template inline bool slotSetKey(VariantSlot* var, TAdaptedString key, MemoryPool* pool) { if (!var) return false; return slotSetKey(var, key, pool, typename TAdaptedString::storage_policy()); } template inline bool slotSetKey(VariantSlot* var, TAdaptedString key, MemoryPool* pool, storage_policies::decide_at_runtime) { if (key.isStatic()) { return slotSetKey(var, key, pool, storage_policies::store_by_address()); } else { return slotSetKey(var, key, pool, storage_policies::store_by_copy()); } } template inline bool slotSetKey(VariantSlot* var, TAdaptedString key, MemoryPool*, storage_policies::store_by_address) { ARDUINOJSON_ASSERT(var); var->setKey(key.data(), storage_policies::store_by_address()); return true; } template inline bool slotSetKey(VariantSlot* var, TAdaptedString key, MemoryPool* pool, storage_policies::store_by_copy) { const char* dup = pool->saveString(key); if (!dup) return false; ARDUINOJSON_ASSERT(var); var->setKey(dup, storage_policies::store_by_copy()); return true; } inline size_t slotSize(const VariantSlot* var) { size_t n = 0; while (var) { n++; var = var->next(); } return n; } inline VariantData* slotData(VariantSlot* slot) { return reinterpret_cast(slot); } struct Visitable { }; template struct IsVisitable : is_base_of {}; template struct IsVisitable : IsVisitable {}; template struct Converter; template class InvalidConversion; // Error here? See https://arduinojson.org/v6/invalid-conversion/ } // namespace ARDUINOJSON_NAMESPACE #ifdef _MSC_VER // Visual Studio # define FORCE_INLINE // __forceinline causes C4714 when returning std::string # define NO_INLINE __declspec(noinline) # ifndef ARDUINOJSON_DEPRECATED # define ARDUINOJSON_DEPRECATED(msg) __declspec(deprecated(msg)) # endif #elif defined(__GNUC__) // GCC or Clang # define FORCE_INLINE __attribute__((always_inline)) # define NO_INLINE __attribute__((noinline)) # ifndef ARDUINOJSON_DEPRECATED # if __GNUC__ > 4 || (__GNUC__ == 4 && __GNUC_MINOR__ >= 5) # define ARDUINOJSON_DEPRECATED(msg) __attribute__((deprecated(msg))) # else # define ARDUINOJSON_DEPRECATED(msg) __attribute__((deprecated)) # endif # endif #else // Other compilers # define FORCE_INLINE # define NO_INLINE # ifndef ARDUINOJSON_DEPRECATED # define ARDUINOJSON_DEPRECATED(msg) # endif #endif #if __cplusplus >= 201103L # define NOEXCEPT noexcept #else # define NOEXCEPT throw() #endif #if defined(__has_attribute) # if __has_attribute(no_sanitize) # define ARDUINOJSON_NO_SANITIZE(check) __attribute__((no_sanitize(check))) # else # define ARDUINOJSON_NO_SANITIZE(check) # endif #else # define ARDUINOJSON_NO_SANITIZE(check) #endif namespace ARDUINOJSON_NAMESPACE { template inline typename TVisitor::result_type variantAccept(const VariantData *var, TVisitor &visitor) { if (var != 0) return var->accept(visitor); else return visitor.visitNull(); } inline const CollectionData *variantAsArray(const VariantData *var) { return var != 0 ? var->asArray() : 0; } inline const CollectionData *variantAsObject(const VariantData *var) { return var != 0 ? var->asObject() : 0; } inline CollectionData *variantAsObject(VariantData *var) { return var != 0 ? var->asObject() : 0; } inline bool variantCopyFrom(VariantData *dst, const VariantData *src, MemoryPool *pool) { if (!dst) return false; if (!src) { dst->setNull(); return true; } return dst->copyFrom(*src, pool); } inline int variantCompare(const VariantData *a, const VariantData *b); inline void variantSetNull(VariantData *var) { if (!var) return; var->setNull(); } template inline bool variantSetString(VariantData *var, TAdaptedString value, MemoryPool *pool) { if (!var) return false; return var->setString(value, pool); } inline size_t variantSize(const VariantData *var) { return var != 0 ? var->size() : 0; } inline CollectionData *variantToArray(VariantData *var) { if (!var) return 0; return &var->toArray(); } inline CollectionData *variantToObject(VariantData *var) { if (!var) return 0; return &var->toObject(); } inline NO_INLINE VariantData *variantAddElement(VariantData *var, MemoryPool *pool) { return var != 0 ? var->addElement(pool) : 0; } inline NO_INLINE VariantData *variantGetOrAddElement(VariantData *var, size_t index, MemoryPool *pool) { return var != 0 ? var->getOrAddElement(index, pool) : 0; } template NO_INLINE VariantData *variantGetOrAddMember(VariantData *var, TChar *key, MemoryPool *pool) { return var != 0 ? var->getOrAddMember(adaptString(key), pool) : 0; } template NO_INLINE VariantData *variantGetOrAddMember(VariantData *var, const TString &key, MemoryPool *pool) { return var != 0 ? var->getOrAddMember(adaptString(key), pool) : 0; } inline bool variantIsNull(const VariantData *var) { return var == 0 || var->isNull(); } enum CompareResult { COMPARE_RESULT_DIFFER = 0, COMPARE_RESULT_EQUAL = 1, COMPARE_RESULT_GREATER = 2, COMPARE_RESULT_LESS = 4, COMPARE_RESULT_GREATER_OR_EQUAL = 3, COMPARE_RESULT_LESS_OR_EQUAL = 5 }; template CompareResult arithmeticCompare(const T &lhs, const T &rhs) { if (lhs < rhs) return COMPARE_RESULT_LESS; else if (lhs > rhs) return COMPARE_RESULT_GREATER; else return COMPARE_RESULT_EQUAL; } template CompareResult arithmeticCompare( const T1 &lhs, const T2 &rhs, typename enable_if::value && is_integral::value && sizeof(T1) < sizeof(T2), int // Using int instead of void to avoid C2572 on >::type * = 0) { return arithmeticCompare(static_cast(lhs), rhs); } template CompareResult arithmeticCompare( const T1 &lhs, const T2 &rhs, typename enable_if::value && is_integral::value && sizeof(T2) < sizeof(T1)>::type * = 0) { return arithmeticCompare(lhs, static_cast(rhs)); } template CompareResult arithmeticCompare( const T1 &lhs, const T2 &rhs, typename enable_if::value && is_integral::value && is_signed::value == is_signed::value && sizeof(T2) == sizeof(T1)>::type * = 0) { return arithmeticCompare(lhs, static_cast(rhs)); } template CompareResult arithmeticCompare( const T1 &lhs, const T2 &rhs, typename enable_if::value && is_integral::value && is_unsigned::value && is_signed::value && sizeof(T2) == sizeof(T1)>::type * = 0) { if (rhs < 0) return COMPARE_RESULT_GREATER; return arithmeticCompare(lhs, static_cast(rhs)); } template CompareResult arithmeticCompare( const T1 &lhs, const T2 &rhs, typename enable_if::value && is_integral::value && is_signed::value && is_unsigned::value && sizeof(T2) == sizeof(T1)>::type * = 0) { if (lhs < 0) return COMPARE_RESULT_LESS; return arithmeticCompare(static_cast(lhs), rhs); } template CompareResult arithmeticCompare( const T1 &lhs, const T2 &rhs, typename enable_if::value || is_floating_point::value>::type * = 0) { return arithmeticCompare(static_cast(lhs), static_cast(rhs)); } template CompareResult arithmeticCompareNegateLeft( UInt, const T2 &, typename enable_if::value>::type * = 0) { return COMPARE_RESULT_LESS; } template CompareResult arithmeticCompareNegateLeft( UInt lhs, const T2 &rhs, typename enable_if::value>::type * = 0) { if (rhs > 0) return COMPARE_RESULT_LESS; return arithmeticCompare(-rhs, static_cast(lhs)); } template CompareResult arithmeticCompareNegateRight( const T1 &, UInt, typename enable_if::value>::type * = 0) { return COMPARE_RESULT_GREATER; } template CompareResult arithmeticCompareNegateRight( const T1 &lhs, UInt rhs, typename enable_if::value>::type * = 0) { if (lhs > 0) return COMPARE_RESULT_GREATER; return arithmeticCompare(static_cast(rhs), -lhs); } struct VariantTag {}; template struct IsVariant : is_base_of {}; template CompareResult compare(const T1 &lhs, const T2 &rhs); // VariantCompare.cpp template struct VariantOperators { template friend typename enable_if::value && !is_array::value, T>::type operator|(const TVariant &variant, const T &defaultValue) { if (variant.template is()) return variant.template as(); else return defaultValue; } friend const char *operator|(const TVariant &variant, const char *defaultValue) { if (variant.template is()) return variant.template as(); else return defaultValue; } template friend typename enable_if::value, typename T::variant_type>::type operator|(const TVariant &variant, T defaultValue) { if (variant) return variant; else return defaultValue; } template friend bool operator==(T *lhs, TVariant rhs) { return compare(rhs, lhs) == COMPARE_RESULT_EQUAL; } template friend bool operator==(const T &lhs, TVariant rhs) { return compare(rhs, lhs) == COMPARE_RESULT_EQUAL; } template friend bool operator==(TVariant lhs, T *rhs) { return compare(lhs, rhs) == COMPARE_RESULT_EQUAL; } template friend typename enable_if::value, bool>::type operator==( TVariant lhs, const T &rhs) { return compare(lhs, rhs) == COMPARE_RESULT_EQUAL; } template friend bool operator!=(T *lhs, TVariant rhs) { return compare(rhs, lhs) != COMPARE_RESULT_EQUAL; } template friend bool operator!=(const T &lhs, TVariant rhs) { return compare(rhs, lhs) != COMPARE_RESULT_EQUAL; } template friend bool operator!=(TVariant lhs, T *rhs) { return compare(lhs, rhs) != COMPARE_RESULT_EQUAL; } template friend typename enable_if::value, bool>::type operator!=( TVariant lhs, const T &rhs) { return compare(lhs, rhs) != COMPARE_RESULT_EQUAL; } template friend bool operator<(T *lhs, TVariant rhs) { return compare(rhs, lhs) == COMPARE_RESULT_GREATER; } template friend bool operator<(const T &lhs, TVariant rhs) { return compare(rhs, lhs) == COMPARE_RESULT_GREATER; } template friend bool operator<(TVariant lhs, T *rhs) { return compare(lhs, rhs) == COMPARE_RESULT_LESS; } template friend typename enable_if::value, bool>::type operator<( TVariant lhs, const T &rhs) { return compare(lhs, rhs) == COMPARE_RESULT_LESS; } template friend bool operator<=(T *lhs, TVariant rhs) { return (compare(rhs, lhs) & COMPARE_RESULT_GREATER_OR_EQUAL) != 0; } template friend bool operator<=(const T &lhs, TVariant rhs) { return (compare(rhs, lhs) & COMPARE_RESULT_GREATER_OR_EQUAL) != 0; } template friend bool operator<=(TVariant lhs, T *rhs) { return (compare(lhs, rhs) & COMPARE_RESULT_LESS_OR_EQUAL) != 0; } template friend typename enable_if::value, bool>::type operator<=( TVariant lhs, const T &rhs) { return (compare(lhs, rhs) & COMPARE_RESULT_LESS_OR_EQUAL) != 0; } template friend bool operator>(T *lhs, TVariant rhs) { return compare(rhs, lhs) == COMPARE_RESULT_LESS; } template friend bool operator>(const T &lhs, TVariant rhs) { return compare(rhs, lhs) == COMPARE_RESULT_LESS; } template friend bool operator>(TVariant lhs, T *rhs) { return compare(lhs, rhs) == COMPARE_RESULT_GREATER; } template friend typename enable_if::value, bool>::type operator>( TVariant lhs, const T &rhs) { return compare(lhs, rhs) == COMPARE_RESULT_GREATER; } template friend bool operator>=(T *lhs, TVariant rhs) { return (compare(rhs, lhs) & COMPARE_RESULT_LESS_OR_EQUAL) != 0; } template friend bool operator>=(const T &lhs, TVariant rhs) { return (compare(rhs, lhs) & COMPARE_RESULT_LESS_OR_EQUAL) != 0; } template friend bool operator>=(TVariant lhs, T *rhs) { return (compare(lhs, rhs) & COMPARE_RESULT_GREATER_OR_EQUAL) != 0; } template friend typename enable_if::value, bool>::type operator>=( TVariant lhs, const T &rhs) { return (compare(lhs, rhs) & COMPARE_RESULT_GREATER_OR_EQUAL) != 0; } }; class ArrayRef; class ObjectRef; template class ElementProxy; template class ArrayShortcuts { public: FORCE_INLINE ElementProxy operator[](size_t index) const; FORCE_INLINE ObjectRef createNestedObject() const; FORCE_INLINE ArrayRef createNestedArray() const; template FORCE_INLINE bool add(const T &value) const { return impl()->addElement().set(value); } template FORCE_INLINE bool add(T *value) const { return impl()->addElement().set(value); } private: const TArray *impl() const { return static_cast(this); } }; template class MemberProxy; template class ObjectShortcuts { public: template FORCE_INLINE typename enable_if::value, bool>::type containsKey(const TString &key) const; template FORCE_INLINE typename enable_if::value, bool>::type containsKey(TChar *key) const; template FORCE_INLINE typename enable_if::value, MemberProxy >::type operator[](const TString &key) const; template FORCE_INLINE typename enable_if::value, MemberProxy >::type operator[](TChar *key) const; template FORCE_INLINE ArrayRef createNestedArray(const TString &key) const; template FORCE_INLINE ArrayRef createNestedArray(TChar *key) const; template ObjectRef createNestedObject(const TString &key) const; template ObjectRef createNestedObject(TChar *key) const; private: const TObject *impl() const { return static_cast(this); } }; template class VariantShortcuts : public ObjectShortcuts, public ArrayShortcuts { public: using ArrayShortcuts::createNestedArray; using ArrayShortcuts::createNestedObject; using ArrayShortcuts::operator[]; using ObjectShortcuts::createNestedArray; using ObjectShortcuts::createNestedObject; using ObjectShortcuts::operator[]; }; class ArrayRef; class ObjectRef; template class VariantRefBase : public VariantTag { public: FORCE_INLINE bool isNull() const { return variantIsNull(_data); } FORCE_INLINE bool isUndefined() const { return !_data; } FORCE_INLINE size_t memoryUsage() const { return _data ? _data->memoryUsage() : 0; } FORCE_INLINE size_t nesting() const { return _data ? _data->nesting() : 0; } size_t size() const { return variantSize(_data); } protected: VariantRefBase(TData *data) : _data(data) {} TData *_data; friend TData *getData(const VariantRefBase &variant) { return variant._data; } }; class VariantRef : public VariantRefBase, public VariantOperators, public VariantShortcuts, public Visitable { typedef VariantRefBase base_type; friend class VariantConstRef; public: FORCE_INLINE VariantRef(MemoryPool *pool, VariantData *data) : base_type(data), _pool(pool) {} FORCE_INLINE VariantRef() : base_type(0), _pool(0) {} FORCE_INLINE void clear() const { return variantSetNull(_data); } template FORCE_INLINE bool set(const T &value) const { return Converter::toJson(value, *this); } bool ARDUINOJSON_DEPRECATED( "Support for char is deprecated, use int8_t or uint8_t instead") set(char value) const; template FORCE_INLINE bool set(T *value) const { return Converter::toJson(value, *this); } template FORCE_INLINE typename enable_if::value && !is_same::value, T>::type as() const { return Converter::fromJson(*this); } template FORCE_INLINE typename enable_if::value, const char *>::type ARDUINOJSON_DEPRECATED("Replace as() with as()") as() const { return as(); } template FORCE_INLINE typename enable_if::value, char>::type ARDUINOJSON_DEPRECATED( "Support for char is deprecated, use int8_t or uint8_t instead") as() const { return as(); } template FORCE_INLINE typename enable_if::value && !is_same::value, bool>::type is() const { return Converter::checkJson(*this); } template FORCE_INLINE typename enable_if::value, bool>::type ARDUINOJSON_DEPRECATED("Replace is() with is()") is() const { return is(); } template FORCE_INLINE typename enable_if::value, bool>::type ARDUINOJSON_DEPRECATED( "Support for char is deprecated, use int8_t or uint8_t instead") is() const { return is(); } template FORCE_INLINE operator T() const { return as(); } template typename TVisitor::result_type accept(TVisitor &visitor) const { return variantAccept(_data, visitor); } template typename enable_if::value, ArrayRef>::type to() const; template typename enable_if::value, ObjectRef>::type to() const; template typename enable_if::value, VariantRef>::type to() const; VariantRef addElement() const; FORCE_INLINE VariantRef getElement(size_t) const; FORCE_INLINE VariantRef getOrAddElement(size_t) const; template FORCE_INLINE VariantRef getMember(TChar *) const; template FORCE_INLINE typename enable_if::value, VariantRef>::type getMember(const TString &) const; template FORCE_INLINE VariantRef getOrAddMember(TChar *) const; template FORCE_INLINE VariantRef getOrAddMember(const TString &) const; FORCE_INLINE void remove(size_t index) const { if (_data) _data->remove(index); } template FORCE_INLINE typename enable_if::value>::type remove( TChar *key) const { if (_data) _data->remove(adaptString(key)); } template FORCE_INLINE typename enable_if::value>::type remove( const TString &key) const { if (_data) _data->remove(adaptString(key)); } private: MemoryPool *_pool; friend MemoryPool *getPool(const VariantRef &variant) { return variant._pool; } }; class VariantConstRef : public VariantRefBase, public VariantOperators, public VariantShortcuts, public Visitable { typedef VariantRefBase base_type; friend class VariantRef; public: VariantConstRef() : base_type(0) {} VariantConstRef(const VariantData *data) : base_type(data) {} VariantConstRef(VariantRef var) : base_type(var._data) {} template typename TVisitor::result_type accept(TVisitor &visitor) const { return variantAccept(_data, visitor); } template FORCE_INLINE typename enable_if::value && !is_same::value, T>::type as() const { return Converter::fromJson(*this); } template FORCE_INLINE typename enable_if::value, const char *>::type ARDUINOJSON_DEPRECATED("Replace as() with as()") as() const { return as(); } template FORCE_INLINE typename enable_if::value, char>::type ARDUINOJSON_DEPRECATED( "Support for char is deprecated, use int8_t or uint8_t instead") as() const { return as(); } template FORCE_INLINE typename enable_if::value && !is_same::value, bool>::type is() const { return Converter::checkJson(*this); } template FORCE_INLINE typename enable_if::value, bool>::type ARDUINOJSON_DEPRECATED("Replace is() with is()") is() const { return is(); } template FORCE_INLINE typename enable_if::value, bool>::type ARDUINOJSON_DEPRECATED( "Support for char is deprecated, use int8_t or uint8_t instead") is() const { return is(); } template FORCE_INLINE operator T() const { return as(); } FORCE_INLINE VariantConstRef getElement(size_t) const; FORCE_INLINE VariantConstRef operator[](size_t index) const { return getElement(index); } template FORCE_INLINE VariantConstRef getMember(const TString &key) const { return VariantConstRef( objectGetMember(variantAsObject(_data), adaptString(key))); } template FORCE_INLINE VariantConstRef getMember(TChar *key) const { const CollectionData *obj = variantAsObject(_data); return VariantConstRef(obj ? obj->getMember(adaptString(key)) : 0); } template FORCE_INLINE typename enable_if::value, VariantConstRef>::type operator[](const TString &key) const { return getMember(key); } template FORCE_INLINE typename enable_if::value, VariantConstRef>::type operator[](TChar *key) const { return getMember(key); } }; template <> struct Converter { static bool toJson(VariantRef src, VariantRef dst) { return variantCopyFrom(getData(dst), getData(src), getPool(dst)); } static VariantRef fromJson(VariantRef src) { return src; } static InvalidConversion fromJson( VariantConstRef); static bool checkJson(VariantRef src) { VariantData *data = getData(src); return !!data; } static bool checkJson(VariantConstRef) { return false; } }; template <> struct Converter { static bool toJson(VariantConstRef src, VariantRef dst) { return variantCopyFrom(getData(dst), getData(src), getPool(dst)); } static VariantConstRef fromJson(VariantConstRef src) { return VariantConstRef(getData(src)); } static bool checkJson(VariantConstRef src) { const VariantData *data = getData(src); return !!data; } }; class VariantPtr { public: VariantPtr(MemoryPool *pool, VariantData *data) : _variant(pool, data) {} VariantRef *operator->() { return &_variant; } VariantRef &operator*() { return _variant; } private: VariantRef _variant; }; class ArrayIterator { public: ArrayIterator() : _slot(0) {} explicit ArrayIterator(MemoryPool *pool, VariantSlot *slot) : _pool(pool), _slot(slot) {} VariantRef operator*() const { return VariantRef(_pool, _slot->data()); } VariantPtr operator->() { return VariantPtr(_pool, _slot->data()); } bool operator==(const ArrayIterator &other) const { return _slot == other._slot; } bool operator!=(const ArrayIterator &other) const { return _slot != other._slot; } ArrayIterator &operator++() { _slot = _slot->next(); return *this; } ArrayIterator &operator+=(size_t distance) { _slot = _slot->next(distance); return *this; } VariantSlot *internal() { return _slot; } private: MemoryPool *_pool; VariantSlot *_slot; }; class VariantConstPtr { public: VariantConstPtr(const VariantData *data) : _variant(data) {} VariantConstRef *operator->() { return &_variant; } VariantConstRef &operator*() { return _variant; } private: VariantConstRef _variant; }; class ArrayConstRefIterator { public: ArrayConstRefIterator() : _slot(0) {} explicit ArrayConstRefIterator(const VariantSlot *slot) : _slot(slot) {} VariantConstRef operator*() const { return VariantConstRef(_slot->data()); } VariantConstPtr operator->() { return VariantConstPtr(_slot->data()); } bool operator==(const ArrayConstRefIterator &other) const { return _slot == other._slot; } bool operator!=(const ArrayConstRefIterator &other) const { return _slot != other._slot; } ArrayConstRefIterator &operator++() { _slot = _slot->next(); return *this; } ArrayConstRefIterator &operator+=(size_t distance) { _slot = _slot->next(distance); return *this; } const VariantSlot *internal() { return _slot; } private: const VariantSlot *_slot; }; } // namespace ARDUINOJSON_NAMESPACE #define JSON_ARRAY_SIZE(NUMBER_OF_ELEMENTS) \ ((NUMBER_OF_ELEMENTS) * sizeof(ARDUINOJSON_NAMESPACE::VariantSlot)) namespace ARDUINOJSON_NAMESPACE { class ObjectRef; template class ElementProxy; template class ArrayRefBase { public: operator VariantConstRef() const { const void* data = _data; // prevent warning cast-align return VariantConstRef(reinterpret_cast(data)); } template FORCE_INLINE typename TVisitor::result_type accept(TVisitor& visitor) const { return arrayAccept(_data, visitor); } FORCE_INLINE bool isNull() const { return _data == 0; } FORCE_INLINE operator bool() const { return _data != 0; } FORCE_INLINE size_t memoryUsage() const { return _data ? _data->memoryUsage() : 0; } FORCE_INLINE size_t nesting() const { return _data ? _data->nesting() : 0; } FORCE_INLINE size_t size() const { return _data ? _data->size() : 0; } protected: ArrayRefBase(TData* data) : _data(data) {} TData* _data; }; class ArrayConstRef : public ArrayRefBase, public Visitable { friend class ArrayRef; typedef ArrayRefBase base_type; public: typedef ArrayConstRefIterator iterator; FORCE_INLINE iterator begin() const { if (!_data) return iterator(); return iterator(_data->head()); } FORCE_INLINE iterator end() const { return iterator(); } FORCE_INLINE ArrayConstRef() : base_type(0) {} FORCE_INLINE ArrayConstRef(const CollectionData* data) : base_type(data) {} FORCE_INLINE bool operator==(ArrayConstRef rhs) const { return arrayEquals(_data, rhs._data); } FORCE_INLINE VariantConstRef operator[](size_t index) const { return getElement(index); } FORCE_INLINE VariantConstRef getElement(size_t index) const { return VariantConstRef(_data ? _data->getElement(index) : 0); } }; class ArrayRef : public ArrayRefBase, public ArrayShortcuts, public Visitable { typedef ArrayRefBase base_type; public: typedef ArrayIterator iterator; FORCE_INLINE ArrayRef() : base_type(0), _pool(0) {} FORCE_INLINE ArrayRef(MemoryPool* pool, CollectionData* data) : base_type(data), _pool(pool) {} operator VariantRef() { void* data = _data; // prevent warning cast-align return VariantRef(_pool, reinterpret_cast(data)); } operator ArrayConstRef() const { return ArrayConstRef(_data); } VariantRef addElement() const { return VariantRef(_pool, arrayAdd(_data, _pool)); } FORCE_INLINE iterator begin() const { if (!_data) return iterator(); return iterator(_pool, _data->head()); } FORCE_INLINE iterator end() const { return iterator(); } FORCE_INLINE bool set(ArrayConstRef src) const { if (!_data || !src._data) return false; return _data->copyFrom(*src._data, _pool); } FORCE_INLINE bool operator==(ArrayRef rhs) const { return arrayEquals(_data, rhs._data); } FORCE_INLINE VariantRef getOrAddElement(size_t index) const { return VariantRef(_pool, _data ? _data->getOrAddElement(index, _pool) : 0); } FORCE_INLINE VariantRef getElement(size_t index) const { return VariantRef(_pool, _data ? _data->getElement(index) : 0); } FORCE_INLINE void remove(iterator it) const { if (!_data) return; _data->removeSlot(it.internal()); } FORCE_INLINE void remove(size_t index) const { if (!_data) return; _data->removeElement(index); } void clear() const { if (!_data) return; _data->clear(); } private: MemoryPool* _pool; }; template <> struct Converter { static bool toJson(VariantConstRef src, VariantRef dst) { return variantCopyFrom(getData(dst), getData(src), getPool(dst)); } static ArrayConstRef fromJson(VariantConstRef src) { return ArrayConstRef(variantAsArray(getData(src))); } static bool checkJson(VariantConstRef src) { const VariantData* data = getData(src); return data && data->isArray(); } }; template <> struct Converter { static bool toJson(VariantConstRef src, VariantRef dst) { return variantCopyFrom(getData(dst), getData(src), getPool(dst)); } static ArrayRef fromJson(VariantRef src) { VariantData* data = getData(src); MemoryPool* pool = getPool(src); return ArrayRef(pool, data != 0 ? data->asArray() : 0); } static InvalidConversion fromJson(VariantConstRef); static bool checkJson(VariantConstRef) { return false; } static bool checkJson(VariantRef src) { VariantData* data = getData(src); return data && data->isArray(); } }; template typename TVisitor::result_type objectAccept(const CollectionData *obj, TVisitor &visitor) { if (obj) return visitor.visitObject(*obj); else return visitor.visitNull(); } inline bool objectEquals(const CollectionData *lhs, const CollectionData *rhs) { if (lhs == rhs) return true; if (!lhs || !rhs) return false; return lhs->equalsObject(*rhs); } template inline VariantData *objectGetMember(const CollectionData *obj, TAdaptedString key) { if (!obj) return 0; return obj->getMember(key); } template void objectRemove(CollectionData *obj, TAdaptedString key) { if (!obj) return; obj->removeMember(key); } template inline VariantData *objectGetOrAddMember(CollectionData *obj, TAdaptedString key, MemoryPool *pool) { if (!obj) return 0; return obj->getOrAddMember(key, pool); } class String { public: String() : _data(0), _isStatic(true) {} String(const char* data, bool isStaticData = true) : _data(data), _isStatic(isStaticData) {} const char* c_str() const { return _data; } bool isNull() const { return !_data; } bool isStatic() const { return _isStatic; } friend bool operator==(String lhs, String rhs) { if (lhs._data == rhs._data) return true; if (!lhs._data) return false; if (!rhs._data) return false; return strcmp(lhs._data, rhs._data) == 0; } friend bool operator!=(String lhs, String rhs) { if (lhs._data == rhs._data) return false; if (!lhs._data) return true; if (!rhs._data) return true; return strcmp(lhs._data, rhs._data) != 0; } private: const char* _data; bool _isStatic; }; class StringAdapter : public RamStringAdapter { public: StringAdapter(const String& str) : RamStringAdapter(str.c_str()), _isStatic(str.isStatic()) {} bool isStatic() const { return _isStatic; } typedef storage_policies::decide_at_runtime storage_policy; private: bool _isStatic; }; template <> struct IsString : true_type {}; inline StringAdapter adaptString(const String& str) { return StringAdapter(str); } class Pair { public: Pair(MemoryPool* pool, VariantSlot* slot) { if (slot) { _key = String(slot->key(), !slot->ownsKey()); _value = VariantRef(pool, slot->data()); } } String key() const { return _key; } VariantRef value() const { return _value; } private: String _key; VariantRef _value; }; class PairConst { public: PairConst(const VariantSlot* slot) { if (slot) { _key = String(slot->key(), !slot->ownsKey()); _value = VariantConstRef(slot->data()); } } String key() const { return _key; } VariantConstRef value() const { return _value; } private: String _key; VariantConstRef _value; }; class PairPtr { public: PairPtr(MemoryPool *pool, VariantSlot *slot) : _pair(pool, slot) {} const Pair *operator->() const { return &_pair; } const Pair &operator*() const { return _pair; } private: Pair _pair; }; class ObjectIterator { public: ObjectIterator() : _slot(0) {} explicit ObjectIterator(MemoryPool *pool, VariantSlot *slot) : _pool(pool), _slot(slot) {} Pair operator*() const { return Pair(_pool, _slot); } PairPtr operator->() { return PairPtr(_pool, _slot); } bool operator==(const ObjectIterator &other) const { return _slot == other._slot; } bool operator!=(const ObjectIterator &other) const { return _slot != other._slot; } ObjectIterator &operator++() { _slot = _slot->next(); return *this; } ObjectIterator &operator+=(size_t distance) { _slot = _slot->next(distance); return *this; } VariantSlot *internal() { return _slot; } private: MemoryPool *_pool; VariantSlot *_slot; }; class PairConstPtr { public: PairConstPtr(const VariantSlot *slot) : _pair(slot) {} const PairConst *operator->() const { return &_pair; } const PairConst &operator*() const { return _pair; } private: PairConst _pair; }; class ObjectConstIterator { public: ObjectConstIterator() : _slot(0) {} explicit ObjectConstIterator(const VariantSlot *slot) : _slot(slot) {} PairConst operator*() const { return PairConst(_slot); } PairConstPtr operator->() { return PairConstPtr(_slot); } bool operator==(const ObjectConstIterator &other) const { return _slot == other._slot; } bool operator!=(const ObjectConstIterator &other) const { return _slot != other._slot; } ObjectConstIterator &operator++() { _slot = _slot->next(); return *this; } ObjectConstIterator &operator+=(size_t distance) { _slot = _slot->next(distance); return *this; } const VariantSlot *internal() { return _slot; } private: const VariantSlot *_slot; }; } // namespace ARDUINOJSON_NAMESPACE #define JSON_OBJECT_SIZE(NUMBER_OF_ELEMENTS) \ ((NUMBER_OF_ELEMENTS) * sizeof(ARDUINOJSON_NAMESPACE::VariantSlot)) namespace ARDUINOJSON_NAMESPACE { template class ObjectRefBase { public: operator VariantConstRef() const { const void* data = _data; // prevent warning cast-align return VariantConstRef(reinterpret_cast(data)); } template typename TVisitor::result_type accept(TVisitor& visitor) const { return objectAccept(_data, visitor); } FORCE_INLINE bool isNull() const { return _data == 0; } FORCE_INLINE operator bool() const { return _data != 0; } FORCE_INLINE size_t memoryUsage() const { return _data ? _data->memoryUsage() : 0; } FORCE_INLINE size_t nesting() const { return _data ? _data->nesting() : 0; } FORCE_INLINE size_t size() const { return _data ? _data->size() : 0; } protected: ObjectRefBase(TData* data) : _data(data) {} TData* _data; }; class ObjectConstRef : public ObjectRefBase, public Visitable { friend class ObjectRef; typedef ObjectRefBase base_type; public: typedef ObjectConstIterator iterator; ObjectConstRef() : base_type(0) {} ObjectConstRef(const CollectionData* data) : base_type(data) {} FORCE_INLINE iterator begin() const { if (!_data) return iterator(); return iterator(_data->head()); } FORCE_INLINE iterator end() const { return iterator(); } template FORCE_INLINE bool containsKey(const TString& key) const { return !getMember(key).isUndefined(); } template FORCE_INLINE bool containsKey(TChar* key) const { return !getMember(key).isUndefined(); } template FORCE_INLINE VariantConstRef getMember(const TString& key) const { return get_impl(adaptString(key)); } template FORCE_INLINE VariantConstRef getMember(TChar* key) const { return get_impl(adaptString(key)); } template FORCE_INLINE typename enable_if::value, VariantConstRef>::type operator[](const TString& key) const { return get_impl(adaptString(key)); } template FORCE_INLINE typename enable_if::value, VariantConstRef>::type operator[](TChar* key) const { return get_impl(adaptString(key)); } FORCE_INLINE bool operator==(ObjectConstRef rhs) const { return objectEquals(_data, rhs._data); } private: template FORCE_INLINE VariantConstRef get_impl(TAdaptedString key) const { return VariantConstRef(objectGetMember(_data, key)); } }; class ObjectRef : public ObjectRefBase, public ObjectShortcuts, public Visitable { typedef ObjectRefBase base_type; public: typedef ObjectIterator iterator; FORCE_INLINE ObjectRef() : base_type(0), _pool(0) {} FORCE_INLINE ObjectRef(MemoryPool* buf, CollectionData* data) : base_type(data), _pool(buf) {} operator VariantRef() const { void* data = _data; // prevent warning cast-align return VariantRef(_pool, reinterpret_cast(data)); } operator ObjectConstRef() const { return ObjectConstRef(_data); } FORCE_INLINE iterator begin() const { if (!_data) return iterator(); return iterator(_pool, _data->head()); } FORCE_INLINE iterator end() const { return iterator(); } void clear() const { if (!_data) return; _data->clear(); } FORCE_INLINE bool set(ObjectConstRef src) { if (!_data || !src._data) return false; return _data->copyFrom(*src._data, _pool); } template FORCE_INLINE VariantRef getMember(const TString& key) const { return VariantRef(_pool, objectGetMember(_data, adaptString(key))); } template FORCE_INLINE VariantRef getMember(TChar* key) const { return VariantRef(_pool, objectGetMember(_data, adaptString(key))); } template FORCE_INLINE VariantRef getOrAddMember(const TString& key) const { return VariantRef(_pool, objectGetOrAddMember(_data, adaptString(key), _pool)); } template FORCE_INLINE VariantRef getOrAddMember(TChar* key) const { return VariantRef(_pool, objectGetOrAddMember(_data, adaptString(key), _pool)); } FORCE_INLINE bool operator==(ObjectRef rhs) const { return objectEquals(_data, rhs._data); } FORCE_INLINE void remove(iterator it) const { if (!_data) return; _data->removeSlot(it.internal()); } template FORCE_INLINE void remove(const TString& key) const { objectRemove(_data, adaptString(key)); } template FORCE_INLINE void remove(TChar* key) const { objectRemove(_data, adaptString(key)); } private: MemoryPool* _pool; }; template <> struct Converter { static bool toJson(VariantConstRef src, VariantRef dst) { return variantCopyFrom(getData(dst), getData(src), getPool(dst)); } static ObjectConstRef fromJson(VariantConstRef src) { return ObjectConstRef(variantAsObject(getData(src))); } static bool checkJson(VariantConstRef src) { const VariantData* data = getData(src); return data && data->isObject(); } }; template <> struct Converter { static bool toJson(VariantConstRef src, VariantRef dst) { return variantCopyFrom(getData(dst), getData(src), getPool(dst)); } static ObjectRef fromJson(VariantRef src) { VariantData* data = getData(src); MemoryPool* pool = getPool(src); return ObjectRef(pool, data != 0 ? data->asObject() : 0); } static InvalidConversion fromJson( VariantConstRef); static bool checkJson(VariantConstRef) { return false; } static bool checkJson(VariantRef src) { VariantData* data = getData(src); return data && data->isObject(); } }; class ArrayRef; class ObjectRef; class VariantRef; template struct VariantTo {}; template <> struct VariantTo { typedef ArrayRef type; }; template <> struct VariantTo { typedef ObjectRef type; }; template <> struct VariantTo { typedef VariantRef type; }; } // namespace ARDUINOJSON_NAMESPACE #ifdef _MSC_VER # pragma warning(push) # pragma warning(disable : 4522) #endif namespace ARDUINOJSON_NAMESPACE { template class ElementProxy : public VariantOperators >, public VariantShortcuts >, public Visitable, public VariantTag { typedef ElementProxy this_type; public: typedef VariantRef variant_type; FORCE_INLINE ElementProxy(TArray array, size_t index) : _array(array), _index(index) {} FORCE_INLINE ElementProxy(const ElementProxy& src) : _array(src._array), _index(src._index) {} FORCE_INLINE this_type& operator=(const this_type& src) { getOrAddUpstreamElement().set(src.as()); return *this; } template FORCE_INLINE this_type& operator=(const T& src) { getOrAddUpstreamElement().set(src); return *this; } template FORCE_INLINE this_type& operator=(T* src) { getOrAddUpstreamElement().set(src); return *this; } FORCE_INLINE void clear() const { getUpstreamElement().clear(); } FORCE_INLINE bool isNull() const { return getUpstreamElement().isNull(); } template FORCE_INLINE typename enable_if::value, T>::type as() const { return getUpstreamElement().template as(); } template FORCE_INLINE typename enable_if::value, const char*>::type ARDUINOJSON_DEPRECATED("Replace as() with as()") as() const { return as(); } template FORCE_INLINE operator T() const { return getUpstreamElement(); } template FORCE_INLINE bool is() const { return getUpstreamElement().template is(); } template FORCE_INLINE typename VariantTo::type to() const { return getOrAddUpstreamElement().template to(); } template FORCE_INLINE bool set(const TValue& value) const { return getOrAddUpstreamElement().set(value); } template FORCE_INLINE bool set(TValue* value) const { return getOrAddUpstreamElement().set(value); } template typename TVisitor::result_type accept(TVisitor& visitor) const { return getUpstreamElement().accept(visitor); } FORCE_INLINE size_t size() const { return getUpstreamElement().size(); } template VariantRef getMember(TNestedKey* key) const { return getUpstreamElement().getMember(key); } template VariantRef getMember(const TNestedKey& key) const { return getUpstreamElement().getMember(key); } template VariantRef getOrAddMember(TNestedKey* key) const { return getOrAddUpstreamElement().getOrAddMember(key); } template VariantRef getOrAddMember(const TNestedKey& key) const { return getOrAddUpstreamElement().getOrAddMember(key); } VariantRef addElement() const { return getOrAddUpstreamElement().addElement(); } VariantRef getElement(size_t index) const { return getOrAddUpstreamElement().getElement(index); } VariantRef getOrAddElement(size_t index) const { return getOrAddUpstreamElement().getOrAddElement(index); } FORCE_INLINE void remove(size_t index) const { getUpstreamElement().remove(index); } template FORCE_INLINE typename enable_if::value>::type remove( TChar* key) const { getUpstreamElement().remove(key); } template FORCE_INLINE typename enable_if::value>::type remove( const TString& key) const { getUpstreamElement().remove(key); } private: FORCE_INLINE VariantRef getUpstreamElement() const { return _array.getElement(_index); } FORCE_INLINE VariantRef getOrAddUpstreamElement() const { return _array.getOrAddElement(_index); } friend bool convertToJson(const this_type& src, VariantRef dst) { return dst.set(src.getUpstreamElement()); } TArray _array; const size_t _index; }; } // namespace ARDUINOJSON_NAMESPACE #ifdef _MSC_VER # pragma warning(pop) #endif #ifdef _MSC_VER # pragma warning(push) # pragma warning(disable : 4522) #endif namespace ARDUINOJSON_NAMESPACE { template class MemberProxy : public VariantOperators >, public VariantShortcuts >, public Visitable, public VariantTag { typedef MemberProxy this_type; public: typedef VariantRef variant_type; FORCE_INLINE MemberProxy(TObject variant, TStringRef key) : _object(variant), _key(key) {} FORCE_INLINE MemberProxy(const MemberProxy &src) : _object(src._object), _key(src._key) {} FORCE_INLINE operator VariantConstRef() const { return getUpstreamMember(); } FORCE_INLINE this_type &operator=(const this_type &src) { getOrAddUpstreamMember().set(src); return *this; } template FORCE_INLINE typename enable_if::value, this_type &>::type operator=(const TValue &src) { getOrAddUpstreamMember().set(src); return *this; } template FORCE_INLINE this_type &operator=(TChar *src) { getOrAddUpstreamMember().set(src); return *this; } FORCE_INLINE void clear() const { getUpstreamMember().clear(); } FORCE_INLINE bool isNull() const { return getUpstreamMember().isNull(); } template FORCE_INLINE typename enable_if::value, T>::type as() const { return getUpstreamMember().template as(); } template FORCE_INLINE typename enable_if::value, const char *>::type ARDUINOJSON_DEPRECATED("Replace as() with as()") as() const { return as(); } template FORCE_INLINE operator T() const { return getUpstreamMember(); } template FORCE_INLINE bool is() const { return getUpstreamMember().template is(); } FORCE_INLINE size_t size() const { return getUpstreamMember().size(); } FORCE_INLINE void remove(size_t index) const { getUpstreamMember().remove(index); } template FORCE_INLINE typename enable_if::value>::type remove( TChar *key) const { getUpstreamMember().remove(key); } template FORCE_INLINE typename enable_if::value>::type remove( const TString &key) const { getUpstreamMember().remove(key); } template FORCE_INLINE typename VariantTo::type to() { return getOrAddUpstreamMember().template to(); } template FORCE_INLINE bool set(const TValue &value) { return getOrAddUpstreamMember().set(value); } template FORCE_INLINE bool set(TChar *value) { return getOrAddUpstreamMember().set(value); } template typename TVisitor::result_type accept(TVisitor &visitor) const { return getUpstreamMember().accept(visitor); } FORCE_INLINE VariantRef addElement() const { return getOrAddUpstreamMember().addElement(); } FORCE_INLINE VariantRef getElement(size_t index) const { return getUpstreamMember().getElement(index); } FORCE_INLINE VariantRef getOrAddElement(size_t index) const { return getOrAddUpstreamMember().getOrAddElement(index); } template FORCE_INLINE VariantRef getMember(TChar *key) const { return getUpstreamMember().getMember(key); } template FORCE_INLINE VariantRef getMember(const TString &key) const { return getUpstreamMember().getMember(key); } template FORCE_INLINE VariantRef getOrAddMember(TChar *key) const { return getOrAddUpstreamMember().getOrAddMember(key); } template FORCE_INLINE VariantRef getOrAddMember(const TString &key) const { return getOrAddUpstreamMember().getOrAddMember(key); } private: FORCE_INLINE VariantRef getUpstreamMember() const { return _object.getMember(_key); } FORCE_INLINE VariantRef getOrAddUpstreamMember() const { return _object.getOrAddMember(_key); } friend bool convertToJson(const this_type &src, VariantRef dst) { return dst.set(src.getUpstreamMember()); } TObject _object; TStringRef _key; }; } // namespace ARDUINOJSON_NAMESPACE #ifdef _MSC_VER # pragma warning(pop) #endif namespace ARDUINOJSON_NAMESPACE { class JsonDocument : public Visitable { public: template typename TVisitor::result_type accept(TVisitor& visitor) const { return getVariant().accept(visitor); } template T as() { return getVariant().template as(); } template T as() const { return getVariant().template as(); } void clear() { _pool.clear(); _data.init(); } template bool is() { return getVariant().template is(); } template bool is() const { return getVariant().template is(); } bool isNull() const { return getVariant().isNull(); } size_t memoryUsage() const { return _pool.size(); } bool overflowed() const { return _pool.overflowed(); } size_t nesting() const { return _data.nesting(); } size_t capacity() const { return _pool.capacity(); } size_t size() const { return _data.size(); } bool set(const JsonDocument& src) { return to().set(src.as()); } template typename enable_if::value, bool>::type set( const T& src) { return to().set(src); } template typename VariantTo::type to() { clear(); return getVariant().template to(); } MemoryPool& memoryPool() { return _pool; } VariantData& data() { return _data; } ArrayRef createNestedArray() { return addElement().to(); } template ArrayRef createNestedArray(TChar* key) { return getOrAddMember(key).template to(); } template ArrayRef createNestedArray(const TString& key) { return getOrAddMember(key).template to(); } ObjectRef createNestedObject() { return addElement().to(); } template ObjectRef createNestedObject(TChar* key) { return getOrAddMember(key).template to(); } template ObjectRef createNestedObject(const TString& key) { return getOrAddMember(key).template to(); } template bool containsKey(TChar* key) const { return !getMember(key).isUndefined(); } template bool containsKey(const TString& key) const { return !getMember(key).isUndefined(); } template FORCE_INLINE typename enable_if::value, MemberProxy >::type operator[](const TString& key) { return MemberProxy(*this, key); } template FORCE_INLINE typename enable_if::value, MemberProxy >::type operator[](TChar* key) { return MemberProxy(*this, key); } template FORCE_INLINE typename enable_if::value, VariantConstRef>::type operator[](const TString& key) const { return getMember(key); } template FORCE_INLINE typename enable_if::value, VariantConstRef>::type operator[](TChar* key) const { return getMember(key); } FORCE_INLINE ElementProxy operator[](size_t index) { return ElementProxy(*this, index); } FORCE_INLINE VariantConstRef operator[](size_t index) const { return getElement(index); } FORCE_INLINE VariantRef getElement(size_t index) { return VariantRef(&_pool, _data.getElement(index)); } FORCE_INLINE VariantConstRef getElement(size_t index) const { return VariantConstRef(_data.getElement(index)); } FORCE_INLINE VariantRef getOrAddElement(size_t index) { return VariantRef(&_pool, _data.getOrAddElement(index, &_pool)); } template FORCE_INLINE VariantConstRef getMember(TChar* key) const { return VariantConstRef(_data.getMember(adaptString(key))); } template FORCE_INLINE typename enable_if::value, VariantConstRef>::type getMember(const TString& key) const { return VariantConstRef(_data.getMember(adaptString(key))); } template FORCE_INLINE VariantRef getMember(TChar* key) { return VariantRef(&_pool, _data.getMember(adaptString(key))); } template FORCE_INLINE typename enable_if::value, VariantRef>::type getMember(const TString& key) { return VariantRef(&_pool, _data.getMember(adaptString(key))); } template FORCE_INLINE VariantRef getOrAddMember(TChar* key) { return VariantRef(&_pool, _data.getOrAddMember(adaptString(key), &_pool)); } template FORCE_INLINE VariantRef getOrAddMember(const TString& key) { return VariantRef(&_pool, _data.getOrAddMember(adaptString(key), &_pool)); } FORCE_INLINE VariantRef addElement() { return VariantRef(&_pool, _data.addElement(&_pool)); } template FORCE_INLINE bool add(const TValue& value) { return addElement().set(value); } template FORCE_INLINE bool add(TChar* value) { return addElement().set(value); } FORCE_INLINE void remove(size_t index) { _data.remove(index); } template FORCE_INLINE typename enable_if::value>::type remove( TChar* key) { _data.remove(adaptString(key)); } template FORCE_INLINE typename enable_if::value>::type remove( const TString& key) { _data.remove(adaptString(key)); } FORCE_INLINE operator VariantConstRef() const { return VariantConstRef(&_data); } bool operator==(VariantConstRef rhs) const { return getVariant() == rhs; } bool operator!=(VariantConstRef rhs) const { return getVariant() != rhs; } protected: JsonDocument() : _pool(0, 0) { _data.init(); } JsonDocument(MemoryPool pool) : _pool(pool) { _data.init(); } JsonDocument(char* buf, size_t capa) : _pool(buf, capa) { _data.init(); } ~JsonDocument() {} void replacePool(MemoryPool pool) { _pool = pool; } VariantRef getVariant() { return VariantRef(&_pool, &_data); } VariantConstRef getVariant() const { return VariantConstRef(&_data); } MemoryPool _pool; VariantData _data; private: JsonDocument(const JsonDocument&); JsonDocument& operator=(const JsonDocument&); }; inline bool convertToJson(const JsonDocument& src, VariantRef dst) { return dst.set(src.as()); } template class AllocatorOwner { public: AllocatorOwner() {} AllocatorOwner(const AllocatorOwner& src) : _allocator(src._allocator) {} AllocatorOwner(TAllocator a) : _allocator(a) {} void* allocate(size_t size) { return _allocator.allocate(size); } void deallocate(void* ptr) { if (ptr) _allocator.deallocate(ptr); } void* reallocate(void* ptr, size_t new_size) { return _allocator.reallocate(ptr, new_size); } TAllocator& allocator() { return _allocator; } private: TAllocator _allocator; }; template class BasicJsonDocument : AllocatorOwner, public JsonDocument { public: explicit BasicJsonDocument(size_t capa, TAllocator alloc = TAllocator()) : AllocatorOwner(alloc), JsonDocument(allocPool(capa)) {} BasicJsonDocument(const BasicJsonDocument& src) : AllocatorOwner(src), JsonDocument() { copyAssignFrom(src); } #if ARDUINOJSON_HAS_RVALUE_REFERENCES BasicJsonDocument(BasicJsonDocument&& src) : AllocatorOwner(src) { moveAssignFrom(src); } #endif BasicJsonDocument(const JsonDocument& src) { copyAssignFrom(src); } template BasicJsonDocument( const T& src, typename enable_if< is_same::value || is_same::value || is_same::value || is_same::value || is_same::value || is_same::value>::type* = 0) : JsonDocument(allocPool(src.memoryUsage())) { set(src); } BasicJsonDocument(VariantRef src) : JsonDocument(allocPool(src.memoryUsage())) { set(src); } ~BasicJsonDocument() { freePool(); } BasicJsonDocument& operator=(const BasicJsonDocument& src) { copyAssignFrom(src); return *this; } #if ARDUINOJSON_HAS_RVALUE_REFERENCES BasicJsonDocument& operator=(BasicJsonDocument&& src) { moveAssignFrom(src); return *this; } #endif template BasicJsonDocument& operator=(const T& src) { reallocPoolIfTooSmall(src.memoryUsage()); set(src); return *this; } void shrinkToFit() { ptrdiff_t bytes_reclaimed = _pool.squash(); if (bytes_reclaimed == 0) return; void* old_ptr = _pool.buffer(); void* new_ptr = this->reallocate(old_ptr, _pool.capacity()); ptrdiff_t ptr_offset = static_cast(new_ptr) - static_cast(old_ptr); _pool.movePointers(ptr_offset); _data.movePointers(ptr_offset, ptr_offset - bytes_reclaimed); } bool garbageCollect() { BasicJsonDocument tmp(*this); if (!tmp.capacity()) return false; tmp.set(*this); moveAssignFrom(tmp); return true; } using AllocatorOwner::allocator; private: MemoryPool allocPool(size_t requiredSize) { size_t capa = addPadding(requiredSize); return MemoryPool(reinterpret_cast(this->allocate(capa)), capa); } void reallocPoolIfTooSmall(size_t requiredSize) { if (requiredSize <= capacity()) return; freePool(); replacePool(allocPool(addPadding(requiredSize))); } void freePool() { this->deallocate(memoryPool().buffer()); } void copyAssignFrom(const JsonDocument& src) { reallocPoolIfTooSmall(src.capacity()); set(src); } void moveAssignFrom(BasicJsonDocument& src) { freePool(); _data = src._data; _pool = src._pool; src._data.setNull(); src._pool = MemoryPool(0, 0); } }; } // namespace ARDUINOJSON_NAMESPACE #include namespace ARDUINOJSON_NAMESPACE { struct DefaultAllocator { void* allocate(size_t size) { return malloc(size); } void deallocate(void* ptr) { free(ptr); } void* reallocate(void* ptr, size_t new_size) { return realloc(ptr, new_size); } }; typedef BasicJsonDocument DynamicJsonDocument; template class StaticJsonDocument : public JsonDocument { static const size_t _capacity = AddPadding::value>::value; public: StaticJsonDocument() : JsonDocument(_buffer, _capacity) {} StaticJsonDocument(const StaticJsonDocument& src) : JsonDocument(_buffer, _capacity) { set(src); } template StaticJsonDocument(const T& src, typename enable_if::value>::type* = 0) : JsonDocument(_buffer, _capacity) { set(src); } StaticJsonDocument(VariantRef src) : JsonDocument(_buffer, _capacity) { set(src); } StaticJsonDocument operator=(const StaticJsonDocument& src) { set(src); return *this; } template StaticJsonDocument operator=(const T& src) { set(src); return *this; } void garbageCollect() { StaticJsonDocument tmp(*this); set(tmp); } private: char _buffer[_capacity]; }; template inline ArrayRef ArrayShortcuts::createNestedArray() const { return impl()->addElement().template to(); } template inline ObjectRef ArrayShortcuts::createNestedObject() const { return impl()->addElement().template to(); } template inline ElementProxy ArrayShortcuts::operator[]( size_t index) const { return ElementProxy(*impl(), index); } template struct Visitor { typedef TResult result_type; TResult visitArray(const CollectionData &) { return TResult(); } TResult visitBoolean(bool) { return TResult(); } TResult visitFloat(Float) { return TResult(); } TResult visitSignedInteger(Integer) { return TResult(); } TResult visitNull() { return TResult(); } TResult visitObject(const CollectionData &) { return TResult(); } TResult visitUnsignedInteger(UInt) { return TResult(); } TResult visitRawJson(const char *, size_t) { return TResult(); } TResult visitString(const char *) { return TResult(); } }; template inline typename enable_if::value && !is_base_of::value, bool>::type copyArray(T (&src)[N], const TDestination& dst) { return copyArray(src, N, dst); } template inline bool copyArray(T (&src)[N], JsonDocument& dst) { return copyArray(src, dst.to()); } template inline typename enable_if::value && !is_base_of::value, bool>::type copyArray(T* src, size_t len, const TDestination& dst) { bool ok = true; for (size_t i = 0; i < len; i++) { ok &= dst.add(src[i]); } return ok; } template inline bool copyArray(T* src, size_t len, JsonDocument& dst) { return copyArray(src, len, dst.to()); } template inline typename enable_if::value, bool>::type copyArray(T (&src)[N1][N2], const TDestination& dst) { bool ok = true; for (size_t i = 0; i < N1; i++) { ArrayRef nestedArray = dst.createNestedArray(); for (size_t j = 0; j < N2; j++) { ok &= nestedArray.add(src[i][j]); } } return ok; } template inline bool copyArray(T (&src)[N1][N2], JsonDocument& dst) { return copyArray(src, dst.to()); } template class ArrayCopier1D : public Visitor { public: ArrayCopier1D(T* destination, size_t capacity) : _destination(destination), _capacity(capacity) {} size_t visitArray(const CollectionData& array) { size_t size = 0; VariantSlot* slot = array.head(); while (slot != 0 && size < _capacity) { _destination[size++] = Converter::fromJson(VariantConstRef(slot->data())); slot = slot->next(); } return size; } private: T* _destination; size_t _capacity; }; template class ArrayCopier2D : public Visitor { public: ArrayCopier2D(T (*destination)[N1][N2]) : _destination(destination) {} void visitArray(const CollectionData& array) { VariantSlot* slot = array.head(); size_t n = 0; while (slot != 0 && n < N1) { ArrayCopier1D copier((*_destination)[n++], N2); variantAccept(slot->data(), copier); slot = slot->next(); } } private: T (*_destination)[N1][N2]; size_t _capacity1, _capacity2; }; template inline typename enable_if::value, size_t>::type copyArray( const TSource& src, T (&dst)[N]) { return copyArray(src, dst, N); } template inline size_t copyArray(const TSource& src, T* dst, size_t len) { ArrayCopier1D copier(dst, len); return src.accept(copier); } template inline void copyArray(const TSource& src, T (&dst)[N1][N2]) { ArrayCopier2D copier(&dst); src.accept(copier); } inline bool variantEquals(const VariantData* a, const VariantData* b) { return variantCompare(a, b) == COMPARE_RESULT_EQUAL; } inline VariantSlot* CollectionData::addSlot(MemoryPool* pool) { VariantSlot* slot = pool->allocVariant(); if (!slot) return 0; if (_tail) { _tail->setNextNotNull(slot); _tail = slot; } else { _head = slot; _tail = slot; } slot->clear(); return slot; } inline VariantData* CollectionData::addElement(MemoryPool* pool) { return slotData(addSlot(pool)); } template inline VariantData* CollectionData::addMember(TAdaptedString key, MemoryPool* pool) { VariantSlot* slot = addSlot(pool); if (!slotSetKey(slot, key, pool)) { removeSlot(slot); return 0; } return slot->data(); } inline void CollectionData::clear() { _head = 0; _tail = 0; } template inline bool CollectionData::containsKey(const TAdaptedString& key) const { return getSlot(key) != 0; } inline bool CollectionData::copyFrom(const CollectionData& src, MemoryPool* pool) { clear(); for (VariantSlot* s = src._head; s; s = s->next()) { VariantData* var; if (s->key() != 0) { if (s->ownsKey()) var = addMember(RamStringAdapter(s->key()), pool); else var = addMember(ConstRamStringAdapter(s->key()), pool); } else { var = addElement(pool); } if (!var) return false; if (!var->copyFrom(*s->data(), pool)) return false; } return true; } inline bool CollectionData::equalsObject(const CollectionData& other) const { size_t count = 0; for (VariantSlot* slot = _head; slot; slot = slot->next()) { VariantData* v1 = slot->data(); VariantData* v2 = other.getMember(adaptString(slot->key())); if (!variantEquals(v1, v2)) return false; count++; } return count == other.size(); } inline bool CollectionData::equalsArray(const CollectionData& other) const { VariantSlot* s1 = _head; VariantSlot* s2 = other._head; for (;;) { if (s1 == s2) return true; if (!s1 || !s2) return false; if (!variantEquals(s1->data(), s2->data())) return false; s1 = s1->next(); s2 = s2->next(); } } template inline VariantSlot* CollectionData::getSlot(TAdaptedString key) const { VariantSlot* slot = _head; while (slot) { if (key.equals(slot->key())) break; slot = slot->next(); } return slot; } inline VariantSlot* CollectionData::getSlot(size_t index) const { if (!_head) return 0; return _head->next(index); } inline VariantSlot* CollectionData::getPreviousSlot(VariantSlot* target) const { VariantSlot* current = _head; while (current) { VariantSlot* next = current->next(); if (next == target) return current; current = next; } return 0; } template inline VariantData* CollectionData::getMember(TAdaptedString key) const { VariantSlot* slot = getSlot(key); return slot ? slot->data() : 0; } template inline VariantData* CollectionData::getOrAddMember(TAdaptedString key, MemoryPool* pool) { if (key.isNull()) return 0; VariantSlot* slot = getSlot(key); if (slot) return slot->data(); return addMember(key, pool); } inline VariantData* CollectionData::getElement(size_t index) const { VariantSlot* slot = getSlot(index); return slot ? slot->data() : 0; } inline VariantData* CollectionData::getOrAddElement(size_t index, MemoryPool* pool) { VariantSlot* slot = _head; while (slot && index > 0) { slot = slot->next(); index--; } if (!slot) index++; while (index > 0) { slot = addSlot(pool); index--; } return slotData(slot); } inline void CollectionData::removeSlot(VariantSlot* slot) { if (!slot) return; VariantSlot* prev = getPreviousSlot(slot); VariantSlot* next = slot->next(); if (prev) prev->setNext(next); else _head = next; if (!next) _tail = prev; } inline void CollectionData::removeElement(size_t index) { removeSlot(getSlot(index)); } inline size_t CollectionData::memoryUsage() const { size_t total = 0; for (VariantSlot* s = _head; s; s = s->next()) { total += sizeof(VariantSlot) + s->data()->memoryUsage(); if (s->ownsKey()) total += strlen(s->key()) + 1; } return total; } inline size_t CollectionData::nesting() const { size_t maxChildNesting = 0; for (VariantSlot* s = _head; s; s = s->next()) { size_t childNesting = s->data()->nesting(); if (childNesting > maxChildNesting) maxChildNesting = childNesting; } return maxChildNesting + 1; } inline size_t CollectionData::size() const { return slotSize(_head); } template inline void movePointer(T*& p, ptrdiff_t offset) { if (!p) return; p = reinterpret_cast( reinterpret_cast(reinterpret_cast(p) + offset)); ARDUINOJSON_ASSERT(isAligned(p)); } inline void CollectionData::movePointers(ptrdiff_t stringDistance, ptrdiff_t variantDistance) { movePointer(_head, variantDistance); movePointer(_tail, variantDistance); for (VariantSlot* slot = _head; slot; slot = slot->next()) slot->movePointers(stringDistance, variantDistance); } template template inline ArrayRef ObjectShortcuts::createNestedArray( const TString& key) const { return impl()->getOrAddMember(key).template to(); } template template inline ArrayRef ObjectShortcuts::createNestedArray(TChar* key) const { return impl()->getOrAddMember(key).template to(); } template template inline ObjectRef ObjectShortcuts::createNestedObject( const TString& key) const { return impl()->getOrAddMember(key).template to(); } template template inline ObjectRef ObjectShortcuts::createNestedObject( TChar* key) const { return impl()->getOrAddMember(key).template to(); } template template inline typename enable_if::value, bool>::type ObjectShortcuts::containsKey(const TString& key) const { return !impl()->getMember(key).isUndefined(); } template template inline typename enable_if::value, bool>::type ObjectShortcuts::containsKey(TChar* key) const { return !impl()->getMember(key).isUndefined(); } template template inline typename enable_if::value, MemberProxy >::type ObjectShortcuts::operator[](TString* key) const { return MemberProxy(*impl(), key); } template template inline typename enable_if::value, MemberProxy >::type ObjectShortcuts::operator[](const TString& key) const { return MemberProxy(*impl(), key); } } // namespace ARDUINOJSON_NAMESPACE #if ARDUINOJSON_ENABLE_ARDUINO_STRING #endif #if ARDUINOJSON_ENABLE_STD_STRING #endif namespace ARDUINOJSON_NAMESPACE { template struct IsWriteableString : false_type {}; #if ARDUINOJSON_ENABLE_ARDUINO_STRING template <> struct IsWriteableString< ::String> : true_type {}; #endif #if ARDUINOJSON_ENABLE_STD_STRING template struct IsWriteableString > : true_type {}; #endif template struct Converter { static bool toJson(const T& src, VariantRef dst) { return convertToJson(src, dst); // Error here? See https://arduinojson.org/v6/unsupported-set/ } static T fromJson(VariantConstRef src) { T result; // Error here? See https://arduinojson.org/v6/non-default-constructible/ convertFromJson(src, result); // Error here? See https://arduinojson.org/v6/unsupported-as/ return result; } static bool checkJson(VariantConstRef src) { T dummy; return canConvertFromJson(src, dummy); // Error here? See https://arduinojson.org/v6/unsupported-is/ } }; template struct Converter< T, typename enable_if::value && !is_same::value && !is_same::value>::type> { static bool toJson(T src, VariantRef dst) { VariantData* data = getData(dst); ARDUINOJSON_ASSERT_INTEGER_TYPE_IS_SUPPORTED(T); if (!data) return false; data->setInteger(src); return true; } static T fromJson(VariantConstRef src) { ARDUINOJSON_ASSERT_INTEGER_TYPE_IS_SUPPORTED(T); const VariantData* data = getData(src); return data ? data->asIntegral() : T(); } static bool checkJson(VariantConstRef src) { const VariantData* data = getData(src); return data && data->isInteger(); } }; template struct Converter::value>::type> { static bool toJson(T src, VariantRef dst) { return dst.set(static_cast(src)); } static T fromJson(VariantConstRef src) { const VariantData* data = getData(src); return data ? static_cast(data->asIntegral()) : T(); } static bool checkJson(VariantConstRef src) { const VariantData* data = getData(src); return data && data->isInteger(); } }; template <> struct Converter { static bool toJson(bool src, VariantRef dst) { VariantData* data = getData(dst); if (!data) return false; data->setBoolean(src); return true; } static bool fromJson(VariantConstRef src) { const VariantData* data = getData(src); return data ? data->asBoolean() : false; } static bool checkJson(VariantConstRef src) { const VariantData* data = getData(src); return data && data->isBoolean(); } }; template struct Converter::value>::type> { static bool toJson(T src, VariantRef dst) { VariantData* data = getData(dst); if (!data) return false; data->setFloat(static_cast(src)); return true; } static T fromJson(VariantConstRef src) { const VariantData* data = getData(src); return data ? data->asFloat() : false; } static bool checkJson(VariantConstRef src) { const VariantData* data = getData(src); return data && data->isFloat(); } }; template <> struct Converter { static bool toJson(const char* src, VariantRef dst) { return variantSetString(getData(dst), adaptString(src), getPool(dst)); } static const char* fromJson(VariantConstRef src) { const VariantData* data = getData(src); return data ? data->asString() : 0; } static bool checkJson(VariantConstRef src) { const VariantData* data = getData(src); return data && data->isString(); } }; template inline typename enable_if::value, bool>::type convertToJson( const T& src, VariantRef dst) { VariantData* data = getData(dst); MemoryPool* pool = getPool(dst); return variantSetString(data, adaptString(src), pool); } template inline typename enable_if::value>::type convertFromJson( VariantConstRef src, T& dst) { const VariantData* data = getData(src); const char* cstr = data != 0 ? data->asString() : 0; if (cstr) dst = cstr; else serializeJson(src, dst); } template inline typename enable_if::value, bool>::type canConvertFromJson(VariantConstRef src, const T&) { const VariantData* data = getData(src); return data && data->isString(); } template <> struct Converter > { static bool toJson(SerializedValue src, VariantRef dst) { VariantData* data = getData(dst); if (!data) return false; data->setLinkedRaw(src); return true; } }; template struct Converter, typename enable_if::value>::type> { static bool toJson(SerializedValue src, VariantRef dst) { VariantData* data = getData(dst); MemoryPool* pool = getPool(dst); return data != 0 && data->setOwnedRaw(src, pool); } }; #if ARDUINOJSON_HAS_NULLPTR template <> struct Converter { static bool toJson(decltype(nullptr), VariantRef dst) { variantSetNull(getData(dst)); return true; } static decltype(nullptr) fromJson(VariantConstRef) { return nullptr; } static bool checkJson(VariantConstRef src) { const VariantData* data = getData(src); return data == 0 || data->isNull(); } }; #endif #if ARDUINOJSON_ENABLE_ARDUINO_STREAM class MemoryPoolPrint : public Print { public: MemoryPoolPrint(MemoryPool* pool) : _pool(pool), _size(0) { pool->getFreeZone(&_string, &_capacity); } const char* c_str() { _string[_size++] = 0; ARDUINOJSON_ASSERT(_size <= _capacity); return _pool->saveStringFromFreeZone(_size); } size_t write(uint8_t c) { if (_size >= _capacity) return 0; _string[_size++] = char(c); return 1; } size_t write(const uint8_t* buffer, size_t size) { if (_size + size >= _capacity) { _size = _capacity; // mark as overflowed return 0; } memcpy(&_string[_size], buffer, size); _size += size; return size; } bool overflowed() const { return _size >= _capacity; } private: MemoryPool* _pool; size_t _size; char* _string; size_t _capacity; }; inline bool convertToJson(const ::Printable& src, VariantRef dst) { MemoryPool* pool = getPool(dst); VariantData* data = getData(dst); if (!pool || !data) return false; MemoryPoolPrint print(pool); src.printTo(print); if (print.overflowed()) { pool->markAsOverflowed(); data->setNull(); return false; } data->setStringPointer(print.c_str(), storage_policies::store_by_copy()); return true; } #endif class CollectionData; struct ComparerBase : Visitor {}; template struct Comparer; template struct Comparer::value>::type> : ComparerBase { T rhs; explicit Comparer(T value) : rhs(value) {} CompareResult visitString(const char *lhs) { int i = adaptString(rhs).compare(lhs); if (i < 0) return COMPARE_RESULT_GREATER; else if (i > 0) return COMPARE_RESULT_LESS; else return COMPARE_RESULT_EQUAL; } CompareResult visitNull() { if (adaptString(rhs).isNull()) return COMPARE_RESULT_EQUAL; else return COMPARE_RESULT_DIFFER; } }; template struct Comparer::value || is_floating_point::value>::type> : ComparerBase { T rhs; explicit Comparer(T value) : rhs(value) {} CompareResult visitFloat(Float lhs) { return arithmeticCompare(lhs, rhs); } CompareResult visitSignedInteger(Integer lhs) { return arithmeticCompare(lhs, rhs); } CompareResult visitUnsignedInteger(UInt lhs) { return arithmeticCompare(lhs, rhs); } CompareResult visitBoolean(bool lhs) { return visitUnsignedInteger(static_cast(lhs)); } }; struct NullComparer : ComparerBase { CompareResult visitNull() { return COMPARE_RESULT_EQUAL; } }; #if ARDUINOJSON_HAS_NULLPTR template <> struct Comparer : NullComparer { explicit Comparer(decltype(nullptr)) : NullComparer() {} }; #endif struct ArrayComparer : ComparerBase { const CollectionData *_rhs; explicit ArrayComparer(const CollectionData &rhs) : _rhs(&rhs) {} CompareResult visitArray(const CollectionData &lhs) { if (lhs.equalsArray(*_rhs)) return COMPARE_RESULT_EQUAL; else return COMPARE_RESULT_DIFFER; } }; struct ObjectComparer : ComparerBase { const CollectionData *_rhs; explicit ObjectComparer(const CollectionData &rhs) : _rhs(&rhs) {} CompareResult visitObject(const CollectionData &lhs) { if (lhs.equalsObject(*_rhs)) return COMPARE_RESULT_EQUAL; else return COMPARE_RESULT_DIFFER; } }; struct RawComparer : ComparerBase { const char *_rhsData; size_t _rhsSize; explicit RawComparer(const char *rhsData, size_t rhsSize) : _rhsData(rhsData), _rhsSize(rhsSize) {} CompareResult visitRawJson(const char *lhsData, size_t lhsSize) { size_t size = _rhsSize < lhsSize ? _rhsSize : lhsSize; int n = memcmp(lhsData, _rhsData, size); if (n < 0) return COMPARE_RESULT_LESS; else if (n > 0) return COMPARE_RESULT_GREATER; else return COMPARE_RESULT_EQUAL; } }; template struct Comparer::value>::type> : ComparerBase { T rhs; explicit Comparer(T value) : rhs(value) {} CompareResult visitArray(const CollectionData &lhs) { ArrayComparer comparer(lhs); return accept(comparer); } CompareResult visitObject(const CollectionData &lhs) { ObjectComparer comparer(lhs); return accept(comparer); } CompareResult visitFloat(Float lhs) { Comparer comparer(lhs); return accept(comparer); } CompareResult visitString(const char *lhs) { Comparer comparer(lhs); return accept(comparer); } CompareResult visitRawJson(const char *lhsData, size_t lhsSize) { RawComparer comparer(lhsData, lhsSize); return accept(comparer); } CompareResult visitSignedInteger(Integer lhs) { Comparer comparer(lhs); return accept(comparer); } CompareResult visitUnsignedInteger(UInt lhs) { Comparer comparer(lhs); return accept(comparer); } CompareResult visitBoolean(bool lhs) { Comparer comparer(lhs); return accept(comparer); } CompareResult visitNull() { NullComparer comparer; return accept(comparer); } private: template CompareResult accept(TComparer &comparer) { CompareResult reversedResult = rhs.accept(comparer); switch (reversedResult) { case COMPARE_RESULT_GREATER: return COMPARE_RESULT_LESS; case COMPARE_RESULT_LESS: return COMPARE_RESULT_GREATER; default: return reversedResult; } } }; template CompareResult compare(const T1 &lhs, const T2 &rhs) { Comparer comparer(rhs); return lhs.accept(comparer); } inline int variantCompare(const VariantData *a, const VariantData *b) { return compare(VariantConstRef(a), VariantConstRef(b)); } #ifndef isnan template bool isnan(T x) { return x != x; } #endif #ifndef isinf template bool isinf(T x) { return x != 0.0 && x * 2 == x; } #endif template struct alias_cast_t { union { F raw; T data; }; }; template T alias_cast(F raw_data) { alias_cast_t ac; ac.raw = raw_data; return ac.data; } } // namespace ARDUINOJSON_NAMESPACE #if ARDUINOJSON_ENABLE_PROGMEM namespace ARDUINOJSON_NAMESPACE { template typename enable_if::value, T>::type pgm_read(const void* p) { return reinterpret_cast(pgm_read_ptr(p)); } template typename enable_if::value && sizeof(T) == sizeof(float), // on AVR sizeof(double) == T>::type pgm_read(const void* p) { return pgm_read_float(p); } template typename enable_if::value, T>::type pgm_read( const void* p) { return pgm_read_dword(p); } } // namespace ARDUINOJSON_NAMESPACE # ifndef ARDUINOJSON_DEFINE_STATIC_ARRAY # define ARDUINOJSON_DEFINE_STATIC_ARRAY(type, name, value) \ static type const name[] PROGMEM = value; # endif # ifndef ARDUINOJSON_READ_STATIC_ARRAY # define ARDUINOJSON_READ_STATIC_ARRAY(type, name, index) \ pgm_read(name + index) # endif #else // i.e. ARDUINOJSON_ENABLE_PROGMEM == 0 # ifndef ARDUINOJSON_DEFINE_STATIC_ARRAY # define ARDUINOJSON_DEFINE_STATIC_ARRAY(type, name, value) \ static type const name[] = value; # endif # ifndef ARDUINOJSON_READ_STATIC_ARRAY # define ARDUINOJSON_READ_STATIC_ARRAY(type, name, index) name[index] # endif #endif namespace ARDUINOJSON_NAMESPACE { template struct FloatTraits {}; template struct FloatTraits { typedef uint64_t mantissa_type; static const short mantissa_bits = 52; static const mantissa_type mantissa_max = (mantissa_type(1) << mantissa_bits) - 1; typedef int16_t exponent_type; static const exponent_type exponent_max = 308; template static T make_float(T m, TExponent e) { if (e > 0) { for (uint8_t index = 0; e != 0; index++) { if (e & 1) m *= positiveBinaryPowerOfTen(index); e >>= 1; } } else { e = TExponent(-e); for (uint8_t index = 0; e != 0; index++) { if (e & 1) m *= negativeBinaryPowerOfTen(index); e >>= 1; } } return m; } static T positiveBinaryPowerOfTen(int index) { ARDUINOJSON_DEFINE_STATIC_ARRAY( // uint32_t, factors, ARDUINOJSON_EXPAND18({ 0x40240000, 0x00000000, // 1e1 0x40590000, 0x00000000, // 1e2 0x40C38800, 0x00000000, // 1e4 0x4197D784, 0x00000000, // 1e8 0x4341C379, 0x37E08000, // 1e16 0x4693B8B5, 0xB5056E17, // 1e32 0x4D384F03, 0xE93FF9F5, // 1e64 0x5A827748, 0xF9301D32, // 1e128 0x75154FDD, 0x7F73BF3C // 1e256 })); return forge( ARDUINOJSON_READ_STATIC_ARRAY(uint32_t, factors, 2 * index), ARDUINOJSON_READ_STATIC_ARRAY(uint32_t, factors, 2 * index + 1)); } static T negativeBinaryPowerOfTen(int index) { ARDUINOJSON_DEFINE_STATIC_ARRAY( // uint32_t, factors, ARDUINOJSON_EXPAND18({ 0x3FB99999, 0x9999999A, // 1e-1 0x3F847AE1, 0x47AE147B, // 1e-2 0x3F1A36E2, 0xEB1C432D, // 1e-4 0x3E45798E, 0xE2308C3A, // 1e-8 0x3C9CD2B2, 0x97D889BC, // 1e-16 0x3949F623, 0xD5A8A733, // 1e-32 0x32A50FFD, 0x44F4A73D, // 1e-64 0x255BBA08, 0xCF8C979D, // 1e-128 0x0AC80628, 0x64AC6F43 // 1e-256 })); return forge( ARDUINOJSON_READ_STATIC_ARRAY(uint32_t, factors, 2 * index), ARDUINOJSON_READ_STATIC_ARRAY(uint32_t, factors, 2 * index + 1)); } static T negativeBinaryPowerOfTenPlusOne(int index) { ARDUINOJSON_DEFINE_STATIC_ARRAY( // uint32_t, factors, ARDUINOJSON_EXPAND18({ 0x3FF00000, 0x00000000, // 1e0 0x3FB99999, 0x9999999A, // 1e-1 0x3F50624D, 0xD2F1A9FC, // 1e-3 0x3E7AD7F2, 0x9ABCAF48, // 1e-7 0x3CD203AF, 0x9EE75616, // 1e-15 0x398039D6, 0x65896880, // 1e-31 0x32DA53FC, 0x9631D10D, // 1e-63 0x25915445, 0x81B7DEC2, // 1e-127 0x0AFE07B2, 0x7DD78B14 // 1e-255 })); return forge( ARDUINOJSON_READ_STATIC_ARRAY(uint32_t, factors, 2 * index), ARDUINOJSON_READ_STATIC_ARRAY(uint32_t, factors, 2 * index + 1)); } static T nan() { return forge(0x7ff80000, 0x00000000); } static T inf() { return forge(0x7ff00000, 0x00000000); } static T highest() { return forge(0x7FEFFFFF, 0xFFFFFFFF); } static T lowest() { return forge(0xFFEFFFFF, 0xFFFFFFFF); } static T forge(uint32_t msb, uint32_t lsb) { return alias_cast((uint64_t(msb) << 32) | lsb); } }; template struct FloatTraits { typedef uint32_t mantissa_type; static const short mantissa_bits = 23; static const mantissa_type mantissa_max = (mantissa_type(1) << mantissa_bits) - 1; typedef int8_t exponent_type; static const exponent_type exponent_max = 38; template static T make_float(T m, TExponent e) { if (e > 0) { for (uint8_t index = 0; e != 0; index++) { if (e & 1) m *= positiveBinaryPowerOfTen(index); e >>= 1; } } else { e = -e; for (uint8_t index = 0; e != 0; index++) { if (e & 1) m *= negativeBinaryPowerOfTen(index); e >>= 1; } } return m; } static T positiveBinaryPowerOfTen(int index) { ARDUINOJSON_DEFINE_STATIC_ARRAY( T, factors, ARDUINOJSON_EXPAND6({1e1f, 1e2f, 1e4f, 1e8f, 1e16f, 1e32f})); return ARDUINOJSON_READ_STATIC_ARRAY(T, factors, index); } static T negativeBinaryPowerOfTen(int index) { ARDUINOJSON_DEFINE_STATIC_ARRAY( T, factors, ARDUINOJSON_EXPAND6({1e-1f, 1e-2f, 1e-4f, 1e-8f, 1e-16f, 1e-32f})); return ARDUINOJSON_READ_STATIC_ARRAY(T, factors, index); } static T negativeBinaryPowerOfTenPlusOne(int index) { ARDUINOJSON_DEFINE_STATIC_ARRAY( T, factors, ARDUINOJSON_EXPAND6({1e0f, 1e-1f, 1e-3f, 1e-7f, 1e-15f, 1e-31f})); return ARDUINOJSON_READ_STATIC_ARRAY(T, factors, index); } static T forge(uint32_t bits) { return alias_cast(bits); } static T nan() { return forge(0x7fc00000); } static T inf() { return forge(0x7f800000); } static T highest() { return forge(0x7f7fffff); } static T lowest() { return forge(0xFf7fffff); } }; #ifndef isdigit inline bool isdigit(char c) { return '0' <= c && c <= '9'; } #endif inline bool issign(char c) { return '-' == c || c == '+'; } template struct choose_largest : conditional<(sizeof(A) > sizeof(B)), A, B> {}; inline bool parseNumber(const char* s, VariantData& result) { typedef FloatTraits traits; typedef choose_largest::type mantissa_t; typedef traits::exponent_type exponent_t; ARDUINOJSON_ASSERT(s != 0); bool is_negative = false; switch (*s) { case '-': is_negative = true; s++; break; case '+': s++; break; } #if ARDUINOJSON_ENABLE_NAN if (*s == 'n' || *s == 'N') { result.setFloat(traits::nan()); return true; } #endif #if ARDUINOJSON_ENABLE_INFINITY if (*s == 'i' || *s == 'I') { result.setFloat(is_negative ? -traits::inf() : traits::inf()); return true; } #endif if (!isdigit(*s) && *s != '.') return false; mantissa_t mantissa = 0; exponent_t exponent_offset = 0; const mantissa_t maxUint = UInt(-1); while (isdigit(*s)) { uint8_t digit = uint8_t(*s - '0'); if (mantissa > maxUint / 10) break; mantissa *= 10; if (mantissa > maxUint - digit) break; mantissa += digit; s++; } if (*s == '\0') { if (is_negative) { const mantissa_t sintMantissaMax = mantissa_t(1) << (sizeof(Integer) * 8 - 1); if (mantissa <= sintMantissaMax) { result.setInteger(Integer(~mantissa + 1)); return true; } } else { result.setInteger(UInt(mantissa)); return true; } } while (mantissa > traits::mantissa_max) { mantissa /= 10; exponent_offset++; } while (isdigit(*s)) { exponent_offset++; s++; } if (*s == '.') { s++; while (isdigit(*s)) { if (mantissa < traits::mantissa_max / 10) { mantissa = mantissa * 10 + uint8_t(*s - '0'); exponent_offset--; } s++; } } int exponent = 0; if (*s == 'e' || *s == 'E') { s++; bool negative_exponent = false; if (*s == '-') { negative_exponent = true; s++; } else if (*s == '+') { s++; } while (isdigit(*s)) { exponent = exponent * 10 + (*s - '0'); if (exponent + exponent_offset > traits::exponent_max) { if (negative_exponent) result.setFloat(is_negative ? -0.0f : 0.0f); else result.setFloat(is_negative ? -traits::inf() : traits::inf()); return true; } s++; } if (negative_exponent) exponent = -exponent; } exponent += exponent_offset; if (*s != '\0') return false; Float final_result = traits::make_float(static_cast(mantissa), exponent); result.setFloat(is_negative ? -final_result : final_result); return true; } template inline T parseNumber(const char* s) { VariantData value; value.init(); // VariantData is a POD, so it has no constructor parseNumber(s, value); return Converter::fromJson(VariantConstRef(&value)); } template inline T VariantData::asIntegral() const { switch (type()) { case VALUE_IS_BOOLEAN: return _content.asBoolean; case VALUE_IS_UNSIGNED_INTEGER: return convertNumber(_content.asUnsignedInteger); case VALUE_IS_SIGNED_INTEGER: return convertNumber(_content.asSignedInteger); case VALUE_IS_LINKED_STRING: case VALUE_IS_OWNED_STRING: return parseNumber(_content.asString); case VALUE_IS_FLOAT: return convertNumber(_content.asFloat); default: return 0; } } inline bool VariantData::asBoolean() const { switch (type()) { case VALUE_IS_BOOLEAN: return _content.asBoolean; case VALUE_IS_SIGNED_INTEGER: case VALUE_IS_UNSIGNED_INTEGER: return _content.asUnsignedInteger != 0; case VALUE_IS_FLOAT: return _content.asFloat != 0; case VALUE_IS_NULL: return false; default: return true; } } template inline T VariantData::asFloat() const { switch (type()) { case VALUE_IS_BOOLEAN: return static_cast(_content.asBoolean); case VALUE_IS_UNSIGNED_INTEGER: return static_cast(_content.asUnsignedInteger); case VALUE_IS_SIGNED_INTEGER: return static_cast(_content.asSignedInteger); case VALUE_IS_LINKED_STRING: case VALUE_IS_OWNED_STRING: return parseNumber(_content.asString); case VALUE_IS_FLOAT: return static_cast(_content.asFloat); default: return 0; } } inline const char *VariantData::asString() const { switch (type()) { case VALUE_IS_LINKED_STRING: case VALUE_IS_OWNED_STRING: return _content.asString; default: return 0; } } template inline typename enable_if::value, ArrayRef>::type VariantRef::to() const { return ArrayRef(_pool, variantToArray(_data)); } template typename enable_if::value, ObjectRef>::type VariantRef::to() const { return ObjectRef(_pool, variantToObject(_data)); } template typename enable_if::value, VariantRef>::type VariantRef::to() const { variantSetNull(_data); return *this; } inline VariantConstRef VariantConstRef::getElement(size_t index) const { return ArrayConstRef(_data != 0 ? _data->asArray() : 0)[index]; } inline VariantRef VariantRef::addElement() const { return VariantRef(_pool, variantAddElement(_data, _pool)); } inline VariantRef VariantRef::getElement(size_t index) const { return VariantRef(_pool, _data != 0 ? _data->getElement(index) : 0); } inline VariantRef VariantRef::getOrAddElement(size_t index) const { return VariantRef(_pool, variantGetOrAddElement(_data, index, _pool)); } template inline VariantRef VariantRef::getMember(TChar *key) const { return VariantRef(_pool, _data != 0 ? _data->getMember(adaptString(key)) : 0); } template inline typename enable_if::value, VariantRef>::type VariantRef::getMember(const TString &key) const { return VariantRef(_pool, _data != 0 ? _data->getMember(adaptString(key)) : 0); } template inline VariantRef VariantRef::getOrAddMember(TChar *key) const { return VariantRef(_pool, variantGetOrAddMember(_data, key, _pool)); } template inline VariantRef VariantRef::getOrAddMember(const TString &key) const { return VariantRef(_pool, variantGetOrAddMember(_data, key, _pool)); } inline VariantConstRef operator|(VariantConstRef preferedValue, VariantConstRef defaultValue) { return preferedValue ? preferedValue : defaultValue; } inline bool VariantRef::set(char value) const { return set(value); } } // namespace ARDUINOJSON_NAMESPACE #if ARDUINOJSON_ENABLE_STD_STREAM #include #endif namespace ARDUINOJSON_NAMESPACE { class DeserializationError { typedef void (DeserializationError::*bool_type)() const; void safeBoolHelper() const {} public: enum Code { Ok, EmptyInput, IncompleteInput, InvalidInput, NoMemory, TooDeep }; DeserializationError() {} DeserializationError(Code c) : _code(c) {} friend bool operator==(const DeserializationError& lhs, const DeserializationError& rhs) { return lhs._code == rhs._code; } friend bool operator!=(const DeserializationError& lhs, const DeserializationError& rhs) { return lhs._code != rhs._code; } friend bool operator==(const DeserializationError& lhs, Code rhs) { return lhs._code == rhs; } friend bool operator==(Code lhs, const DeserializationError& rhs) { return lhs == rhs._code; } friend bool operator!=(const DeserializationError& lhs, Code rhs) { return lhs._code != rhs; } friend bool operator!=(Code lhs, const DeserializationError& rhs) { return lhs != rhs._code; } operator bool_type() const { return _code != Ok ? &DeserializationError::safeBoolHelper : 0; } friend bool operator==(bool value, const DeserializationError& err) { return static_cast(err) == value; } friend bool operator==(const DeserializationError& err, bool value) { return static_cast(err) == value; } friend bool operator!=(bool value, const DeserializationError& err) { return static_cast(err) != value; } friend bool operator!=(const DeserializationError& err, bool value) { return static_cast(err) != value; } Code code() const { return _code; } const char* c_str() const { static const char* messages[] = { "Ok", "EmptyInput", "IncompleteInput", "InvalidInput", "NoMemory", "TooDeep"}; ARDUINOJSON_ASSERT(static_cast(_code) < sizeof(messages) / sizeof(messages[0])); return messages[_code]; } #if ARDUINOJSON_ENABLE_PROGMEM const __FlashStringHelper* f_str() const { ARDUINOJSON_DEFINE_STATIC_ARRAY(char, s0, "Ok"); ARDUINOJSON_DEFINE_STATIC_ARRAY(char, s1, "EmptyInput"); ARDUINOJSON_DEFINE_STATIC_ARRAY(char, s2, "IncompleteInput"); ARDUINOJSON_DEFINE_STATIC_ARRAY(char, s3, "InvalidInput"); ARDUINOJSON_DEFINE_STATIC_ARRAY(char, s4, "NoMemory"); ARDUINOJSON_DEFINE_STATIC_ARRAY(char, s5, "TooDeep"); ARDUINOJSON_DEFINE_STATIC_ARRAY( const char*, messages, ARDUINOJSON_EXPAND6({s0, s1, s2, s3, s4, s5})); return ARDUINOJSON_READ_STATIC_ARRAY(const __FlashStringHelper*, messages, _code); } #endif private: Code _code; }; #if ARDUINOJSON_ENABLE_STD_STREAM inline std::ostream& operator<<(std::ostream& s, const DeserializationError& e) { s << e.c_str(); return s; } inline std::ostream& operator<<(std::ostream& s, DeserializationError::Code c) { s << DeserializationError(c).c_str(); return s; } #endif class Filter { public: explicit Filter(VariantConstRef v) : _variant(v) {} bool allow() const { return _variant; } bool allowArray() const { return _variant == true || _variant.is(); } bool allowObject() const { return _variant == true || _variant.is(); } bool allowValue() const { return _variant == true; } template Filter operator[](const TKey& key) const { if (_variant == true) // "true" means "allow recursively" return *this; else return Filter(_variant[key] | _variant["*"]); } private: VariantConstRef _variant; }; struct AllowAllFilter { bool allow() const { return true; } bool allowArray() const { return true; } bool allowObject() const { return true; } bool allowValue() const { return true; } template AllowAllFilter operator[](const TKey&) const { return AllowAllFilter(); } }; class NestingLimit { public: NestingLimit() : _value(ARDUINOJSON_DEFAULT_NESTING_LIMIT) {} explicit NestingLimit(uint8_t n) : _value(n) {} NestingLimit decrement() const { ARDUINOJSON_ASSERT(_value > 0); return NestingLimit(static_cast(_value - 1)); } bool reached() const { return _value == 0; } private: uint8_t _value; }; template struct Reader { public: Reader(TSource& source) : _source(&source) {} int read() { return _source->read(); } size_t readBytes(char* buffer, size_t length) { return _source->readBytes(buffer, length); } private: TSource* _source; }; template struct BoundedReader { }; template class IteratorReader { TIterator _ptr, _end; public: explicit IteratorReader(TIterator begin, TIterator end) : _ptr(begin), _end(end) {} int read() { if (_ptr < _end) return static_cast(*_ptr++); else return -1; } size_t readBytes(char* buffer, size_t length) { size_t i = 0; while (i < length && _ptr < _end) buffer[i++] = *_ptr++; return i; } }; template struct void_ { typedef void type; }; template struct Reader::type> : IteratorReader { explicit Reader(const TSource& source) : IteratorReader(source.begin(), source.end()) {} }; template struct IsCharOrVoid { static const bool value = is_same::value || is_same::value || is_same::value || is_same::value; }; template struct IsCharOrVoid : IsCharOrVoid {}; template struct Reader::value>::type> { const char* _ptr; public: explicit Reader(const void* ptr) : _ptr(ptr ? reinterpret_cast(ptr) : "") {} int read() { return static_cast(*_ptr++); } size_t readBytes(char* buffer, size_t length) { for (size_t i = 0; i < length; i++) buffer[i] = *_ptr++; return length; } }; template struct BoundedReader::value>::type> : public IteratorReader { public: explicit BoundedReader(const void* ptr, size_t len) : IteratorReader(reinterpret_cast(ptr), reinterpret_cast(ptr) + len) {} }; template struct Reader, void> : Reader { explicit Reader(const ElementProxy& x) : Reader(x.template as()) {} }; template struct Reader, void> : Reader { explicit Reader(const MemberProxy& x) : Reader(x.template as()) {} }; template <> struct Reader : Reader { explicit Reader(VariantRef x) : Reader(x.as()) {} }; template <> struct Reader : Reader { explicit Reader(VariantConstRef x) : Reader(x.as()) {} }; } // namespace ARDUINOJSON_NAMESPACE #if ARDUINOJSON_ENABLE_ARDUINO_STREAM namespace ARDUINOJSON_NAMESPACE { template struct Reader::value>::type> { public: explicit Reader(Stream& stream) : _stream(&stream) {} int read() { char c; return _stream->readBytes(&c, 1) ? static_cast(c) : -1; } size_t readBytes(char* buffer, size_t length) { return _stream->readBytes(buffer, length); } private: Stream* _stream; }; } // namespace ARDUINOJSON_NAMESPACE #endif #if ARDUINOJSON_ENABLE_ARDUINO_STRING namespace ARDUINOJSON_NAMESPACE { template struct Reader::value>::type> : BoundedReader { explicit Reader(const ::String& s) : BoundedReader(s.c_str(), s.length()) {} }; } // namespace ARDUINOJSON_NAMESPACE #endif #if ARDUINOJSON_ENABLE_PROGMEM namespace ARDUINOJSON_NAMESPACE { template <> struct Reader { const char* _ptr; public: explicit Reader(const __FlashStringHelper* ptr) : _ptr(reinterpret_cast(ptr)) {} int read() { return pgm_read_byte(_ptr++); } size_t readBytes(char* buffer, size_t length) { memcpy_P(buffer, _ptr, length); _ptr += length; return length; } }; template <> struct BoundedReader { const char* _ptr; const char* _end; public: explicit BoundedReader(const __FlashStringHelper* ptr, size_t size) : _ptr(reinterpret_cast(ptr)), _end(_ptr + size) {} int read() { if (_ptr < _end) return pgm_read_byte(_ptr++); else return -1; } size_t readBytes(char* buffer, size_t length) { size_t available = static_cast(_end - _ptr); if (available < length) length = available; memcpy_P(buffer, _ptr, length); _ptr += length; return length; } }; } // namespace ARDUINOJSON_NAMESPACE #endif #if ARDUINOJSON_ENABLE_STD_STREAM #include namespace ARDUINOJSON_NAMESPACE { template struct Reader::value>::type> { public: explicit Reader(std::istream& stream) : _stream(&stream) {} int read() { return _stream->get(); } size_t readBytes(char* buffer, size_t length) { _stream->read(buffer, static_cast(length)); return static_cast(_stream->gcount()); } private: std::istream* _stream; }; } // namespace ARDUINOJSON_NAMESPACE #endif namespace ARDUINOJSON_NAMESPACE { class StringCopier { public: StringCopier(MemoryPool& pool) : _pool(&pool) {} void startString() { _pool->getFreeZone(&_ptr, &_capacity); _size = 0; } const char* save() { ARDUINOJSON_ASSERT(_ptr); return _pool->saveStringFromFreeZone(_size); } void append(const char* s) { while (*s) append(*s++); } void append(const char* s, size_t n) { while (n-- > 0) append(*s++); } void append(char c) { if (!_ptr) return; if (_size >= _capacity) { _ptr = 0; _pool->markAsOverflowed(); return; } _ptr[_size++] = c; } bool isValid() { return _ptr != 0; } const char* c_str() { return _ptr; } typedef storage_policies::store_by_copy storage_policy; private: MemoryPool* _pool; char* _ptr; size_t _size, _capacity; }; class StringMover { public: StringMover(char* ptr) : _writePtr(ptr) {} void startString() { _startPtr = _writePtr; } const char* save() const { return _startPtr; } void append(char c) { *_writePtr++ = c; } bool isValid() const { return true; } const char* c_str() const { return _startPtr; } typedef storage_policies::store_by_address storage_policy; private: char* _writePtr; char* _startPtr; }; template StringCopier makeStringStorage(TInput&, MemoryPool& pool) { return StringCopier(pool); } template StringMover makeStringStorage( TChar* input, MemoryPool&, typename enable_if::value>::type* = 0) { return StringMover(reinterpret_cast(input)); } template