diff --git a/.github/workflows/usermods.yml b/.github/workflows/usermods.yml index 24eda32ece..a890c96307 100644 --- a/.github/workflows/usermods.yml +++ b/.github/workflows/usermods.yml @@ -4,6 +4,9 @@ on: pull_request: paths: - usermods/** + push: + paths: + - usermods/** env: FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true @@ -12,28 +15,40 @@ 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 + if: github.event_name == 'pull_request' && 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 + fetch-depth: 0 + - name: Get changed usermod 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 + # Usermods whose directories changed in this PR + changed=$(git diff --name-only ${{ github.event.pull_request.base.sha }} HEAD \ + | grep '^usermods/' | cut -d/ -f2 | sort -u || true) + + # All usermods with a library.json (excluding known-incompatible ones) + all=$(find usermods/ -name library.json \ + | xargs dirname | xargs -n 1 basename \ + | grep -v PWM_fan | grep -v BME68X_v2 | grep -v pixels_dice_tray \ + | sort || true) + + if [ -z "$changed" ] || [ -z "$all" ]; then + echo "usermods=[]" >> $GITHUB_OUTPUT + else + usermods=$(comm -12 <(echo "$all") <(echo "$changed") | jq -R | jq --slurp -c) + echo "usermods=$usermods" >> $GITHUB_OUTPUT + fi 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 + # Skip when no changed usermods were found (e.g. only non-library changes) + if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != github.repository && needs.get_usermod_envs.outputs.usermods != '[]' name: Build Enviornments runs-on: ubuntu-latest needs: get_usermod_envs @@ -74,4 +89,83 @@ jobs: cat platformio_override.ini - name: Build firmware - run: pio run -e ${{ matrix.environment }} + run: pio run -e ${{ matrix.environment }} + + + get_custom_build_envs: + name: Gather Custom Build Environments + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Find usermods with custom build environments + id: custom_envs + run: | + # On PRs: only scan usermods whose directories changed. + # On push: scan all usermods (validates the full set on merge). + if [ "${{ github.event_name }}" = "pull_request" ]; then + changed=$(git diff --name-only ${{ github.event.pull_request.base.sha }} HEAD \ + | grep '^usermods/' | cut -d/ -f2 | sort -u || true) + if [ -z "$changed" ]; then + echo "matrix=[]" >> $GITHUB_OUTPUT + exit 0 + fi + samples=$(for mod in $changed; do + f="usermods/$mod/platformio_override.ini.sample" + [ -f "$f" ] && echo "$f" + done | sort) + else + samples=$(find usermods/ -name "platformio_override.ini.sample" | sort) + fi + + result='[]' + for sample in $samples; do + usermod=$(dirname "$sample" | xargs basename) + envs=$(grep -E '^\[env:[^]]+\]' "$sample" | sed 's/^\[env:\(.*\)\]$/\1/') + for env in $envs; do + result=$(echo "$result" | jq --arg u "$usermod" --arg e "$env" '. + [{usermod: $u, env: $e}]') + done + done + echo "matrix=$(echo "$result" | jq -c '.')" >> $GITHUB_OUTPUT + outputs: + matrix: ${{ steps.custom_envs.outputs.matrix }} + + + build_custom: + name: Build Custom Env (${{ matrix.usermod }} / ${{ matrix.env }}) + runs-on: ubuntu-latest + needs: get_custom_build_envs + if: needs.get_custom_build_envs.outputs.matrix != '[]' + strategy: + fail-fast: false + matrix: + include: ${{ fromJSON(needs.get_custom_build_envs.outputs.matrix) }} + 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.env }}-${{ hashFiles('platformio.ini', 'pio-scripts/output_bins.py') }}-${{ hashFiles('wled00/**', 'usermods/**') }} + restore-keys: pio-${{ runner.os }}-${{ matrix.env }}-${{ 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: Apply custom build environment + run: cp -v "usermods/${{ matrix.usermod }}/platformio_override.ini.sample" platformio_override.ini + - name: Build firmware + run: pio run -e ${{ matrix.env }} diff --git a/platformio_override.ini b/platformio_override.ini new file mode 100644 index 0000000000..ac85b5d5a9 --- /dev/null +++ b/platformio_override.ini @@ -0,0 +1,11 @@ +[env:esp32s3dev_8MB_opi_dali_gear] +;; ESP32-S3 dev board (8MB Flash, QSPI PSRAM) with dali_gear usermod +extends = env:esp32s3dev_8MB_qspi +custom_usermods = dali_gear +lib_deps = ${env:esp32s3dev_8MB_qspi.lib_deps} + https://github.com/netmindz/DALI-Lighting-Interface.git#fix/esp32-volatile-cast +build_flags = ${common.build_flags} ${esp32s3.build_flags} -D WLED_RELEASE_NAME=\"ESP32-S3_dali_gear\" + -D WLED_WATCHDOG_TIMEOUT=0 + -D ARDUINO_USB_CDC_ON_BOOT=0 ;; use UART0 via built-in JTAG/serial debug unit (303a:1001) + -DBOARD_HAS_PSRAM + -D WLED_DEBUG diff --git a/platformio_override.sample.ini b/platformio_override.sample.ini index 16e73be007..e8787e613f 100644 --- a/platformio_override.sample.ini +++ b/platformio_override.sample.ini @@ -506,42 +506,15 @@ lib_deps = ${esp8266.lib_deps} # ------------------------------------------------------------------------------ # EleksTube-IPS +# See usermods/EleksTube_IPS/platformio_override.ini.sample # ------------------------------------------------------------------------------ -[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 +# 433MHz RF remote example: see usermods/usermod_v2_RF433/platformio_override.ini.sample # External usermod from a git repository. # The library's `library.json` must include `"build": {"libArchive": false}`. diff --git a/usermods/AHT10_v2/platformio_override.ini b/usermods/AHT10_v2/platformio_override.ini deleted file mode 100644 index 74dcd659bb..0000000000 --- a/usermods/AHT10_v2/platformio_override.ini +++ /dev/null @@ -1,5 +0,0 @@ -[env:aht10_example] -extends = env:esp32dev -build_flags = - ${common.build_flags} ${esp32.build_flags} - ; -D USERMOD_AHT10_DEBUG ; -- add a debug status to the info modal diff --git a/usermods/AHT10_v2/platformio_override.ini.sample b/usermods/AHT10_v2/platformio_override.ini.sample new file mode 100644 index 0000000000..993b99ce39 --- /dev/null +++ b/usermods/AHT10_v2/platformio_override.ini.sample @@ -0,0 +1,9 @@ +# AHT10/AHT15/AHT20 temperature/humidity usermod build environment +# Copy to platformio_override.ini in the WLED root to use. + +[platformio] +default_envs = aht10_example + +[env:aht10_example] +extends = env:esp32dev +custom_usermods = ${env:esp32dev.custom_usermods} AHT10_v2 diff --git a/usermods/BME280_v2/platformio_override.ini.sample b/usermods/BME280_v2/platformio_override.ini.sample new file mode 100644 index 0000000000..12fa389e59 --- /dev/null +++ b/usermods/BME280_v2/platformio_override.ini.sample @@ -0,0 +1,9 @@ +# BME280_v2 usermod build environment +# Copy to platformio_override.ini in the WLED root to use. + +[platformio] +default_envs = usermod_bme280_esp8266_2m + +[env:usermod_esp8266_2m] +extends = env:esp8266_2m +custom_usermods = ${env:esp8266_2m.custom_usermods} BME280_v2 diff --git a/usermods/DHT/platformio_override.ini b/usermods/DHT/platformio_override.ini.sample similarity index 84% rename from usermods/DHT/platformio_override.ini rename to usermods/DHT/platformio_override.ini.sample index 6ec2fb9992..e78137b774 100644 --- a/usermods/DHT/platformio_override.ini +++ b/usermods/DHT/platformio_override.ini.sample @@ -8,10 +8,10 @@ ; USERMOD_DHT_MQTT - publish measurements to the MQTT broker ; USERMOD_DHT_STATS - For debug, report delay stats -[env:d1_mini_usermod_dht_C] -extends = env:d1_mini -custom_usermods = ${env:d1_mini.custom_usermods} DHT -build_flags = ${env:d1_mini.build_flags} -D USERMOD_DHT_CELSIUS +[env:esp8266_2m_usermod_dht_C] +extends = env:esp8266_2m +custom_usermods = ${env:esp8266_2m.custom_usermods} DHT +build_flags = ${env:esp8266_2m.build_flags} -D USERMOD_DHT_CELSIUS [env:custom32_LEDPIN_16_usermod_dht_C] extends = env:custom32_LEDPIN_16 diff --git a/usermods/EleksTube_IPS/platformio_override.ini.sample b/usermods/EleksTube_IPS/platformio_override.ini.sample new file mode 100644 index 0000000000..5e761ce5f8 --- /dev/null +++ b/usermods/EleksTube_IPS/platformio_override.ini.sample @@ -0,0 +1,32 @@ +# EleksTube IPS clock build environment +# Copy to platformio_override.ini in the WLED root to use. +# +# Note: usermods/EleksTube_IPS/library.json is currently disabled. +# To enable custom_usermods support, rename library.json.disabled to library.json. + +[platformio] +default_envs = 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 diff --git a/usermods/INA226_v2/platformio_override.ini b/usermods/INA226_v2/platformio_override.ini deleted file mode 100644 index 9968cbf721..0000000000 --- a/usermods/INA226_v2/platformio_override.ini +++ /dev/null @@ -1,6 +0,0 @@ -[env:ina226_example] -extends = env:esp32dev -custom_usermods = ${env:esp32dev.custom_usermods} INA226_v2 -build_flags = - ${env:esp32dev.build_flags} - ; -D USERMOD_INA226_DEBUG ; -- add a debug status to the info modal diff --git a/usermods/INA226_v2/platformio_override.ini.sample b/usermods/INA226_v2/platformio_override.ini.sample new file mode 100644 index 0000000000..b735ac5048 --- /dev/null +++ b/usermods/INA226_v2/platformio_override.ini.sample @@ -0,0 +1,23 @@ +# INA226 power monitor usermod build environments +# Copy to platformio_override.ini in the WLED root to use. + +[platformio] +default_envs = ina226_example + +# Minimal example — enable the usermod with default settings +[env:ina226_example] +extends = env:esp32dev +custom_usermods = ${env:esp32dev.custom_usermods} INA226_v2 +build_flags = ${env:esp32dev.build_flags} + ; -D USERMOD_INA226_DEBUG ; uncomment to add debug status to the info modal + +# Custom calibration example — adjust constants to match your hardware +[env:ina226_custom] +extends = env:esp32dev +custom_usermods = ${env:esp32dev.custom_usermods} INA226_v2 +build_flags = ${env:esp32dev.build_flags} + -D INA226_ENABLED_DEFAULT=true + -D INA226_SHUNT_MICRO_OHMS=2888 + -D INA226_DEFAULT_CURRENT_RANGE=10000 + -D INA226_CURRENT_OFFSET_MA=-118 + -D INA226_CHECK_INTERVAL_MS=1000 diff --git a/usermods/LD2410_v2/platformio_override.ini.sample b/usermods/LD2410_v2/platformio_override.ini.sample new file mode 100644 index 0000000000..407b14dea2 --- /dev/null +++ b/usermods/LD2410_v2/platformio_override.ini.sample @@ -0,0 +1,9 @@ +# LD2410 presence sensor usermod build environment +# Copy to platformio_override.ini in the WLED root to use. + +[platformio] +default_envs = usermod_LD2410_v2_esp32dev + +[env:usermod_LD2410_v2_esp32dev] +extends = env:esp32dev +custom_usermods = ${env:esp32dev.custom_usermods} LD2410_v2 diff --git a/usermods/LDR_Dusk_Dawn_v2/platformio_override.ini.sample b/usermods/LDR_Dusk_Dawn_v2/platformio_override.ini.sample new file mode 100644 index 0000000000..d12d73d8dc --- /dev/null +++ b/usermods/LDR_Dusk_Dawn_v2/platformio_override.ini.sample @@ -0,0 +1,10 @@ +# LDR Dusk/Dawn usermod build environment +# Copy to platformio_override.ini in the WLED root to use. + +[platformio] +default_envs = usermod_LDR_Dusk_Dawn_esp32dev + +[env:usermod_LDR_Dusk_Dawn_esp32dev] +extends = env:esp32dev +custom_usermods = ${env:esp32dev.custom_usermods} + LDR_Dusk_Dawn diff --git a/usermods/MAX17048_v2/platformio_override.ini.sample b/usermods/MAX17048_v2/platformio_override.ini.sample new file mode 100644 index 0000000000..dd339bf758 --- /dev/null +++ b/usermods/MAX17048_v2/platformio_override.ini.sample @@ -0,0 +1,9 @@ +# MAX17048 battery fuel gauge usermod build environment +# Copy to platformio_override.ini in the WLED root to use. + +[platformio] +default_envs = usermod_max17048_esp8266_2m + +[env:usermod_max17048_esp8266_2m] +extends = env:esp8266_2m +custom_usermods = ${env:esp8266_2m.custom_usermods} MAX17048_v2 diff --git a/usermods/SN_Photoresistor/platformio_override.ini b/usermods/SN_Photoresistor/platformio_override.ini.sample similarity index 79% rename from usermods/SN_Photoresistor/platformio_override.ini rename to usermods/SN_Photoresistor/platformio_override.ini.sample index 91bc5de2a6..c5e05ed3ee 100644 --- a/usermods/SN_Photoresistor/platformio_override.ini +++ b/usermods/SN_Photoresistor/platformio_override.ini.sample @@ -1,6 +1,5 @@ ; Options ; ------- -; USERMOD_SN_PHOTORESISTOR - define this to have this user mod included wled00\usermods_list.cpp ; USERMOD_SN_PHOTORESISTOR_MEASUREMENT_INTERVAL - the number of milliseconds between measurements, defaults to 60 seconds ; USERMOD_SN_PHOTORESISTOR_FIRST_MEASUREMENT_AT - the number of milliseconds after boot to take first measurement, defaults to 20 seconds ; USERMOD_SN_PHOTORESISTOR_REFERENCE_VOLTAGE - the voltage supplied to the sensor, defaults to 5v @@ -8,9 +7,10 @@ ; USERMOD_SN_PHOTORESISTOR_RESISTOR_VALUE - the resistor size, defaults to 10000.0 (10K hms) ; USERMOD_SN_PHOTORESISTOR_OFFSET_VALUE - the offset value to report on, defaults to 25 ; -[env:usermod_sn_photoresistor_d1_mini] -extends = env:d1_mini +[env:usermod_sn_photoresistor_esp8266_2m] +extends = env:esp8266_2m +custom_usermods = ${env:esp8266_2m.custom_usermods} SN_Photoresistor build_flags = ${common.build_flags_esp8266} - -D USERMOD_SN_PHOTORESISTOR + -D USERMOD_SN_PHOTORESISTOR_MEASUREMENT_INTERVAL=60 lib_deps = ${env.lib_deps} diff --git a/usermods/TTGO-T-Display/platformio_override.ini b/usermods/TTGO-T-Display/platformio_override.ini deleted file mode 100644 index 7e42d9a54a..0000000000 --- a/usermods/TTGO-T-Display/platformio_override.ini +++ /dev/null @@ -1,8 +0,0 @@ -[env:esp32dev] -build_flags = ${common.build_flags_esp32} -; PIN defines - uncomment and change, if needed: -; -D LEDPIN=2 - -D BTNPIN=35 -; -D IRPIN=4 -; -D RLYPIN=12 -; -D RLYMDE=1 diff --git a/usermods/TTGO-T-Display/platformio_override.ini.sample b/usermods/TTGO-T-Display/platformio_override.ini.sample new file mode 100644 index 0000000000..2777b4f21c --- /dev/null +++ b/usermods/TTGO-T-Display/platformio_override.ini.sample @@ -0,0 +1,16 @@ +; TTGO-T-Display usermod build example. +; Note: this usermod has no library.json so custom_usermods is not available. +; The usermod.cpp must be included manually in your build. + +[platformio] +default_envs = ttgo_t_display_example + +[env:ttgo_t_display_example] +extends = env:esp32dev +build_flags = ${env:esp32dev.build_flags} +; PIN defines - uncomment and change, if needed: +; -D LEDPIN=2 + -D BTNPIN=35 +; -D IRPIN=4 +; -D RLYPIN=12 +; -D RLYMDE=1 diff --git a/usermods/Temperature/platformio_override.ini b/usermods/Temperature/platformio_override.ini deleted file mode 100644 index a53b5974d9..0000000000 --- a/usermods/Temperature/platformio_override.ini +++ /dev/null @@ -1,5 +0,0 @@ -; Options -; ------- -; USERMOD_DALLASTEMPERATURE_MEASUREMENT_INTERVAL - the number of milliseconds between measurements, defaults to 60 seconds -; - diff --git a/usermods/Temperature/platformio_override.ini.sample b/usermods/Temperature/platformio_override.ini.sample new file mode 100644 index 0000000000..3ebaeabeca --- /dev/null +++ b/usermods/Temperature/platformio_override.ini.sample @@ -0,0 +1,10 @@ +# Temperature (DS18B20) usermod build environment +# Copy to platformio_override.ini in the WLED root to use. + +[platformio] +default_envs = usermod_temperature_esp32dev + +[env:usermod_temperature_esp32dev] +extends = env:esp32dev +custom_usermods = ${env:esp32dev.custom_usermods} + Temperature diff --git a/usermods/dali_gear/library.json b/usermods/dali_gear/library.json new file mode 100644 index 0000000000..865d94045d --- /dev/null +++ b/usermods/dali_gear/library.json @@ -0,0 +1,7 @@ +{ + "name": "dali_gear", + "build": { "libArchive": false }, + "dependencies": { + "qqqDALI": "https://github.com/netmindz/DALI-Lighting-Interface.git#e39a7da06242010bbb6771532c4ac17b3ec73834" + } +} diff --git a/usermods/dali_gear/readme.md b/usermods/dali_gear/readme.md new file mode 100644 index 0000000000..8daa8ff68b --- /dev/null +++ b/usermods/dali_gear/readme.md @@ -0,0 +1,152 @@ +# DALI Gear Usermod + +Makes WLED act as a **DALI control gear** (IEC 62386) — i.e. a light that responds to commands from an external DALI master (wall dimmer, BMS, building automation system, etc.). + +DALI (Digital Addressable Lighting Interface) is a standardised two-wire bus protocol for lighting control. This usermod puts WLED on the bus as a gear device: the DALI master sends brightness/on/off/colour commands, and WLED adjusts its LEDs accordingly. + +> **ESP32 only.** The hardware timer API used for Manchester decoding is not available on ESP8266. + +## Hardware + +You need a DALI bus interface circuit to convert between the DALI bus voltage (9.5–22.5 V) and the ESP32's 3.3 V GPIO levels. + +### Minimal DIY circuit (from [qqqlab/DALI-Lighting-Interface](https://github.com/qqqlab/DALI-Lighting-Interface)) + +```text +3.3V ESP32 5.6V ___ + Zener +----|___|---- 12V Power Supply + ___ Diode | 220 Ω +RX ---+-----|___|---|>|----------+------------- DALI+ + | 10K | + +-+ | + | | 100K ___ |/ PNP DALI BUS + +-+ TX ---|___|----| Transistor + | 1K |\ + | V +GND --+---------------------------+------------- DALI- +``` + +> **⚠️ Warning:** This is a conceptual schematic for experimentation — it is **not isolated** and exposes the ESP32 GPIO to DALI bus voltages through a resistor divider only. Do not use it in a production installation. The PNP transistor produces a **single inversion**: GPIO HIGH drives the bus LOW (asserted). Enable **TX Inverted** in the usermod settings when using this circuit. + +Commercial DALI interface modules with proper isolation (e.g. Waveshare Pico-DALI2, Mikroe DALI Click) are strongly recommended for any real installation. + +### Pin assignment + +| Signal | Direction | Description | +|---|---|---| +| RX | Input | Reads DALI bus state (high = bus idle, low = bus asserted) | +| TX | Output | Drives DALI bus — needed for backward frame responses to QUERY commands | + +Default pins are **RX=14, TX=17** (Waveshare Pico-DALI2). + +Configure both pins in the WLED usermod settings page. + +## Configuration + +| Setting | Default | Description | +|---|---|---| +| Enabled | false | Enable/disable the usermod | +| pin_rx | 14 | GPIO for DALI bus RX | +| pin_tx | 17 | GPIO for DALI bus TX | +| tx_inverted | false | Invert TX polarity. Enable for single-stage inverting circuits (e.g. DIY PNP). Leave off for Waveshare Pico-DALI2 and other NPN+opto-isolated boards. | +| daliAddr | -1 | Short address (0–63) to respond to, or -1 to respond to broadcast only | + +## DALI commands handled + +### Direct Arc Power Control (DAPC) + +When the master sends a DAPC frame, the arc level (0–254) is mapped linearly to WLED brightness (0–255). WLED's existing gamma correction handles perceptual uniformity at the LED output. + +| DALI arc level | WLED behaviour | +|---|---| +| 0 | Turn off | +| 1–254 | Set brightness proportionally, turn on | +| 255 (mask) | Ignored (no change) | + +### Indirect commands + +| Command | Number | WLED action | +|---|---|---| +| OFF | 0 | Turn off | +| UP | 1 | Increase brightness by 10 | +| DOWN | 2 | Decrease brightness by 10 | +| STEP UP | 3 | Increase brightness by 1 | +| STEP DOWN | 4 | Decrease brightness by 1 | +| RECALL MAX LEVEL | 5 | Set brightness to 255, turn on | +| RECALL MIN LEVEL | 6 | Set brightness to 1, turn on | +| STEP DOWN AND OFF | 7 | Decrease by 1; turn off if at minimum | +| ON AND STEP UP | 8 | Turn on if off, then increase by 10 | +| GO TO LAST ACTIVE LEVEL | 10 | Restore last brightness before turn-off | + +### Query commands (backward frame responses) + +These allow a DALI master to detect gear presence and read basic status. Responses are sent as DALI backward frames 4 ms after the query, within the IEC 62386-102 required window of 7Te–22Te (≈2.9–9.2 ms). + +| Command | Byte | Response | +|---|---|---| +| QUERY STATUS | 0x90 | Status byte: bit 2 = lamp on, bit 6 = no short address | +| QUERY CONTROL GEAR PRESENT | 0x91 | `0xFF` (Yes, I am here) | +| QUERY DEVICE TYPE | 0x18 | `0x08` (device type 8 = colour control) | +| QUERY ACTUAL LEVEL | 0xA0 | Current arc level (0–254) derived from WLED brightness | + +### DT8 colour temperature (IEC 62386-209) + +Colour temperature commands from a DALI master are mapped to WLED's CCT value via `strip.setCCT()`. The mired value is converted to Kelvin (`K = 1,000,000 / mireds`). WLED's accepted range is 1900–10091 K; values outside this range are clamped. + +Two CCT application flows are supported: + +**Standard flow (IEC 62386-209 §11.3.4.1):** + +1. `SET DTR0` — lower byte of colour temperature in mireds +2. `SET DTR1` — upper byte of colour temperature in mireds +3. `ENABLE DEVICE TYPE 8` — activates DT8 interpretation +4. `SET TEMPORARY COLOUR TEMPERATURE` (0xE1) — loads DTR0+DTR1 into temporary register +5. `ACTIVATE` (0xE2) — applies the temporary colour temperature + +**Non-standard combined flow (observed in some masters):** + +Some DALI masters skip the `0xE1` + `0xE2` sequence and instead apply the colour temperature implicitly alongside the subsequent DAPC command. The usermod detects this: if DTR0/DTR1 are set and DT8 is active when a DAPC frame arrives, the CCT is applied at the same time as the brightness change. + +**QUERY COLOUR TYPE (0xF7):** + +Some masters query the gear's colour capabilities before sending CCT commands. Per IEC 62386-209 §11.3.4.2, this command is `0xF7`. The usermod responds with `0x02` (bit 1 = Tc colour temperature supported), sent 4 ms after the query frame to meet the DALI spec requirement (7Te–22Te ≈ 2.9–9.2 ms settling window). Command `0xE7` does not generate a backward frame response (it is not a query command per the spec). + +`SET DTR0`, `SET DTR1`, and `ENABLE DEVICE TYPE 8` are sniffed as broadcast-level frames regardless of the configured `daliAddr`. + +### CCT on RGB-only strips + +`strip.setCCT()` adjusts the colour temperature via WLED's internal CCT pipeline. For an RGB-only strip (no dedicated white or CCT channel), **White Balance Correction** must be enabled in WLED LED settings (Config → LED Preferences → White Balance Correction) for the CCT value to affect the LED output. This makes WLED apply a colour temperature correction on the RGB channels. + +## Addressing + +DALI addressing works as follows: + +- **Broadcast** (`0xFE`/`0xFF`): always handled regardless of `daliAddr` setting +- **Short address** (0–63): set `daliAddr` to the address the DALI master has assigned to this device +- **Group address**: not handled + +Set `daliAddr` to `-1` (default) to respond only to broadcast commands. This is useful for a single-gear installation. + +## Enabling the usermod + +Add to your `platformio_override.ini`: + +```ini +[env:esp32dev] +custom_usermods = dali_gear +lib_deps = + ${env.lib_deps} + https://github.com/netmindz/DALI-Lighting-Interface.git#fix/esp32-volatile-cast +``` + +## Limitations + +- No short address commissioning via DALI bus (set the address manually in WLED config) +- No group address support +- No DALI scene mapping + +## Dependencies + +- [qqqlab/DALI-Lighting-Interface](https://github.com/qqqlab/DALI-Lighting-Interface) (GPL-3.0) + Low-level Manchester-encoded DALI bus driver by qqqlab. + This usermod uses the fork at `https://github.com/netmindz/DALI-Lighting-Interface.git#fix/esp32-volatile-cast` which includes an ESP32 volatile-cast fix. diff --git a/usermods/dali_gear/usermod_dali_gear.cpp b/usermods/dali_gear/usermod_dali_gear.cpp new file mode 100644 index 0000000000..81e04d54b1 --- /dev/null +++ b/usermods/dali_gear/usermod_dali_gear.cpp @@ -0,0 +1,521 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// +// DALI Gear Usermod for WLED +// Makes WLED act as a DALI control gear (IEC 62386) — i.e. a light that +// responds to commands from an external DALI master (wall dimmer, BMS, etc.). +// +// Phase 1: RX-only bus listening. Handles DAPC (direct arc power control) +// and basic indirect commands (OFF, MAX, MIN, UP, DOWN, LAST ACTIVE LEVEL). +// Phase 2: DT8 colour temperature (IEC 62386-209). Handles SET DTR0/DTR1 +// special commands, ENABLE DEVICE TYPE 8, SET TEMPORARY COLOUR TEMPERATURE, +// ACTIVATE to map DALI Tc (mireds) → WLED CCT (Kelvin), and backward frame +// responses to QUERY STATUS (0x90), QUERY CONTROL GEAR PRESENT (0x91), +// QUERY DEVICE TYPE (0x18), QUERY ACTUAL LEVEL (0xA0), +// and QUERY COLOUR TYPE (0xF7 per IEC 62386-209). +// +// Hardware: requires a DALI bus interface circuit (see readme.md). +// ESP32 only — uses hardware timer API not available on ESP8266. + +#include "wled.h" + +#ifndef ARDUINO_ARCH_ESP32 +#error "dali_gear usermod requires ESP32 (hardware timer API not available on ESP8266)" +#endif + +#include + +// --------------------------------------------------------------------------- +// DALI frame parsing helpers +// --------------------------------------------------------------------------- + +// Returns true if the address byte of a forward frame is addressed to us. +// daliAddr: our configured short address (0–63), or -1 to accept broadcast only. +static bool daliAddressedToMe(uint8_t addrByte, int8_t daliAddr) { + // Broadcast: 1111 111x (0xFE or 0xFF) + if ((addrByte | 0x01) == 0xFF) return true; + // Short address: 0AAA AAA x — top bit 0 + if (!(addrByte & 0x80) && daliAddr >= 0) { + uint8_t frameAddr = (addrByte >> 1) & 0x3F; + return frameAddr == (uint8_t)daliAddr; + } + // Group address: 100A AAA x — not handled + return false; +} + +// Map a DALI arc level (1–254) to WLED bri (1–255). +// Linear mapping is correct here: WLED's gamma correction handles the LED +// output curve, serving the same perceptual-uniformity purpose as DALI's +// logarithmic arc power table. +static uint8_t daliLevelToWledBri(uint8_t level) { + if (level == 0) return 0; + // level 1–254 → bri 1–255 + return (uint8_t)(((uint16_t)level * 255u + 127u) / 254u); +} + +// Map WLED bri (1–255) back to a DALI arc level (1–254), for QUERY ACTUAL LEVEL. +static uint8_t wledBriToDaliLevel(uint8_t b) { + if (b == 0) return 0; + return (uint8_t)(((uint16_t)b * 254u + 127u) / 255u); +} + +// --------------------------------------------------------------------------- +// ISR and timer — file-scope so the ISR can reach the Dali instance +// --------------------------------------------------------------------------- + +static Dali _dali; +static hw_timer_t *_daliTimer = nullptr; + +static void ARDUINO_ISR_ATTR daliTimerISR() { + _dali.timer(); +} + +// --------------------------------------------------------------------------- +// Usermod class +// --------------------------------------------------------------------------- + +class DaliGearUsermod : public Usermod { + private: + bool _enabled = false; + bool _initDone = false; + int8_t _rxPin = 14; // default: Waveshare Pico-DALI2 RX + int8_t _txPin = 17; // default: Waveshare Pico-DALI2 TX + bool _txInverted = false; // true for circuits with a single-stage inverting TX driver + // (e.g. qqqDALI DIY PNP circuit). + // false (default) for the Waveshare Pico-DALI2 and other + // boards with double-inversion (NPN + opto-isolator). + int8_t _daliAddr = -1; // -1 = respond to broadcast only + uint8_t _lastDaliLevel = 0; // last DALI arc level received (for info panel) + + // DT8 (IEC 62386-209) colour temperature state + uint8_t _dtr0 = 0; // Data Transfer Register 0 (low byte of Tc mireds) + uint8_t _dtr1 = 0; // Data Transfer Register 1 (high byte of Tc mireds) + bool _dt8Active = false; // true after ENABLE DEVICE TYPE 8 + uint16_t _tempCCT = 0; // temporary colour temperature register (mireds) + uint16_t _lastCCTKelvin = 0; // last applied CCT in Kelvin (for info panel) + + // Backward frame scheduling — DALI requires response 7Te–22Te (≈2.9–9.2ms) + // after the forward frame stop bits. We schedule via timestamp. + uint8_t _pendingBF = 0; // backward frame byte to send (0 = none pending) + uint32_t _pendingBFTime = 0; // millis() threshold — send when now >= this + + static const char _name[]; + static const char _enabled_key[]; + + // --------------------------------------------------------------------------- + // Bus HAL callbacks (static so they can be passed as function pointers). + // TX polarity depends on interface hardware: + // _txInverted = false (default): GPIO HIGH = bus idle, GPIO LOW = assert bus. + // Used by Waveshare Pico-DALI2 (NPN + opto-isolator = double inversion). + // _txInverted = true: GPIO LOW = bus idle, GPIO HIGH = assert bus. + // Used by the qqqDALI DIY PNP circuit (single inversion via PNP transistor). + // --------------------------------------------------------------------------- + static uint8_t busIsHigh() { + return digitalRead(_rxPinStatic); + } + static void busSetLow() { + // "set bus low" = assert the DALI bus + digitalWrite(_txPinStatic, _txInvertedStatic ? HIGH : LOW); + } + static void busSetHigh() { + // "set bus high" = release the DALI bus (idle) + digitalWrite(_txPinStatic, _txInvertedStatic ? LOW : HIGH); + } + + // Static copies of pins/config needed by the HAL callbacks + static int8_t _rxPinStatic; + static int8_t _txPinStatic; + static bool _txInvertedStatic; + + // --------------------------------------------------------------------------- + // Schedule a DALI backward frame to be sent after the mandatory settling time. + // DALI IEC 62386-102 requires 7Te (≈2.9ms) min, 22Te (≈9.2ms) max. + // We target 4ms — safely inside the window even with loop jitter. + // --------------------------------------------------------------------------- + void scheduleBF(uint8_t byte) { + _pendingBF = byte; + _pendingBFTime = millis() + 4; // 4ms after frame received in loop() + } + + // --------------------------------------------------------------------------- + // Apply a DALI arc level to WLED + // --------------------------------------------------------------------------- + void applyLevel(uint8_t daliLevel) { + _lastDaliLevel = daliLevel; + if (daliLevel == 0) { + briLast = bri ? bri : briLast; // preserve last brightness for toggle + bri = 0; + } else { + bri = daliLevelToWledBri(daliLevel); + } + stateUpdated(CALL_MODE_DIRECT_CHANGE); + } + + // --------------------------------------------------------------------------- + // Apply a colour temperature in mireds to WLED via strip.setCCT(Kelvin) + // --------------------------------------------------------------------------- + void applyCCT(uint16_t mireds) { + if (mireds == 0) return; // 0 mireds is undefined / mask value — ignore + // Convert mireds to Kelvin. Clamp to WLED's accepted range (1900–10091 K). + uint32_t kelvin = 1000000UL / mireds; + if (kelvin < 1900) kelvin = 1900; + if (kelvin > 10091) kelvin = 10091; + _lastCCTKelvin = (uint16_t)kelvin; + strip.setCCT(_lastCCTKelvin); + stateUpdated(CALL_MODE_DIRECT_CHANGE); + DEBUG_PRINTF("[DALI] CCT applied: %u mireds → %u K\n", mireds, (unsigned)kelvin); + } + + // --------------------------------------------------------------------------- + // Handle an indirect DALI command (S=1 in address byte) + // --------------------------------------------------------------------------- + void handleCommand(uint8_t cmd) { + switch (cmd) { + case DALI_OFF: + applyLevel(0); + break; + case DALI_UP: + if (bri > 0) { + bri = (bri > 245) ? 255 : bri + 10; + stateUpdated(CALL_MODE_DIRECT_CHANGE); + } + break; + case DALI_DOWN: + if (bri > 10) bri -= 10; + else if (bri > 0) bri = 1; + stateUpdated(CALL_MODE_DIRECT_CHANGE); + break; + case DALI_RECALL_MAX_LEVEL: + bri = 255; + stateUpdated(CALL_MODE_DIRECT_CHANGE); + break; + case DALI_RECALL_MIN_LEVEL: + bri = 1; + stateUpdated(CALL_MODE_DIRECT_CHANGE); + break; + case DALI_GO_TO_LAST_ACTIVE_LEVEL: + bri = briLast ? briLast : 128; + stateUpdated(CALL_MODE_DIRECT_CHANGE); + break; + case DALI_ON_AND_STEP_UP: + if (bri == 0) bri = 1; + else bri = (bri > 245) ? 255 : bri + 10; + stateUpdated(CALL_MODE_DIRECT_CHANGE); + break; + case DALI_STEP_UP: + if (bri < 255) bri++; + stateUpdated(CALL_MODE_DIRECT_CHANGE); + break; + case DALI_STEP_DOWN: + if (bri > 1) bri--; + stateUpdated(CALL_MODE_DIRECT_CHANGE); + break; + case DALI_STEP_DOWN_AND_OFF: + if (bri <= 1) bri = 0; + else bri--; + stateUpdated(CALL_MODE_DIRECT_CHANGE); + break; + + // IEC 62386-102 §11.2 query commands — backward frame responses. + // These allow a DALI master to detect gear presence and read basic status + // before sending DT8 or other application commands. + + case 0x90: { + // QUERY STATUS — respond with status byte. + // Bit 2 = lamp arc power on (1 if bri > 0). + // Bit 6 = missing short address (1 if no address configured). + // All other status/fault bits = 0 (no failures to report). + uint8_t status = ((bri > 0) ? 0x04u : 0x00u) + | ((_daliAddr < 0) ? 0x40u : 0x00u); + DEBUG_PRINTF("[DALI] QUERY STATUS → 0x%02x\n", status); + scheduleBF(status); + break; + } + + case 0x91: + // QUERY CONTROL GEAR PRESENT — respond 0xFF ("Yes"). + // Many masters send this first to detect whether any gear is on the bus; + // silence here causes the master to skip all subsequent commands. + DEBUG_PRINTLN(F("[DALI] QUERY CONTROL GEAR PRESENT → 0xFF")); + scheduleBF(0xFF); + break; + + case 0x18: + // QUERY DEVICE TYPE — respond 0x08 (device type 8 = colour control, IEC 62386-209). + // Conformant DALI-2 masters send this before issuing ENABLE DEVICE TYPE 8 or any + // DT8 application extended commands. Silence causes such masters to skip CCT control. + DEBUG_PRINTLN(F("[DALI] QUERY DEVICE TYPE → 0x08")); + scheduleBF(0x08); + break; + + case 0xA0: + // QUERY ACTUAL LEVEL — respond with current arc level (0–254). + // Derived from the current WLED brightness so it stays accurate even if + // bri was changed via the WLED UI rather than a DALI command. + DEBUG_PRINTF("[DALI] QUERY ACTUAL LEVEL → %u\n", wledBriToDaliLevel(bri)); + scheduleBF(wledBriToDaliLevel(bri)); + break; + + // DT8 (IEC 62386-209) application extended commands. + // These are only valid when preceded by ENABLE DEVICE TYPE 8 (addr=0xC1, cmd=8). + // 0xE1 = SET TEMPORARY COLOUR TEMPERATURE — loads DTR0+DTR1 into temp register. + // 0xE2 = ACTIVATE — applies the temporary colour temperature. + // 0xF7 = QUERY COLOUR TYPE (IEC 62386-209 §11.3.4.2) — master asks which DT8 + // colour modes are supported. Response bitmask: + // bit 0 = XY colour, bit 1 = Tc colour temperature, + // bit 2 = Primary N, bit 3 = RGBWAF. We support Tc only → 0x02. + // Note: some non-standard masters send this as 0xE7 instead. Both are + // handled here to maximise interoperability. + case 0xE1: + if (_dt8Active) { + _tempCCT = ((uint16_t)_dtr1 << 8) | _dtr0; + DEBUG_PRINTF("[DALI] SET TEMPORARY COLOUR TEMPERATURE: %u mireds (DTR1=0x%02x DTR0=0x%02x)\n", + _tempCCT, _dtr1, _dtr0); + } else { + DEBUG_PRINTLN(F("[DALI] SET TEMPORARY COLOUR TEMPERATURE received but DT8 not active — ignored")); + } + break; + case 0xE2: + if (_dt8Active && _tempCCT > 0) { + DEBUG_PRINTF("[DALI] ACTIVATE: applying %u mireds\n", _tempCCT); + applyCCT(_tempCCT); + } else { + DEBUG_PRINTF("[DALI] ACTIVATE: skipped (dt8Active=%d tempCCT=%u)\n", _dt8Active, _tempCCT); + } + _dt8Active = false; + break; + + case 0xE7: + // 0xE7 is not QUERY COLOUR TYPE per IEC 62386-209 — do not respond. + // (QUERY COLOUR TYPE is 0xF7; some non-standard masters mistakenly use + // 0xE7, but sending a backward frame here would violate the spec.) + DEBUG_PRINTLN(F("[DALI] cmd 0xE7 (not a query — no response)")); + break; + + case 0xF7: // QUERY COLOUR TYPE — IEC 62386-209 §11.3.4.2 + // Respond regardless of _dt8Active state; master needs to know our + // capabilities before it will send ENABLE DEVICE TYPE 8. + // Response bitmask: bit 1 = Tc colour temperature supported → 0x02. + DEBUG_PRINTLN(F("[DALI] QUERY COLOUR TYPE (0xF7) → scheduling backward frame 0x02 (Tc supported)")); + scheduleBF(0x02); + break; + + default: + DEBUG_PRINTF("[DALI] unhandled command 0x%02x (%u) — ignored\n", cmd, cmd); + break; + } + } + + public: + + void setup() override { + if (!_enabled || _rxPin < 0 || _txPin < 0) { + _initDone = true; + return; + } + + // Claim pins via WLED pin manager + if (!PinManager::allocatePin(_rxPin, false, PinOwner::UM_DALI_GEAR)) { + DEBUG_PRINTLN(F("[DALI] RX pin allocation failed")); + _enabled = false; + _initDone = true; + return; + } + if (!PinManager::allocatePin(_txPin, true, PinOwner::UM_DALI_GEAR)) { + DEBUG_PRINTLN(F("[DALI] TX pin allocation failed")); + PinManager::deallocatePin(_rxPin, PinOwner::UM_DALI_GEAR); + _enabled = false; + _initDone = true; + return; + } + + // Configure GPIO + pinMode(_rxPin, INPUT); + pinMode(_txPin, OUTPUT); + // Idle state: bus not asserted. Polarity depends on interface circuit. + digitalWrite(_txPin, _txInverted ? LOW : HIGH); + + // Store static copies for HAL callbacks + _rxPinStatic = _rxPin; + _txPinStatic = _txPin; + _txInvertedStatic = _txInverted; + + _dali.begin(busIsHigh, busSetLow, busSetHigh); + + // Hardware timer: IDF v4 API + // Timer 1 (timer 0 is used by SparkFunDMX), prescaler 80 → 1 MHz tick. + // Alarm at 104 ticks → ~9615 Hz ≈ 1200 baud × 8 oversample. + _daliTimer = timerBegin(1, 80, true); + timerAttachInterrupt(_daliTimer, &daliTimerISR, true); + timerAlarmWrite(_daliTimer, 104, true); + timerAlarmEnable(_daliTimer); + + DEBUG_PRINTF("[DALI] Gear usermod initialised (RX=%d TX=%d txInv=%d addr=%d)\n", + _rxPin, _txPin, (int)_txInverted, _daliAddr); + _initDone = true; + } + + + void loop() override { + if (!_enabled || !_initDone || _rxPin < 0) return; + + // Send any pending backward frame once the settling window opens (≥7Te ≈ 2.9ms). + if (_pendingBF && (millis() >= _pendingBFTime)) { + uint8_t bf = _pendingBF; + _pendingBF = 0; + uint8_t result = _dali.tx(&bf, 8); + DEBUG_PRINTF("[DALI] backward frame 0x%02x sent (tx result=%u)\n", bf, result); + } + + uint8_t data[4]; + uint8_t bits = _dali.rx(data); + + if (bits == 0) return; // nothing received + + // A DALI forward frame is exactly 16 bits (2 bytes). + // 1-bit returns are normal bus-idle sampling noise from the library — discard silently. + // Log only genuinely unexpected lengths (partial frames: 3–15 bits). + if (bits != 16) { + if (bits > 2) { + DEBUG_PRINTF("[DALI] partial frame: %u bits (data: 0x%02x 0x%02x 0x%02x 0x%02x)\n", + bits, data[0], data[1], data[2], data[3]); + } + return; + } + + uint8_t addrByte = data[0]; + uint8_t cmdByte = data[1]; + + DEBUG_PRINTF("[DALI] raw frame: addr=0x%02x cmd=0x%02x\n", addrByte, cmdByte); + + // Sniff special broadcast commands that are NOT gear-addressed. + // These must be processed regardless of our _daliAddr setting. + // 0xA3 xx — SET DTR0 (Data Transfer Register 0) = xx + // 0xC3 xx — SET DTR1 (Data Transfer Register 1) = xx + // 0xC1 08 — ENABLE DEVICE TYPE 8 + if (addrByte == 0xA3) { + _dtr0 = cmdByte; + DEBUG_PRINTF("[DALI] SET DTR0 = 0x%02x (%u)\n", cmdByte, cmdByte); + return; + } + if (addrByte == 0xC3) { + _dtr1 = cmdByte; + DEBUG_PRINTF("[DALI] SET DTR1 = 0x%02x (%u)\n", cmdByte, cmdByte); + return; + } + if (addrByte == 0xC1) { + if (cmdByte == 8) { + _dt8Active = true; + DEBUG_PRINTLN(F("[DALI] ENABLE DEVICE TYPE 8")); + } else { + DEBUG_PRINTF("[DALI] ENABLE DEVICE TYPE %u (not handled)\n", cmdByte); + } + return; + } + + if (!daliAddressedToMe(addrByte, _daliAddr)) { + DEBUG_PRINTF("[DALI] frame not for us: addr=0x%02x (our addr=%d) — ignored\n", + addrByte, _daliAddr); + return; + } + + bool isDapc = !(addrByte & 0x01); // S bit = 0 → DAPC + + if (isDapc) { + if (cmdByte == 255) { + DEBUG_PRINTLN(F("[DALI] DAPC 255 (mask) — ignored")); + } else { + DEBUG_PRINTF("[DALI] DAPC level=%u → bri=%u\n", cmdByte, daliLevelToWledBri(cmdByte)); + applyLevel(cmdByte); + // Some masters use a non-standard combined flow: DTR0/DTR1 set the colour + // temperature, ENABLE DEVICE TYPE 8 arms it, and the subsequent DAPC applies + // both brightness and CCT in one go (without 0xE1+0xE2). + if (_dt8Active && (_dtr1 || _dtr0)) { + uint16_t mireds = ((uint16_t)_dtr1 << 8) | _dtr0; + applyCCT(mireds); + } + _dt8Active = false; + } + } else { + DEBUG_PRINTF("[DALI] command 0x%02x (%u)\n", cmdByte, cmdByte); + handleCommand(cmdByte); + } + } + + + void addToJsonInfo(JsonObject& root) override { + if (!_initDone) return; + JsonObject user = root["u"]; + if (user.isNull()) user = root.createNestedObject("u"); + + JsonArray arr = user.createNestedArray(FPSTR(_name)); + if (!_enabled) { + arr.add(F("disabled")); + return; + } + if (_rxPin < 0 || _txPin < 0) { + arr.add(F("pins not configured")); + return; + } + arr.add(_lastDaliLevel); + arr.add(F(" DALI level")); + if (_lastCCTKelvin > 0) { + JsonArray cctArr = user.createNestedArray(F("DALIGear CCT")); + cctArr.add(_lastCCTKelvin); + cctArr.add(F(" K")); + } + } + + + void addToConfig(JsonObject& root) override { + JsonObject top = root.createNestedObject(FPSTR(_name)); + top[FPSTR(_enabled_key)] = _enabled; + top["pin_rx"] = _rxPin; + top["pin_tx"] = _txPin; + top["tx_inverted"] = _txInverted; + top["daliAddr"] = _daliAddr; + } + + + bool readFromConfig(JsonObject& root) override { + JsonObject top = root[FPSTR(_name)]; + bool configComplete = !top.isNull(); + + configComplete &= getJsonValue(top[FPSTR(_enabled_key)], _enabled, false); + configComplete &= getJsonValue(top["pin_rx"], _rxPin, (int8_t)14); + configComplete &= getJsonValue(top["pin_tx"], _txPin, (int8_t)17); + configComplete &= getJsonValue(top["tx_inverted"], _txInverted, false); + configComplete &= getJsonValue(top["daliAddr"], _daliAddr, (int8_t)-1); + + return configComplete; + } + + + void appendConfigData() override { + oappend(F("addInfo('")); + oappend(String(FPSTR(_name)).c_str()); + oappend(F(":pin_rx',1,'DALI RX pin');")); + oappend(F("addInfo('")); + oappend(String(FPSTR(_name)).c_str()); + oappend(F(":pin_tx',1,'DALI TX pin');")); + oappend(F("addInfo('")); + oappend(String(FPSTR(_name)).c_str()); + oappend(F(":tx_inverted',1,'Invert TX — enable for single-stage inverting circuits (e.g. DIY PNP). Leave off for Waveshare Pico-DALI2 and NPN+opto boards.');")); + oappend(F("addInfo('")); + oappend(String(FPSTR(_name)).c_str()); + oappend(F(":daliAddr',1,'Short address (0\u201363) or -1 for broadcast only');")); + } + + + uint16_t getId() override { return USERMOD_ID_DALI_GEAR; } +}; + +// Static member definitions +int8_t DaliGearUsermod::_rxPinStatic = -1; +int8_t DaliGearUsermod::_txPinStatic = -1; +bool DaliGearUsermod::_txInvertedStatic = false; + +const char DaliGearUsermod::_name[] PROGMEM = "DALIGear"; +const char DaliGearUsermod::_enabled_key[] PROGMEM = "enabled"; + +static DaliGearUsermod dali_gear_usermod; +REGISTER_USERMOD(dali_gear_usermod); diff --git a/usermods/mpu6050_imu/platformio_override.ini.sample b/usermods/mpu6050_imu/platformio_override.ini.sample new file mode 100644 index 0000000000..cc5a591f63 --- /dev/null +++ b/usermods/mpu6050_imu/platformio_override.ini.sample @@ -0,0 +1,10 @@ +# MPU-6050 IMU usermod build environment +# Copy to platformio_override.ini in the WLED root to use. + +[platformio] +default_envs = usermod_mpu6050_imu_esp32dev + +[env:usermod_mpu6050_imu_esp32dev] +extends = env:esp32dev +custom_usermods = ${env:esp32dev.custom_usermods} + mpu6050_imu diff --git a/usermods/pixels_dice_tray/platformio_override.ini.sample b/usermods/pixels_dice_tray/platformio_override.ini.sample index 6b4fa7768e..f51c2190b3 100644 --- a/usermods/pixels_dice_tray/platformio_override.ini.sample +++ b/usermods/pixels_dice_tray/platformio_override.ini.sample @@ -13,7 +13,7 @@ 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 +build_flags = ${common.build_flags} ${esp32s3.build_flags} -D WLED_RELEASE_NAME=\"T-QT-PRO-8MB_dice\" -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") @@ -75,7 +75,7 @@ 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 +build_flags = ${common.build_flags} ${esp32s3.build_flags} -D WLED_RELEASE_NAME=\"ESP32-S3_8MB_qspi_dice\" -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") @@ -105,7 +105,7 @@ lib_deps = ${esp32s3.lib_deps} # 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 +; build_flags = ${common.build_flags} ${esp32.build_flags} -D WLED_RELEASE_NAME=\"ESP32_dice\" ; ; Enable Pixels dice mod ; -D USERMOD_PIXELS_DICE_TRAY ; lib_deps = ${esp32.lib_deps} diff --git a/usermods/sht/platformio_override.ini.sample b/usermods/sht/platformio_override.ini.sample new file mode 100644 index 0000000000..65e3ffd153 --- /dev/null +++ b/usermods/sht/platformio_override.ini.sample @@ -0,0 +1,13 @@ +# SHT temperature/humidity sensor usermod build environments +# Copy to platformio_override.ini in the WLED root to use. + +[platformio] +default_envs = custom_esp32dev_usermod_sht + +[env:custom_esp32dev_usermod_sht] +extends = env:esp32dev +custom_usermods = ${env:esp32dev.custom_usermods} sht + +[env:custom_esp8266_2m_usermod_sht] +extends = env:esp8266_2m +custom_usermods = ${env:esp8266_2m.custom_usermods} sht diff --git a/usermods/usermod_v2_RF433/platformio_override.ini.sample b/usermods/usermod_v2_RF433/platformio_override.ini.sample new file mode 100644 index 0000000000..69c2d04c46 --- /dev/null +++ b/usermods/usermod_v2_RF433/platformio_override.ini.sample @@ -0,0 +1,11 @@ +# RF433 remote usermod build environment +# Copy to platformio_override.ini in the WLED root to use. + +[platformio] +default_envs = esp32dev_usermod_RF433 + +[env:esp32dev_usermod_RF433] +extends = env:esp32dev +custom_usermods = + ${env:esp32dev.custom_usermods} + RF433 diff --git a/usermods/usermod_v2_four_line_display_ALT/platformio_override.sample.ini b/usermods/usermod_v2_four_line_display_ALT/platformio_override.ini.sample similarity index 50% rename from usermods/usermod_v2_four_line_display_ALT/platformio_override.sample.ini rename to usermods/usermod_v2_four_line_display_ALT/platformio_override.ini.sample index f4fa8c9d8b..ef0e00848f 100644 --- a/usermods/usermod_v2_four_line_display_ALT/platformio_override.sample.ini +++ b/usermods/usermod_v2_four_line_display_ALT/platformio_override.ini.sample @@ -2,10 +2,10 @@ default_envs = esp32dev_fld [env:esp32dev_fld] -extends = env:esp32dev_V4 -custom_usermods = ${env:esp32dev_V4.custom_usermods} four_line_display_ALT +extends = env:esp32dev +custom_usermods = ${env:esp32dev.custom_usermods} four_line_display_ALT build_flags = - ${env:esp32dev_V4.build_flags} + ${env:esp32dev.build_flags} -D FLD_TYPE=SH1106 -D I2CSCLPIN=27 -D I2CSDAPIN=26 diff --git a/usermods/usermod_v2_klipper_percentage/platformio_override.ini.sample b/usermods/usermod_v2_klipper_percentage/platformio_override.ini.sample new file mode 100644 index 0000000000..5239a51ae7 --- /dev/null +++ b/usermods/usermod_v2_klipper_percentage/platformio_override.ini.sample @@ -0,0 +1,10 @@ +# Klipper percentage usermod build environment +# Copy to platformio_override.ini in the WLED root to use. + +[platformio] +default_envs = esp32klipper + +[env:esp32klipper] +extends = env:esp32dev +custom_usermods = ${env:esp32dev.custom_usermods} + usermod_v2_klipper_percentage diff --git a/usermods/usermod_v2_rotary_encoder_ui_ALT/platformio_override.sample.ini b/usermods/usermod_v2_rotary_encoder_ui_ALT/platformio_override.ini.sample similarity index 58% rename from usermods/usermod_v2_rotary_encoder_ui_ALT/platformio_override.sample.ini rename to usermods/usermod_v2_rotary_encoder_ui_ALT/platformio_override.ini.sample index 2511d2fa38..c943bdbe25 100644 --- a/usermods/usermod_v2_rotary_encoder_ui_ALT/platformio_override.sample.ini +++ b/usermods/usermod_v2_rotary_encoder_ui_ALT/platformio_override.ini.sample @@ -2,10 +2,10 @@ default_envs = esp32dev_re [env:esp32dev_re] -extends = env:esp32dev_V4 -custom_usermods = ${env:esp32dev_V4.custom_usermods} rotary_encoder_ui_ALT +extends = env:esp32dev +custom_usermods = ${env:esp32dev.custom_usermods} rotary_encoder_ui_ALT build_flags = - ${env:esp32dev_V4.build_flags} + ${env:esp32dev.build_flags} -D USERMOD_ROTARY_ENCODER_GPIO=INPUT -D ENCODER_DT_PIN=21 -D ENCODER_CLK_PIN=23 diff --git a/wled00/const.h b/wled00/const.h index 70373316fd..97885d26c9 100644 --- a/wled00/const.h +++ b/wled00/const.h @@ -230,6 +230,7 @@ static_assert(WLED_MAX_BUSSES <= 32, "WLED_MAX_BUSSES exceeds hard limit"); #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" +#define USERMOD_ID_DALI_GEAR 59 //Usermod "usermod_dali_gear.cpp" //Wifi encryption type #ifdef WLED_ENABLE_WPA_ENTERPRISE diff --git a/wled00/pin_manager.h b/wled00/pin_manager.h index 7bdd5cfc20..3eb6ebc0d6 100644 --- a/wled00/pin_manager.h +++ b/wled00/pin_manager.h @@ -76,7 +76,8 @@ enum struct PinOwner : uint8_t { 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. + 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. + UM_DALI_GEAR = USERMOD_ID_DALI_GEAR // 0x3B // Usermod "usermod_dali_gear.cpp" }; static_assert(0u == static_cast(PinOwner::None), "PinOwner::None must be zero, so default array initialization works as expected");