diff --git a/.github/ISSUE_TEMPLATE/bug.yml b/.github/ISSUE_TEMPLATE/bug.yml index 285ad419e..6f010aa60 100644 --- a/.github/ISSUE_TEMPLATE/bug.yml +++ b/.github/ISSUE_TEMPLATE/bug.yml @@ -80,7 +80,7 @@ body: id: terms attributes: label: Code of Conduct - description: By submitting this issue, you agree to follow our [Code of Conduct](https://github.com/Aircoookie/WLED/blob/master/CODE_OF_CONDUCT.md) + 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 diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 3de08ff7b..6f2c373ad 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -26,7 +26,7 @@ jobs: build: - name: Build Enviornments + name: Build Environments runs-on: ubuntu-latest needs: get_default_envs strategy: diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index a5c80f22d..818ab94e1 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -39,3 +39,9 @@ jobs: 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 }} diff --git a/.github/workflows/pr-merge.yaml b/.github/workflows/pr-merge.yaml new file mode 100644 index 000000000..9ef843dd4 --- /dev/null +++ b/.github/workflows/pr-merge.yaml @@ -0,0 +1,16 @@ + name: Notify Discord on PR Merge + on: + pull_request: + types: [closed] + + jobs: + notify: + runs-on: ubuntu-latest + steps: + - name: Send Discord notification + shell: bash + env: + DISCORD_WEBHOOK_BETA_TESTERS: ${{ secrets.DISCORD_WEBHOOK_BETA_TESTERS }} + if: github.event.pull_request.merged == true + run: | + curl -H "Content-Type: application/json" -d '{"content": "Pull Request #{{ github.event.pull_request.number }} merged by {{ github.actor }}"}' $DISCORD_WEBHOOK_BETA_TESTERS diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 27beec99c..dfcb808de 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -18,9 +18,16 @@ jobs: - uses: actions/download-artifact@v4 with: merge-multiple: true + - name: "✏️ Generate release changelog" + id: changelog + uses: janheinrichmerker/action-github-changelog-generator@v2.3 + with: + token: ${{ secrets.GITHUB_TOKEN }} + sinceTag: v0.15.0 - name: Create draft release uses: softprops/action-gh-release@v1 with: + body: ${{ steps.changelog.outputs.changelog }} draft: True files: | *.bin diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml new file mode 100644 index 000000000..a9b7aa9b6 --- /dev/null +++ b/.github/workflows/test.yaml @@ -0,0 +1,13 @@ +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 }} diff --git a/.gitignore b/.gitignore index 8f083e3f6..51d321d92 100644 --- a/.gitignore +++ b/.gitignore @@ -15,6 +15,7 @@ wled-update.sh /build_output/ /node_modules/ +/logs/ /wled00/extLibs /wled00/LittleFS diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 670b5561d..e2078df71 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -27,7 +27,7 @@ Github will pick up the changes so your PR stays up-to-date. > For example, we regularly lost review comments when the PR author force-pushes code changes. So, pretty please, do not force-push. -You can find a collection of very useful tips and tricks here: https://github.com/Aircoookie/WLED/wiki/How-to-properly-submit-a-PR +You can find a collection of very useful tips and tricks here: https://github.com/wled-dev/WLED/wiki/How-to-properly-submit-a-PR ### Code style diff --git a/package-lock.json b/package-lock.json index 0afeeaafd..b5e14158d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,27 +1,28 @@ { "name": "wled", - "version": "0.16.0-dev", + "version": "0.16.0-alpha", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "wled", - "version": "0.16.0-dev", + "version": "0.16.0-alpha", "license": "ISC", "dependencies": { "clean-css": "^5.3.3", "html-minifier-terser": "^7.2.0", - "inliner": "^1.13.1", - "nodemon": "^3.1.7" + "nodemon": "^3.1.9", + "web-resource-inliner": "^7.0.0" }, "engines": { "node": ">=20.0.0" } }, "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", - "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz", + "integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==", + "license": "MIT", "dependencies": { "@jridgewell/set-array": "^1.2.1", "@jridgewell/sourcemap-codec": "^1.4.10", @@ -35,6 +36,7 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "license": "MIT", "engines": { "node": ">=6.0.0" } @@ -43,6 +45,7 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", + "license": "MIT", "engines": { "node": ">=6.0.0" } @@ -51,6 +54,7 @@ "version": "0.3.6", "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.6.tgz", "integrity": "sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==", + "license": "MIT", "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25" @@ -59,21 +63,24 @@ "node_modules/@jridgewell/sourcemap-codec": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", - "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==" + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", + "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { "version": "0.3.25", "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "license": "MIT", "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "node_modules/acorn": { - "version": "8.12.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz", - "integrity": "sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==", + "version": "8.14.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz", + "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==", + "license": "MIT", "bin": { "acorn": "bin/acorn" }, @@ -81,62 +88,20 @@ "node": ">=0.4.0" } }, - "node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/align-text": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/align-text/-/align-text-0.1.4.tgz", - "integrity": "sha512-GrTZLRpmp6wIC2ztrWW9MjjTgSKccffgFagbNDOX95/dcjEcYZibYTeaOntySQLcdw1ztBoFkviiUvTMbb9MYg==", - "dependencies": { - "kind-of": "^3.0.2", - "longest": "^1.0.1", - "repeat-string": "^1.5.2" - }, + "node_modules/ansi-colors": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", + "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", + "license": "MIT", "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/ansi-escapes": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-1.4.0.tgz", - "integrity": "sha512-wiXutNjDUlNEDWHcYH3jtZUhd3c4/VojassD8zHdHCY13xbZy2XbW+NKQwA0tWGBVzDA9qEzYwfoSsWmviidhw==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/ansi-regex": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/ansi-styles": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", - "integrity": "sha512-kmCevFghRiWM7HB5zTPULl4r9bVFSWjz62MhqizDGUrq2NWuNMQyuv4tHHoKJHs69M/MF64lEcHdYIocrdWQYA==", - "engines": { - "node": ">=0.10.0" + "node": ">=6" } }, "node_modules/anymatch": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "license": "ISC", "dependencies": { "normalize-path": "^3.0.0", "picomatch": "^2.0.4" @@ -145,70 +110,17 @@ "node": ">= 8" } }, - "node_modules/argparse": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", - "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", - "dependencies": { - "sprintf-js": "~1.0.2" - } - }, - "node_modules/asap": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", - "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==" - }, - "node_modules/asn1": { - "version": "0.2.6", - "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz", - "integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==", - "dependencies": { - "safer-buffer": "~2.1.0" - } - }, - "node_modules/assert-plus": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", - "integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==", - "engines": { - "node": ">=0.8" - } - }, - "node_modules/asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" - }, - "node_modules/aws-sign2": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", - "integrity": "sha512-08kcGqnYf/YmjoRhfxyu+CLxBjUtHLXLXX/vUfx9l2LYzG3c1m61nrpyFUZI6zeS+Li/wWMMidD9KgrqtGq3mA==", - "engines": { - "node": "*" - } - }, - "node_modules/aws4": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.13.2.tgz", - "integrity": "sha512-lHe62zvbTB5eEABUVi/AwVh0ZKY9rMMDhmm+eeyuuUQbQ3+J+fONVQOZyj+DdrvD4BY33uYniyRJ4UJIaSKAfw==" - }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" - }, - "node_modules/bcrypt-pbkdf": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", - "integrity": "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==", - "dependencies": { - "tweetnacl": "^0.14.3" - } + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "license": "MIT" }, "node_modules/binary-extensions": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "license": "MIT", "engines": { "node": ">=8" }, @@ -216,15 +128,11 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/boolbase": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", - "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==" - }, "node_modules/brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -234,6 +142,7 @@ "version": "3.0.3", "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "license": "MIT", "dependencies": { "fill-range": "^7.1.1" }, @@ -244,89 +153,24 @@ "node_modules/buffer-from": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", - "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==" + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "license": "MIT" }, "node_modules/camel-case": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/camel-case/-/camel-case-4.1.2.tgz", "integrity": "sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw==", + "license": "MIT", "dependencies": { "pascal-case": "^3.1.2", "tslib": "^2.0.3" } }, - "node_modules/camelcase": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-1.2.1.tgz", - "integrity": "sha512-wzLkDa4K/mzI1OSITC+DUyjgIl/ETNHE9QvYgy6J6Jvqyyz4C0Xfd+lQhb19sX2jMpZV4IssUn0VDVmglV+s4g==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/caseless": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", - "integrity": "sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==" - }, - "node_modules/center-align": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/center-align/-/center-align-0.1.3.tgz", - "integrity": "sha512-Baz3aNe2gd2LP2qk5U+sDk/m4oSuwSDcBfayTCTBoWpfIGO5XFxPmjILQII4NGiZjD6DoDI6kf7gKaxkf7s3VQ==", - "dependencies": { - "align-text": "^0.1.3", - "lazy-cache": "^1.0.3" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/chalk": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", - "integrity": "sha512-U3lRVLMSlsCfjqYPbLyVv11M9CPW4I728d6TCKMAOJueEeB9/8o+eSsMnxPJD+Q+K909sdESg7C+tIkoH6on1A==", - "dependencies": { - "ansi-styles": "^2.2.1", - "escape-string-regexp": "^1.0.2", - "has-ansi": "^2.0.0", - "strip-ansi": "^3.0.0", - "supports-color": "^2.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/charset": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/charset/-/charset-1.0.1.tgz", - "integrity": "sha512-6dVyOOYjpfFcL1Y4qChrAoQLRHvj2ziyhcm0QJlhOcAhykL/k1kTUPbeo+87MNRTRdk2OIIsIXbuF3x2wi5EXg==", - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/cheerio": { - "version": "0.19.0", - "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-0.19.0.tgz", - "integrity": "sha512-Fwcm3zkR37STnPC8FepSHeSYJM5Rd596TZOcfDUdojR4Q735aK1Xn+M+ISagNneuCwMjK28w4kX+ETILGNT/UQ==", - "dependencies": { - "css-select": "~1.0.0", - "dom-serializer": "~0.1.0", - "entities": "~1.1.1", - "htmlparser2": "~3.8.1", - "lodash": "^3.2.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/cheerio/node_modules/entities": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/entities/-/entities-1.1.2.tgz", - "integrity": "sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w==" - }, "node_modules/chokidar": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "license": "MIT", "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", @@ -346,21 +190,11 @@ "fsevents": "~2.3.2" } }, - "node_modules/clap": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/clap/-/clap-1.2.3.tgz", - "integrity": "sha512-4CoL/A3hf90V3VIEjeuhSvlGFEHKzOz+Wfc2IVZc+FaUgU0ZQafJTP49fvnULipOPcAfqhyI2duwQyns6xqjYA==", - "dependencies": { - "chalk": "^1.1.3" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/clean-css": { "version": "5.3.3", "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-5.3.3.tgz", "integrity": "sha512-D5J+kHaVb/wKSFcyyV75uCn8fiY4sV38XJoe4CUyGQ+mOU/fMVYUdH1hJC+CJQ5uY3EnW27SbJYS4X8BiLrAFg==", + "license": "MIT", "dependencies": { "source-map": "~0.6.0" }, @@ -368,50 +202,11 @@ "node": ">= 10.0" } }, - "node_modules/cliui": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-2.1.0.tgz", - "integrity": "sha512-GIOYRizG+TGoc7Wgc1LiOTLare95R3mzKgoln+Q/lE4ceiYH19gUpl0l0Ffq4lJDEf3FxujMe6IBfOCs7pfqNA==", - "dependencies": { - "center-align": "^0.1.1", - "right-align": "^0.1.1", - "wordwrap": "0.0.2" - } - }, - "node_modules/coa": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/coa/-/coa-1.0.4.tgz", - "integrity": "sha512-KAGck/eNAmCL0dcT3BiuYwLbExK6lduR8DxM3C1TyDzaXhZHyZ8ooX5I5+na2e3dPFuibfxrGdorr0/Lr7RYCQ==", - "dependencies": { - "q": "^1.1.2" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/colors": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/colors/-/colors-1.1.2.tgz", - "integrity": "sha512-ENwblkFQpqqia6b++zLD/KUWafYlVY/UNnAp7oz7LY7E924wmpye416wBOmvv/HMWzl8gL1kJlfvId/1Dg176w==", - "engines": { - "node": ">=0.1.90" - } - }, - "node_modules/combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "dependencies": { - "delayed-stream": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, "node_modules/commander": { "version": "10.0.1", "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==", + "license": "MIT", "engines": { "node": ">=14" } @@ -419,230 +214,135 @@ "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" - }, - "node_modules/configstore": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/configstore/-/configstore-1.4.0.tgz", - "integrity": "sha512-Zcx2SVdZC06IuRHd2MhkVYFNJBkZBj166LGdsJXRcqNC8Gs5Bwh8mosStNeCBBmtIm4wNii2uarD50qztjKOjw==", - "dependencies": { - "graceful-fs": "^4.1.2", - "mkdirp": "^0.5.0", - "object-assign": "^4.0.1", - "os-tmpdir": "^1.0.0", - "osenv": "^0.1.0", - "uuid": "^2.0.1", - "write-file-atomic": "^1.1.2", - "xdg-basedir": "^2.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/configstore/node_modules/uuid": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-2.0.3.tgz", - "integrity": "sha512-FULf7fayPdpASncVy4DLh3xydlXEJJpvIELjYjNeQWYUZ9pclcpvCZSr2gkmN2FrrGcI7G/cJsIEwk5/8vfXpg==", - "deprecated": "Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details." - }, - "node_modules/core-util-is": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", - "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==" - }, - "node_modules/css-select": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/css-select/-/css-select-1.0.0.tgz", - "integrity": "sha512-/xPlD7betkfd7ChGkLGGWx5HWyiHDOSn7aACLzdH0nwucPvB0EAm8hMBm7Xn7vGfAeRRN7KZ8wumGm8NoNcMRw==", - "dependencies": { - "boolbase": "~1.0.0", - "css-what": "1.0", - "domutils": "1.4", - "nth-check": "~1.0.0" - } - }, - "node_modules/css-what": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/css-what/-/css-what-1.0.0.tgz", - "integrity": "sha512-60SUMPBreXrLXgvpM8kYpO0AOyMRhdRlXFX5BMQbZq1SIJCyNE56nqFQhmvREQdUJpedbGRYZ5wOyq3/F6q5Zw==", - "engines": { - "node": "*" - } - }, - "node_modules/csso": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/csso/-/csso-2.0.0.tgz", - "integrity": "sha512-tckZA0LhyEnToPoQDmncCA+TUS3aoIVl/MsSaoipR52Sfa+H83fJvIHRVOHMFn9zW6kIV1L0D7tUDFFjvN28lg==", - "dependencies": { - "clap": "^1.0.9", - "source-map": "^0.5.3" - }, - "bin": { - "csso": "bin/csso" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/csso/node_modules/source-map": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/dashdash": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", - "integrity": "sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g==", - "dependencies": { - "assert-plus": "^1.0.0" - }, - "engines": { - "node": ">=0.10" - } + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "license": "MIT" }, "node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "license": "MIT", "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/decamelize": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", - "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", + "ms": "^2.1.3" + }, "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/deep-extend": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", - "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "engines": { - "node": ">=0.4.0" + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } } }, "node_modules/dom-serializer": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.1.1.tgz", - "integrity": "sha512-l0IU0pPzLWSHBcieZbpOKgkIn3ts3vAh7ZuFyXNwJxJXk/c4Gwj9xaTJwIDVQCXawWD0qb3IzMGH5rglQaO0XA==", + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.4.1.tgz", + "integrity": "sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==", + "license": "MIT", "dependencies": { - "domelementtype": "^1.3.0", - "entities": "^1.1.1" + "domelementtype": "^2.0.1", + "domhandler": "^4.2.0", + "entities": "^2.0.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/dom-serializer/node_modules/domhandler": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.3.1.tgz", + "integrity": "sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==", + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "^2.2.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" } }, "node_modules/dom-serializer/node_modules/entities": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/entities/-/entities-1.1.2.tgz", - "integrity": "sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w==" + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", + "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==", + "license": "BSD-2-Clause", + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } }, "node_modules/domelementtype": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.3.1.tgz", - "integrity": "sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w==" + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "BSD-2-Clause" }, "node_modules/domhandler": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-2.3.0.tgz", - "integrity": "sha512-q9bUwjfp7Eif8jWxxxPSykdRZAb6GkguBGSgvvCrhI9wB71W2K/Kvv4E61CF/mcCfnVJDeDWx/Vb/uAqbDj6UQ==", + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-3.3.0.tgz", + "integrity": "sha512-J1C5rIANUbuYK+FuFL98650rihynUOEzRLxW+90bKZRWB6A1X1Tf82GxR1qAWLyfNPRvjqfip3Q5tdYlmAa9lA==", + "license": "BSD-2-Clause", "dependencies": { - "domelementtype": "1" + "domelementtype": "^2.0.1" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" } }, "node_modules/domutils": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.4.3.tgz", - "integrity": "sha512-ZkVgS/PpxjyJMb+S2iVHHEZjVnOUtjGp0/zstqKGTE9lrZtNHlNQmLwP/lhLMEApYbzc08BKMx9IFpKhaSbW1w==", + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz", + "integrity": "sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==", + "license": "BSD-2-Clause", "dependencies": { - "domelementtype": "1" + "dom-serializer": "^1.0.1", + "domelementtype": "^2.2.0", + "domhandler": "^4.2.0" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/domutils/node_modules/domhandler": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.3.1.tgz", + "integrity": "sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==", + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "^2.2.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" } }, "node_modules/dot-case": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/dot-case/-/dot-case-3.0.4.tgz", "integrity": "sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==", + "license": "MIT", "dependencies": { "no-case": "^3.0.4", "tslib": "^2.0.3" } }, - "node_modules/duplexify": { - "version": "3.7.1", - "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.7.1.tgz", - "integrity": "sha512-07z8uv2wMyS51kKhD1KsdXJg5WQ6t93RneqRxUHnskXVtlYYkLqM0gqStQZ3pj073g687jPCHrqNfCzawLYh5g==", - "dependencies": { - "end-of-stream": "^1.0.0", - "inherits": "^2.0.1", - "readable-stream": "^2.0.0", - "stream-shift": "^1.0.0" - } - }, - "node_modules/duplexify/node_modules/isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" - }, - "node_modules/duplexify/node_modules/readable-stream": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", - "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "node_modules/duplexify/node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" - }, - "node_modules/duplexify/node_modules/string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dependencies": { - "safe-buffer": "~5.1.0" - } - }, - "node_modules/ecc-jsbn": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", - "integrity": "sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw==", - "dependencies": { - "jsbn": "~0.1.0", - "safer-buffer": "^2.1.0" - } - }, - "node_modules/end-of-stream": { - "version": "1.4.4", - "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", - "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", - "dependencies": { - "once": "^1.4.0" - } - }, "node_modules/entities": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "license": "BSD-2-Clause", "engines": { "node": ">=0.12" }, @@ -650,58 +350,23 @@ "url": "https://github.com/fb55/entities?sponsor=1" } }, - "node_modules/es6-promise": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-2.3.0.tgz", - "integrity": "sha512-oyOjMhyKMLEjOOtvkwg0G4pAzLQ9WdbbeX7WdqKzvYXu+UFgD0Zo/Brq5Q49zNmnGPPzV5rmYvrr0jz1zWx8Iw==" - }, - "node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "node_modules/escape-goat": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/escape-goat/-/escape-goat-3.0.0.tgz", + "integrity": "sha512-w3PwNZJwRxlp47QGzhuEBldEqVHHhh8/tIPcl6ecf2Bou99cdAt0knihBV0Ecc7CGxYduXVBDheH1K2oADRlvw==", + "license": "MIT", "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/esprima": { - "version": "2.7.3", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-2.7.3.tgz", - "integrity": "sha512-OarPfz0lFCiW4/AV2Oy1Rp9qu0iusTKqykwTspGCZtPxmF81JR4MmIebvF1F9+UOKth2ZubLQ4XGGaU+hSn99A==", - "bin": { - "esparse": "bin/esparse.js", - "esvalidate": "bin/esvalidate.js" + "node": ">=10" }, - "engines": { - "node": ">=0.10.0" + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/extend": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", - "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" - }, - "node_modules/extsprintf": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", - "integrity": "sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g==", - "engines": [ - "node >=0.6.0" - ] - }, - "node_modules/fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" - }, - "node_modules/fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==" - }, "node_modules/fill-range": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "license": "MIT", "dependencies": { "to-regex-range": "^5.0.1" }, @@ -709,32 +374,12 @@ "node": ">=8" } }, - "node_modules/forever-agent": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", - "integrity": "sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw==", - "engines": { - "node": "*" - } - }, - "node_modules/form-data": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", - "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.6", - "mime-types": "^2.1.12" - }, - "engines": { - "node": ">= 0.12" - } - }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", "hasInstallScript": true, + "license": "MIT", "optional": true, "os": [ "darwin" @@ -743,18 +388,11 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, - "node_modules/getpass": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", - "integrity": "sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng==", - "dependencies": { - "assert-plus": "^1.0.0" - } - }, "node_modules/glob-parent": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "license": "ISC", "dependencies": { "is-glob": "^4.0.1" }, @@ -762,75 +400,11 @@ "node": ">= 6" } }, - "node_modules/got": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/got/-/got-3.3.1.tgz", - "integrity": "sha512-7chPlc0pWHjvq7B6dEEXz4GphoDupOvBSSl6AwRsAJX7GPTZ+bturaZiIigX4Dp6KrAP67nvzuKkNc0SLA0DKg==", - "dependencies": { - "duplexify": "^3.2.0", - "infinity-agent": "^2.0.0", - "is-redirect": "^1.0.0", - "is-stream": "^1.0.0", - "lowercase-keys": "^1.0.0", - "nested-error-stacks": "^1.0.0", - "object-assign": "^3.0.0", - "prepend-http": "^1.0.0", - "read-all-stream": "^3.0.0", - "timed-out": "^2.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/got/node_modules/object-assign": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-3.0.0.tgz", - "integrity": "sha512-jHP15vXVGeVh1HuaA2wY6lxk+whK/x4KBG88VXeRma7CCun7iGD5qPc4eYykQ9sdQvg8jkwFKsSxHln2ybW3xQ==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/graceful-fs": { - "version": "4.2.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", - "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==" - }, - "node_modules/har-schema": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", - "integrity": "sha512-Oqluz6zhGX8cyRaTQlFMPw80bSJVG2x/cFb8ZPhUILGgHka9SsokCCOQgpveePerqidZOrT14ipqfJb7ILcW5Q==", - "engines": { - "node": ">=4" - } - }, - "node_modules/har-validator": { - "version": "5.1.5", - "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.5.tgz", - "integrity": "sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w==", - "deprecated": "this library is no longer supported", - "dependencies": { - "ajv": "^6.12.3", - "har-schema": "^2.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/has-ansi": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", - "integrity": "sha512-C8vBJ8DwUCx19vhm7urhTuUsr4/IyP6l4VzNQDv+ryHQObW3TTTp9yB68WpYgRe2bbaGuZ/se74IqFeVnMnLZg==", - "dependencies": { - "ansi-regex": "^2.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/has-flag": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "license": "MIT", "engines": { "node": ">=4" } @@ -839,6 +413,7 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/html-minifier-terser/-/html-minifier-terser-7.2.0.tgz", "integrity": "sha512-tXgn3QfqPIpGl9o+K5tpcj3/MN4SfLtsx2GWwBC3SSd0tXQGyF3gsSqad8loJgKZGM3ZxbYDd5yhiBIdWpmvLA==", + "license": "MIT", "dependencies": { "camel-case": "^4.1.2", "clean-css": "~5.3.2", @@ -856,117 +431,40 @@ } }, "node_modules/htmlparser2": { - "version": "3.8.3", - "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-3.8.3.tgz", - "integrity": "sha512-hBxEg3CYXe+rPIua8ETe7tmG3XDn9B0edOE/e9wH2nLczxzgdu0m0aNHY+5wFZiviLWLdANPJTssa92dMcXQ5Q==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-5.0.1.tgz", + "integrity": "sha512-vKZZra6CSe9qsJzh0BjBGXo8dvzNsq/oGvsjfRdOrrryfeD9UOBEEQdeoqCRmKZchF5h2zOBMQ6YuQ0uRUmdbQ==", + "license": "MIT", "dependencies": { - "domelementtype": "1", - "domhandler": "2.3", - "domutils": "1.5", - "entities": "1.0", - "readable-stream": "1.1" - } - }, - "node_modules/htmlparser2/node_modules/domutils": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.5.1.tgz", - "integrity": "sha512-gSu5Oi/I+3wDENBsOWBiRK1eoGxcywYSqg3rR960/+EfY0CF4EX1VPkgHOZ3WiS/Jg2DtliF6BhWcHlfpYUcGw==", - "dependencies": { - "dom-serializer": "0", - "domelementtype": "1" + "domelementtype": "^2.0.1", + "domhandler": "^3.3.0", + "domutils": "^2.4.2", + "entities": "^2.0.0" + }, + "funding": { + "url": "https://github.com/fb55/htmlparser2?sponsor=1" } }, "node_modules/htmlparser2/node_modules/entities": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-1.0.0.tgz", - "integrity": "sha512-LbLqfXgJMmy81t+7c14mnulFHJ170cM6E+0vMXR9k/ZiZwgX8i5pNgjTCX3SO4VeUsFLV+8InixoretwU+MjBQ==" - }, - "node_modules/http-signature": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", - "integrity": "sha512-CAbnr6Rz4CYQkLYUtSNXxQPUH2gK8f3iWexVlsnMeD+GjlsQ0Xsy1cOX+mN3dtxYomRy21CiOzU8Uhw6OwncEQ==", - "dependencies": { - "assert-plus": "^1.0.0", - "jsprim": "^1.2.2", - "sshpk": "^1.7.0" - }, - "engines": { - "node": ">=0.8", - "npm": ">=1.3.7" - } - }, - "node_modules/iconv-lite": { - "version": "0.4.24", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3" - }, - "engines": { - "node": ">=0.10.0" + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", + "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==", + "license": "BSD-2-Clause", + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" } }, "node_modules/ignore-by-default": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", - "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==" - }, - "node_modules/imurmurhash": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", - "engines": { - "node": ">=0.8.19" - } - }, - "node_modules/infinity-agent": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/infinity-agent/-/infinity-agent-2.0.3.tgz", - "integrity": "sha512-CnfUJe5o2S9aAQWXGMhDZI4UL39MAJV3guOTfHHIdos4tuVHkl1j/J+1XLQn+CLIvqcpgQR/p+xXYXzcrhCe5w==" - }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" - }, - "node_modules/ini": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", - "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==" - }, - "node_modules/inliner": { - "version": "1.13.1", - "resolved": "https://registry.npmjs.org/inliner/-/inliner-1.13.1.tgz", - "integrity": "sha512-yoS+56puOu+Ug8FBRtxtTFnEn2NHqFs8BNQgSOvzh3J0ommbwNw8VKiaVNYjWK6fgPuByq95KyV0LC+qV9IwLw==", - "dependencies": { - "ansi-escapes": "^1.4.0", - "ansi-styles": "^2.2.1", - "chalk": "^1.1.3", - "charset": "^1.0.0", - "cheerio": "^0.19.0", - "debug": "^2.2.0", - "es6-promise": "^2.3.0", - "iconv-lite": "^0.4.11", - "jschardet": "^1.3.0", - "lodash.assign": "^3.2.0", - "lodash.defaults": "^3.1.2", - "lodash.foreach": "^3.0.3", - "mime": "^1.3.4", - "minimist": "^1.1.3", - "request": "^2.74.0", - "svgo": "^0.6.6", - "then-fs": "^2.0.0", - "uglify-js": "^2.8.0", - "update-notifier": "^0.5.0" - }, - "bin": { - "inliner": "cli/index.js" - } + "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==", + "license": "ISC" }, "node_modules/is-binary-path": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "license": "MIT", "dependencies": { "binary-extensions": "^2.0.0" }, @@ -974,34 +472,20 @@ "node": ">=8" } }, - "node_modules/is-buffer": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", - "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==" - }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "license": "MIT", "engines": { "node": ">=0.10.0" } }, - "node_modules/is-finite": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-finite/-/is-finite-1.1.0.tgz", - "integrity": "sha512-cdyMtqX/BOqqNBBiKlIVkytNHm49MtMlYyn1zxzvJKWmFMlGzm+ry5BBfYyeY9YmNKbRSo/o7OX9w9ale0wg3w==", - "engines": { - "node": ">=0.10.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/is-glob": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "license": "MIT", "dependencies": { "is-extglob": "^2.1.1" }, @@ -1009,310 +493,41 @@ "node": ">=0.10.0" } }, - "node_modules/is-npm": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-npm/-/is-npm-1.0.0.tgz", - "integrity": "sha512-9r39FIr3d+KD9SbX0sfMsHzb5PP3uimOiwr3YupUaUFG4W0l1U57Rx3utpttV7qz5U3jmrO5auUa04LU9pyHsg==", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/is-number": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "license": "MIT", "engines": { "node": ">=0.12.0" } }, - "node_modules/is-redirect": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-redirect/-/is-redirect-1.0.0.tgz", - "integrity": "sha512-cr/SlUEe5zOGmzvj9bUyC4LVvkNVAXu4GytXLNMr1pny+a65MpQ9IJzFHD5vi7FyJgb4qt27+eS3TuQnqB+RQw==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-stream": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", - "integrity": "sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-typedarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", - "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==" - }, - "node_modules/isarray": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", - "integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==" - }, - "node_modules/isstream": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", - "integrity": "sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==" - }, - "node_modules/js-yaml": { - "version": "3.6.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.6.1.tgz", - "integrity": "sha512-BLv3oxhfET+w5fjPwq3PsAsxzi9i3qzU//HMpWVz0A6KplF86HdR9x2TGnv9DXhSUrO7LO8czUiTd3yb3mLSvg==", - "dependencies": { - "argparse": "^1.0.7", - "esprima": "^2.6.0" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "node_modules/jsbn": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", - "integrity": "sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==" - }, - "node_modules/jschardet": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/jschardet/-/jschardet-1.6.0.tgz", - "integrity": "sha512-xYuhvQ7I9PDJIGBWev9xm0+SMSed3ZDBAmvVjbFR1ZRLAF+vlXcQu6cRI9uAlj81rzikElRVteehwV7DuX2ZmQ==", - "engines": { - "node": ">=0.1.90" - } - }, - "node_modules/json-schema": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", - "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==" - }, - "node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" - }, - "node_modules/json-stringify-safe": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", - "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==" - }, - "node_modules/jsprim": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.2.tgz", - "integrity": "sha512-P2bSOMAc/ciLz6DzgjVlGJP9+BrJWu5UDGK70C2iweC5QBIeFf0ZXRvGjEj2uYgrY2MkAAhsSWHDWlFtEroZWw==", - "dependencies": { - "assert-plus": "1.0.0", - "extsprintf": "1.3.0", - "json-schema": "0.4.0", - "verror": "1.10.0" - }, - "engines": { - "node": ">=0.6.0" - } - }, - "node_modules/kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", - "dependencies": { - "is-buffer": "^1.1.5" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/latest-version": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/latest-version/-/latest-version-1.0.1.tgz", - "integrity": "sha512-HERbxp4SBlmI380+eM0B0u4nxjfTaPeydIMzl9+9UQ4nSu3xMWKlX9WoT34e4wy7VWe67c53Nv9qPVjS8fHKgg==", - "dependencies": { - "package-json": "^1.0.0" - }, - "bin": { - "latest-version": "cli.js" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/lazy-cache": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/lazy-cache/-/lazy-cache-1.0.4.tgz", - "integrity": "sha512-RE2g0b5VGZsOCFOCgP7omTRYFqydmZkBwl5oNnQ1lDYC57uyO9KqNnNVxT7COSHTxrRCWVcAVOcbjk+tvh/rgQ==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/lodash": { - "version": "3.10.1", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-3.10.1.tgz", - "integrity": "sha512-9mDDwqVIma6OZX79ZlDACZl8sBm0TEnkf99zV3iMA4GzkIT/9hiqP5mY0HoT1iNLCrKc/R1HByV+yJfRWVJryQ==" - }, - "node_modules/lodash._arrayeach": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/lodash._arrayeach/-/lodash._arrayeach-3.0.0.tgz", - "integrity": "sha512-Mn7HidOVcl3mkQtbPsuKR0Fj0N6Q6DQB77CtYncZcJc0bx5qv2q4Gl6a0LC1AN+GSxpnBDNnK3CKEm9XNA4zqQ==" - }, - "node_modules/lodash._baseassign": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/lodash._baseassign/-/lodash._baseassign-3.2.0.tgz", - "integrity": "sha512-t3N26QR2IdSN+gqSy9Ds9pBu/J1EAFEshKlUHpJG3rvyJOYgcELIxcIeKKfZk7sjOz11cFfzJRsyFry/JyabJQ==", - "dependencies": { - "lodash._basecopy": "^3.0.0", - "lodash.keys": "^3.0.0" - } - }, - "node_modules/lodash._basecopy": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/lodash._basecopy/-/lodash._basecopy-3.0.1.tgz", - "integrity": "sha512-rFR6Vpm4HeCK1WPGvjZSJ+7yik8d8PVUdCJx5rT2pogG4Ve/2ZS7kfmO5l5T2o5V2mqlNIfSF5MZlr1+xOoYQQ==" - }, - "node_modules/lodash._baseeach": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/lodash._baseeach/-/lodash._baseeach-3.0.4.tgz", - "integrity": "sha512-IqUZ9MQo2UT1XPGuBntInqTOlc+oV+bCo0kMp+yuKGsfvRSNgUW0YjWVZUrG/gs+8z/Eyuc0jkJjOBESt9BXxg==", - "dependencies": { - "lodash.keys": "^3.0.0" - } - }, - "node_modules/lodash._bindcallback": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/lodash._bindcallback/-/lodash._bindcallback-3.0.1.tgz", - "integrity": "sha512-2wlI0JRAGX8WEf4Gm1p/mv/SZ+jLijpj0jyaE/AXeuQphzCgD8ZQW4oSpoN8JAopujOFGU3KMuq7qfHBWlGpjQ==" - }, - "node_modules/lodash._createassigner": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/lodash._createassigner/-/lodash._createassigner-3.1.1.tgz", - "integrity": "sha512-LziVL7IDnJjQeeV95Wvhw6G28Z8Q6da87LWKOPWmzBLv4u6FAT/x5v00pyGW0u38UoogNF2JnD3bGgZZDaNEBw==", - "dependencies": { - "lodash._bindcallback": "^3.0.0", - "lodash._isiterateecall": "^3.0.0", - "lodash.restparam": "^3.0.0" - } - }, - "node_modules/lodash._getnative": { - "version": "3.9.1", - "resolved": "https://registry.npmjs.org/lodash._getnative/-/lodash._getnative-3.9.1.tgz", - "integrity": "sha512-RrL9VxMEPyDMHOd9uFbvMe8X55X16/cGM5IgOKgRElQZutpX89iS6vwl64duTV1/16w5JY7tuFNXqoekmh1EmA==" - }, - "node_modules/lodash._isiterateecall": { - "version": "3.0.9", - "resolved": "https://registry.npmjs.org/lodash._isiterateecall/-/lodash._isiterateecall-3.0.9.tgz", - "integrity": "sha512-De+ZbrMu6eThFti/CSzhRvTKMgQToLxbij58LMfM8JnYDNSOjkjTCIaa8ixglOeGh2nyPlakbt5bJWJ7gvpYlQ==" - }, - "node_modules/lodash.assign": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/lodash.assign/-/lodash.assign-3.2.0.tgz", - "integrity": "sha512-/VVxzgGBmbphasTg51FrztxQJ/VgAUpol6zmJuSVSGcNg4g7FA4z7rQV8Ovr9V3vFBNWZhvKWHfpAytjTVUfFA==", - "dependencies": { - "lodash._baseassign": "^3.0.0", - "lodash._createassigner": "^3.0.0", - "lodash.keys": "^3.0.0" - } - }, - "node_modules/lodash.defaults": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-3.1.2.tgz", - "integrity": "sha512-X7135IXFQt5JDFnYxOVAzVz+kFvwDn3N8DJYf+nrz/mMWEuSu7+OL6rWqsk3+VR1T4TejFCSu5isBJOLSID2bg==", - "dependencies": { - "lodash.assign": "^3.0.0", - "lodash.restparam": "^3.0.0" - } - }, - "node_modules/lodash.foreach": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/lodash.foreach/-/lodash.foreach-3.0.3.tgz", - "integrity": "sha512-PA7Lp7pe2HMJBoB1vELegEIF3waUFnM0fWDKJVYolwZ4zHh6WTmnq0xmzfQksD66gx2quhDNyBdyaE2T8/DP3Q==", - "dependencies": { - "lodash._arrayeach": "^3.0.0", - "lodash._baseeach": "^3.0.0", - "lodash._bindcallback": "^3.0.0", - "lodash.isarray": "^3.0.0" - } - }, - "node_modules/lodash.isarguments": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz", - "integrity": "sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==" - }, - "node_modules/lodash.isarray": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/lodash.isarray/-/lodash.isarray-3.0.4.tgz", - "integrity": "sha512-JwObCrNJuT0Nnbuecmqr5DgtuBppuCvGD9lxjFpAzwnVtdGoDQ1zig+5W8k5/6Gcn0gZ3936HDAlGd28i7sOGQ==" - }, - "node_modules/lodash.keys": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/lodash.keys/-/lodash.keys-3.1.2.tgz", - "integrity": "sha512-CuBsapFjcubOGMn3VD+24HOAPxM79tH+V6ivJL3CHYjtrawauDJHUk//Yew9Hvc6e9rbCrURGk8z6PC+8WJBfQ==", - "dependencies": { - "lodash._getnative": "^3.0.0", - "lodash.isarguments": "^3.0.0", - "lodash.isarray": "^3.0.0" - } - }, - "node_modules/lodash.restparam": { - "version": "3.6.1", - "resolved": "https://registry.npmjs.org/lodash.restparam/-/lodash.restparam-3.6.1.tgz", - "integrity": "sha512-L4/arjjuq4noiUJpt3yS6KIKDtJwNe2fIYgMqyYYKoeIfV1iEqvPwhCx23o+R9dzouGihDAPN1dTIRWa7zk8tw==" - }, - "node_modules/longest": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/longest/-/longest-1.0.1.tgz", - "integrity": "sha512-k+yt5n3l48JU4k8ftnKG6V7u32wyH2NfKzeMto9F/QRE0amxy/LayxwlvjjkZEIzqR+19IrtFO8p5kB9QaYUFg==", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/lower-case": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz", "integrity": "sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==", + "license": "MIT", "dependencies": { "tslib": "^2.0.3" } }, - "node_modules/lowercase-keys": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-1.0.1.tgz", - "integrity": "sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA==", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/mime": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", - "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", + "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", + "license": "MIT", "bin": { "mime": "cli.js" }, "engines": { - "node": ">=4" - } - }, - "node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" + "node": ">=4.0.0" } }, "node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" }, @@ -1320,51 +535,27 @@ "node": "*" } }, - "node_modules/minimist": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", - "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/mkdirp": { - "version": "0.5.6", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", - "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", - "dependencies": { - "minimist": "^1.2.6" - }, - "bin": { - "mkdirp": "bin/cmd.js" - } - }, "node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" - }, - "node_modules/nested-error-stacks": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/nested-error-stacks/-/nested-error-stacks-1.0.2.tgz", - "integrity": "sha512-o32anp9JA7oezPOFSfG2BBXSdHepOm5FpJvwxHWDtfJ3Bg3xdi68S6ijPlEOfUg6quxZWyvJM+8fHk1yMDKspA==", - "dependencies": { - "inherits": "~2.0.1" - } + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" }, "node_modules/no-case": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz", "integrity": "sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==", + "license": "MIT", "dependencies": { "lower-case": "^2.0.2", "tslib": "^2.0.3" } }, "node_modules/nodemon": { - "version": "3.1.7", - "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.7.tgz", - "integrity": "sha512-hLj7fuMow6f0lbB0cD14Lz2xNjwsyruH251Pk4t/yIitCFJbmY1myuLlHm/q06aST4jg6EgAh74PIBBrRqpVAQ==", + "version": "3.1.9", + "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.9.tgz", + "integrity": "sha512-hdr1oIb2p6ZSxu3PB2JWWYS7ZQ0qvaZsc3hK8DR8f02kRzc8rjYmxAIvdz+aYC+8F2IjNaB7HMcSDg8nQpJxyg==", + "license": "MIT", "dependencies": { "chokidar": "^3.5.2", "debug": "^4", @@ -1388,112 +579,11 @@ "url": "https://opencollective.com/nodemon" } }, - "node_modules/nodemon/node_modules/debug": { - "version": "4.3.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", - "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/nodemon/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" - }, - "node_modules/nodemon/node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dependencies": { - "has-flag": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/nth-check": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-1.0.2.tgz", - "integrity": "sha512-WeBOdju8SnzPN5vTUJYxYUxLeXpCaVP5i5e0LF8fg7WORF2Wd7wFX/pk0tYZk7s8T+J7VLy0Da6J1+wCT0AtHg==", - "dependencies": { - "boolbase": "~1.0.0" - } - }, - "node_modules/oauth-sign": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", - "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==", - "engines": { - "node": "*" - } - }, - "node_modules/object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dependencies": { - "wrappy": "1" - } - }, - "node_modules/os-homedir": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", - "integrity": "sha512-B5JU3cabzk8c67mRRd3ECmROafjYMXbuzlwtqdM8IbS8ktlTix8aFGb2bAGKrSRIlnfKwovGUUr72JUPyOb6kQ==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/os-tmpdir": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", - "integrity": "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/osenv": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.5.tgz", - "integrity": "sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==", - "deprecated": "This package is no longer supported.", - "dependencies": { - "os-homedir": "^1.0.0", - "os-tmpdir": "^1.0.0" - } - }, - "node_modules/package-json": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/package-json/-/package-json-1.2.0.tgz", - "integrity": "sha512-knDtirWWqKVJrLY3gEBLflVvueTMpyjbAwX/9j/EKi2DsjNemp5voS8cyKyGh57SNaMJNhNRZbIaWdneOcLU1g==", - "dependencies": { - "got": "^3.2.0", - "registry-url": "^3.0.0" - }, + "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -1502,6 +592,7 @@ "version": "3.0.4", "resolved": "https://registry.npmjs.org/param-case/-/param-case-3.0.4.tgz", "integrity": "sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A==", + "license": "MIT", "dependencies": { "dot-case": "^3.0.4", "tslib": "^2.0.3" @@ -1511,20 +602,17 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/pascal-case/-/pascal-case-3.1.2.tgz", "integrity": "sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g==", + "license": "MIT", "dependencies": { "no-case": "^3.0.4", "tslib": "^2.0.3" } }, - "node_modules/performance-now": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", - "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==" - }, "node_modules/picomatch": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "license": "MIT", "engines": { "node": ">=8.6" }, @@ -1532,155 +620,17 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/pinkie": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz", - "integrity": "sha512-MnUuEycAemtSaeFSjXKW/aroV7akBbY+Sv+RkyqFjgAe73F+MR0TBWKBRDkmfWq/HiFmdavfZ1G7h4SPZXaCSg==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/pinkie-promise": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz", - "integrity": "sha512-0Gni6D4UcLTbv9c57DfxDGdr41XfgUjqWZu492f0cIGr16zDU06BWP/RAEvOuo7CQ0CNjHaLlM59YJJFm3NWlw==", - "dependencies": { - "pinkie": "^2.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/prepend-http": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-1.0.4.tgz", - "integrity": "sha512-PhmXi5XmoyKw1Un4E+opM2KcsJInDvKyuOumcjjw3waw86ZNjHwVUOOWLc4bCzLdcKNaWBH9e99sbWzDQsVaYg==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/process-nextick-args": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", - "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" - }, - "node_modules/promise": { - "version": "7.3.1", - "resolved": "https://registry.npmjs.org/promise/-/promise-7.3.1.tgz", - "integrity": "sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==", - "dependencies": { - "asap": "~2.0.3" - } - }, - "node_modules/psl": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", - "integrity": "sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==" - }, "node_modules/pstree.remy": { "version": "1.1.8", "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", - "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==" - }, - "node_modules/punycode": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", - "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", - "engines": { - "node": ">=6" - } - }, - "node_modules/q": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz", - "integrity": "sha512-kV/CThkXo6xyFEZUugw/+pIOywXcDbFYgSct5cT3gqlbkBE1SJdwy6UQoZvodiWF/ckQLZyDE/Bu1M6gVu5lVw==", - "deprecated": "You or someone you depend on is using Q, the JavaScript Promise library that gave JavaScript developers strong feelings about promises. They can almost certainly migrate to the native JavaScript promise now. Thank you literally everyone for joining me in this bet against the odds. Be excellent to each other.\n\n(For a CapTP with native promises, see @endo/eventual-send and @endo/captp)", - "engines": { - "node": ">=0.6.0", - "teleport": ">=0.2.0" - } - }, - "node_modules/qs": { - "version": "6.5.3", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.3.tgz", - "integrity": "sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA==", - "engines": { - "node": ">=0.6" - } - }, - "node_modules/rc": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", - "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", - "dependencies": { - "deep-extend": "^0.6.0", - "ini": "~1.3.0", - "minimist": "^1.2.0", - "strip-json-comments": "~2.0.1" - }, - "bin": { - "rc": "cli.js" - } - }, - "node_modules/read-all-stream": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/read-all-stream/-/read-all-stream-3.1.0.tgz", - "integrity": "sha512-DI1drPHbmBcUDWrJ7ull/F2Qb8HkwBncVx8/RpKYFSIACYaVRQReISYPdZz/mt1y1+qMCOrfReTopERmaxtP6w==", - "dependencies": { - "pinkie-promise": "^2.0.0", - "readable-stream": "^2.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/read-all-stream/node_modules/isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" - }, - "node_modules/read-all-stream/node_modules/readable-stream": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", - "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "node_modules/read-all-stream/node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" - }, - "node_modules/read-all-stream/node_modules/string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dependencies": { - "safe-buffer": "~5.1.0" - } - }, - "node_modules/readable-stream": { - "version": "1.1.14", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", - "integrity": "sha512-+MeVjFf4L44XUkhM1eYbD8fyEsxcV81pqMSR5gblfcLCHfZvbrqy4/qYHE+/R5HoBUT11WV5O08Cr1n3YXkWVQ==", - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.1", - "isarray": "0.0.1", - "string_decoder": "~0.10.x" - } + "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==", + "license": "MIT" }, "node_modules/readdirp": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "license": "MIT", "dependencies": { "picomatch": "^2.2.1" }, @@ -1688,122 +638,20 @@ "node": ">=8.10.0" } }, - "node_modules/registry-url": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/registry-url/-/registry-url-3.1.0.tgz", - "integrity": "sha512-ZbgR5aZEdf4UKZVBPYIgaglBmSF2Hi94s2PcIHhRGFjKYu+chjJdYfHn4rt3hB6eCKLJ8giVIIfgMa1ehDfZKA==", - "dependencies": { - "rc": "^1.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/relateurl": { "version": "0.2.7", "resolved": "https://registry.npmjs.org/relateurl/-/relateurl-0.2.7.tgz", "integrity": "sha512-G08Dxvm4iDN3MLM0EsP62EDV9IuhXPR6blNz6Utcp7zyV3tr4HVNINt6MpaRWbxoOHT3Q7YN2P+jaHX8vUbgog==", + "license": "MIT", "engines": { "node": ">= 0.10" } }, - "node_modules/repeat-string": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", - "integrity": "sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w==", - "engines": { - "node": ">=0.10" - } - }, - "node_modules/repeating": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/repeating/-/repeating-1.1.3.tgz", - "integrity": "sha512-Nh30JLeMHdoI+AsQ5eblhZ7YlTsM9wiJQe/AHIunlK3KWzvXhXb36IJ7K1IOeRjIOtzMjdUHjwXUFxKJoPTSOg==", - "dependencies": { - "is-finite": "^1.0.0" - }, - "bin": { - "repeating": "cli.js" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/request": { - "version": "2.88.2", - "resolved": "https://registry.npmjs.org/request/-/request-2.88.2.tgz", - "integrity": "sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==", - "deprecated": "request has been deprecated, see https://github.com/request/request/issues/3142", - "dependencies": { - "aws-sign2": "~0.7.0", - "aws4": "^1.8.0", - "caseless": "~0.12.0", - "combined-stream": "~1.0.6", - "extend": "~3.0.2", - "forever-agent": "~0.6.1", - "form-data": "~2.3.2", - "har-validator": "~5.1.3", - "http-signature": "~1.2.0", - "is-typedarray": "~1.0.0", - "isstream": "~0.1.2", - "json-stringify-safe": "~5.0.1", - "mime-types": "~2.1.19", - "oauth-sign": "~0.9.0", - "performance-now": "^2.1.0", - "qs": "~6.5.2", - "safe-buffer": "^5.1.2", - "tough-cookie": "~2.5.0", - "tunnel-agent": "^0.6.0", - "uuid": "^3.3.2" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/right-align": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/right-align/-/right-align-0.1.3.tgz", - "integrity": "sha512-yqINtL/G7vs2v+dFIZmFUDbnVyFUJFKd6gK22Kgo6R4jfJGFtisKyncWDDULgjfqf4ASQuIQyjJ7XZ+3aWpsAg==", - "dependencies": { - "align-text": "^0.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, - "node_modules/safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" - }, - "node_modules/sax": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", - "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==" - }, "node_modules/semver": { - "version": "7.6.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", - "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "version": "7.7.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.0.tgz", + "integrity": "sha512-DrfFnPzblFmNrIZzg5RzHegbiRWg7KMR7btwi2yjHwx06zsUbO5g613sVwEV7FTwmzJu+Io0lJe2GJ3LxqpvBQ==", + "license": "ISC", "bin": { "semver": "bin/semver.js" }, @@ -1811,29 +659,11 @@ "node": ">=10" } }, - "node_modules/semver-diff": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/semver-diff/-/semver-diff-2.1.0.tgz", - "integrity": "sha512-gL8F8L4ORwsS0+iQ34yCYv///jsOq0ZL7WP55d1HnJ32o7tyFYEFQZQA22mrLIacZdU6xecaBBZ+uEiffGNyXw==", - "dependencies": { - "semver": "^5.0.3" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/semver-diff/node_modules/semver": { - "version": "5.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", - "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", - "bin": { - "semver": "bin/semver" - } - }, "node_modules/simple-update-notifier": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==", + "license": "MIT", "dependencies": { "semver": "^7.5.3" }, @@ -1841,18 +671,11 @@ "node": ">=10" } }, - "node_modules/slide": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/slide/-/slide-1.1.6.tgz", - "integrity": "sha512-NwrtjCg+lZoqhFU8fOwl4ay2ei8PaqCBOUV3/ektPY9trO1yQ1oXEfmHAhKArUVUr/hOHvy5f6AdP17dCM0zMw==", - "engines": { - "node": "*" - } - }, "node_modules/source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" } @@ -1861,113 +684,29 @@ "version": "0.5.21", "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "license": "MIT", "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" } }, - "node_modules/sprintf-js": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", - "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==" - }, - "node_modules/sshpk": { - "version": "1.18.0", - "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.18.0.tgz", - "integrity": "sha512-2p2KJZTSqQ/I3+HX42EpYOa2l3f8Erv8MWKsy2I9uf4wA7yFIkXRffYdsx86y6z4vHtV8u7g+pPlr8/4ouAxsQ==", - "dependencies": { - "asn1": "~0.2.3", - "assert-plus": "^1.0.0", - "bcrypt-pbkdf": "^1.0.0", - "dashdash": "^1.12.0", - "ecc-jsbn": "~0.1.1", - "getpass": "^0.1.1", - "jsbn": "~0.1.0", - "safer-buffer": "^2.0.2", - "tweetnacl": "~0.14.0" - }, - "bin": { - "sshpk-conv": "bin/sshpk-conv", - "sshpk-sign": "bin/sshpk-sign", - "sshpk-verify": "bin/sshpk-verify" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/stream-shift": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.3.tgz", - "integrity": "sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ==" - }, - "node_modules/string_decoder": { - "version": "0.10.31", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", - "integrity": "sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ==" - }, - "node_modules/string-length": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/string-length/-/string-length-1.0.1.tgz", - "integrity": "sha512-MNCACnufWUf3pQ57O5WTBMkKhzYIaKEcUioO0XHrTMafrbBaNk4IyDOLHBv5xbXO0jLLdsYWeFjpjG2hVHRDtw==", - "dependencies": { - "strip-ansi": "^3.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/strip-ansi": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg==", - "dependencies": { - "ansi-regex": "^2.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/strip-json-comments": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", - "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/supports-color": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", - "integrity": "sha512-KKNVtd6pCYgPIKU4cp2733HWYCpplQhddZLBUryaAHou723x+FRzQ5Df824Fj+IyyuiQTRoub4SnIFfIcrp70g==", - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/svgo": { - "version": "0.6.6", - "resolved": "https://registry.npmjs.org/svgo/-/svgo-0.6.6.tgz", - "integrity": "sha512-C5A1r5SjFesNoKsmc+kWBxmB04iBGH2D/nFy8HJaME9+SyZKcmqcN8QG+GwxIc7D2+JWhaaW7uaM9+XwfplTEQ==", - "deprecated": "This SVGO version is no longer supported. Upgrade to v2.x.x.", + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "license": "MIT", "dependencies": { - "coa": "~1.0.1", - "colors": "~1.1.2", - "csso": "~2.0.0", - "js-yaml": "~3.6.0", - "mkdirp": "~0.5.1", - "sax": "~1.2.1", - "whet.extend": "~0.9.9" - }, - "bin": { - "svgo": "bin/svgo" + "has-flag": "^3.0.0" }, "engines": { - "node": ">=0.10.0" + "node": ">=4" } }, "node_modules/terser": { - "version": "5.34.0", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.34.0.tgz", - "integrity": "sha512-y5NUX+U9HhVsK/zihZwoq4r9dICLyV2jXGOriDAVOeKhq3LKVjgJbGO90FisozXLlJfvjHqgckGmJFBb9KYoWQ==", + "version": "5.37.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.37.0.tgz", + "integrity": "sha512-B8wRRkmre4ERucLM/uXx4MOV5cbnOlVAqUst+1+iLKPI0dOgFO28f84ptoQt9HEI537PMzfYa/d+GEPKTRXmYA==", + "license": "BSD-2-Clause", "dependencies": { "@jridgewell/source-map": "^0.3.3", "acorn": "^8.8.2", @@ -1984,28 +723,14 @@ "node_modules/terser/node_modules/commander": { "version": "2.20.3", "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", - "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==" - }, - "node_modules/then-fs": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/then-fs/-/then-fs-2.0.0.tgz", - "integrity": "sha512-5ffcBcU+vFUCYDNi/o507IqjqrTkuGsLVZ1Fp50hwgZRY7ufVFa9jFfTy5uZ2QnSKacKigWKeaXkOqLa4DsjLw==", - "dependencies": { - "promise": ">=3.2 <8" - } - }, - "node_modules/timed-out": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/timed-out/-/timed-out-2.0.0.tgz", - "integrity": "sha512-pqqJOi1rF5zNs/ps4vmbE4SFCrM4iR7LW+GHAsHqO/EumqbIWceioevYLM5xZRgQSH6gFgL9J/uB7EcJhQ9niQ==", - "engines": { - "node": ">=0.10.0" - } + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "license": "MIT" }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "license": "MIT", "dependencies": { "is-number": "^7.0.0" }, @@ -2017,196 +742,46 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.1.tgz", "integrity": "sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==", + "license": "ISC", "bin": { "nodetouch": "bin/nodetouch.js" } }, - "node_modules/tough-cookie": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", - "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==", - "dependencies": { - "psl": "^1.1.28", - "punycode": "^2.1.1" - }, - "engines": { - "node": ">=0.8" - } - }, "node_modules/tslib": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz", - "integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==" - }, - "node_modules/tunnel-agent": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", - "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", - "dependencies": { - "safe-buffer": "^5.0.1" - }, - "engines": { - "node": "*" - } - }, - "node_modules/tweetnacl": { - "version": "0.14.5", - "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", - "integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==" - }, - "node_modules/uglify-js": { - "version": "2.8.29", - "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-2.8.29.tgz", - "integrity": "sha512-qLq/4y2pjcU3vhlhseXGGJ7VbFO4pBANu0kwl8VCa9KEI0V8VfZIx2Fy3w01iSTA/pGwKZSmu/+I4etLNDdt5w==", - "dependencies": { - "source-map": "~0.5.1", - "yargs": "~3.10.0" - }, - "bin": { - "uglifyjs": "bin/uglifyjs" - }, - "engines": { - "node": ">=0.8.0" - }, - "optionalDependencies": { - "uglify-to-browserify": "~1.0.0" - } - }, - "node_modules/uglify-js/node_modules/source-map": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/uglify-to-browserify": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/uglify-to-browserify/-/uglify-to-browserify-1.0.2.tgz", - "integrity": "sha512-vb2s1lYx2xBtUgy+ta+b2J/GLVUR+wmpINwHePmPRhOsIVCG2wDzKJ0n14GslH1BifsqVzSOwQhRaCAsZ/nI4Q==", - "optional": true + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" }, "node_modules/undefsafe": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", - "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==" + "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==", + "license": "MIT" }, - "node_modules/update-notifier": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/update-notifier/-/update-notifier-0.5.0.tgz", - "integrity": "sha512-zOGOlUKDAgDlLHLv7Oiszz3pSj8fKlSJ3i0u49sEakjXUEVJ6DMjo/Mh/B6mg2eOALvRTJkd0kbChcipQoYCng==", + "node_modules/valid-data-url": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/valid-data-url/-/valid-data-url-3.0.1.tgz", + "integrity": "sha512-jOWVmzVceKlVVdwjNSenT4PbGghU0SBIizAev8ofZVgivk/TVHXSbNL8LP6M3spZvkR9/QolkyJavGSX5Cs0UA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/web-resource-inliner": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/web-resource-inliner/-/web-resource-inliner-7.0.0.tgz", + "integrity": "sha512-NlfnGF8MY9ZUwFjyq3vOUBx7KwF8bmE+ywR781SB0nWB6MoMxN4BA8gtgP1KGTZo/O/AyWJz7HZpR704eaj4mg==", + "license": "MIT", "dependencies": { - "chalk": "^1.0.0", - "configstore": "^1.0.0", - "is-npm": "^1.0.0", - "latest-version": "^1.0.0", - "repeating": "^1.1.2", - "semver-diff": "^2.0.0", - "string-length": "^1.0.0" + "ansi-colors": "^4.1.1", + "escape-goat": "^3.0.0", + "htmlparser2": "^5.0.0", + "mime": "^2.4.6", + "valid-data-url": "^3.0.0" }, "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/uri-js": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "dependencies": { - "punycode": "^2.1.0" - } - }, - "node_modules/util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" - }, - "node_modules/uuid": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", - "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", - "deprecated": "Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details.", - "bin": { - "uuid": "bin/uuid" - } - }, - "node_modules/verror": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", - "integrity": "sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw==", - "engines": [ - "node >=0.6.0" - ], - "dependencies": { - "assert-plus": "^1.0.0", - "core-util-is": "1.0.2", - "extsprintf": "^1.2.0" - } - }, - "node_modules/verror/node_modules/core-util-is": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", - "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==" - }, - "node_modules/whet.extend": { - "version": "0.9.9", - "resolved": "https://registry.npmjs.org/whet.extend/-/whet.extend-0.9.9.tgz", - "integrity": "sha512-mmIPAft2vTgEILgPeZFqE/wWh24SEsR/k+N9fJ3Jxrz44iDFy9aemCxdksfURSHYFCLmvs/d/7Iso5XjPpNfrA==", - "engines": { - "node": ">=0.6.0" - } - }, - "node_modules/window-size": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/window-size/-/window-size-0.1.0.tgz", - "integrity": "sha512-1pTPQDKTdd61ozlKGNCjhNRd+KPmgLSGa3mZTHoOliaGcESD8G1PXhh7c1fgiPjVbNVfgy2Faw4BI8/m0cC8Mg==", - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/wordwrap": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.2.tgz", - "integrity": "sha512-xSBsCeh+g+dinoBv3GAOWM4LcVVO68wLXRanibtBSdUvkGWQRGeE9P7IwU9EmDDi4jA6L44lz15CGMwdw9N5+Q==", - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" - }, - "node_modules/write-file-atomic": { - "version": "1.3.4", - "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-1.3.4.tgz", - "integrity": "sha512-SdrHoC/yVBPpV0Xq/mUZQIpW2sWXAShb/V4pomcJXh92RuaO+f3UTWItiR3Px+pLnV2PvC2/bfn5cwr5X6Vfxw==", - "dependencies": { - "graceful-fs": "^4.1.11", - "imurmurhash": "^0.1.4", - "slide": "^1.1.5" - } - }, - "node_modules/xdg-basedir": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-2.0.0.tgz", - "integrity": "sha512-NF1pPn594TaRSUO/HARoB4jK8I+rWgcpVlpQCK6/6o5PHyLUt2CSiDrpUZbQ6rROck+W2EwF8mBJcTs+W98J9w==", - "dependencies": { - "os-homedir": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/yargs": { - "version": "3.10.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-3.10.0.tgz", - "integrity": "sha512-QFzUah88GAGy9lyDKGBqZdkYApt63rCXYBGYnEP4xDJPXNqXXnBDACnbrXnViV6jRSqAePwrATi2i8mfYm4L1A==", - "dependencies": { - "camelcase": "^1.0.2", - "cliui": "^2.1.0", - "decamelize": "^1.0.0", - "window-size": "0.1.0" + "node": ">=10.0.0" } } } diff --git a/package.json b/package.json index 68260982e..2d7ad2de4 100644 --- a/package.json +++ b/package.json @@ -14,21 +14,21 @@ }, "repository": { "type": "git", - "url": "git+https://github.com/Aircoookie/WLED.git" + "url": "git+https://github.com/wled-dev/WLED.git" }, "author": "", "license": "ISC", "bugs": { - "url": "https://github.com/Aircoookie/WLED/issues" + "url": "https://github.com/wled-dev/WLED/issues" }, - "homepage": "https://github.com/Aircoookie/WLED#readme", + "homepage": "https://github.com/wled-dev/WLED#readme", "dependencies": { "clean-css": "^5.3.3", "html-minifier-terser": "^7.2.0", - "inliner": "^1.13.1", - "nodemon": "^3.1.7" + "web-resource-inliner": "^7.0.0", + "nodemon": "^3.1.9" }, "engines": { "node": ">=20.0.0" } -} +} \ No newline at end of file diff --git a/pio-scripts/build_ui.py b/pio-scripts/build_ui.py index f3688a5d4..eb7a01b36 100644 --- a/pio-scripts/build_ui.py +++ b/pio-scripts/build_ui.py @@ -1,3 +1,21 @@ -Import('env') +Import("env") +import shutil -env.Execute("npm run build") \ No newline at end of file +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) diff --git a/platformio.ini b/platformio.ini index e8ddd309c..a7485244c 100644 --- a/platformio.ini +++ b/platformio.ini @@ -139,7 +139,7 @@ lib_compat_mode = strict lib_deps = fastled/FastLED @ 3.6.0 IRremoteESP8266 @ 2.8.2 - makuna/NeoPixelBus @ 2.8.0 + makuna/NeoPixelBus @ 2.8.3 #https://github.com/makuna/NeoPixelBus.git#CoreShaderBeta https://github.com/Aircoookie/ESPAsyncWebServer.git#v2.4.0 # for I2C interface @@ -360,6 +360,7 @@ 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 @@ -369,12 +370,14 @@ extends = env:nodemcuv2 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] @@ -384,6 +387,8 @@ 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] @@ -392,11 +397,15 @@ extends = env:esp8266_2m 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] @@ -407,6 +416,8 @@ 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] @@ -415,12 +426,16 @@ extends = env:esp01_1m_full 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] diff --git a/platformio_override.sample.ini b/platformio_override.sample.ini index 60f9efe65..ee2b17714 100644 --- a/platformio_override.sample.ini +++ b/platformio_override.sample.ini @@ -34,7 +34,7 @@ lib_deps = ${esp8266.lib_deps} build_unflags = ${common.build_unflags} build_flags = ${common.build_flags} ${esp8266.build_flags} ; -; *** To use the below defines/overrides, copy and paste each onto it's own line just below build_flags in the section above. +; *** 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\" @@ -280,7 +280,7 @@ 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/Aircoookie/WLED/pull/2905#issuecomment-1328049860 +;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 diff --git a/readme.md b/readme.md index 8c9a08801..c0d24cffa 100644 --- a/readme.md +++ b/readme.md @@ -1,12 +1,12 @@

- - + + - +

diff --git a/requirements.in b/requirements.in index 7c715125f..a74bd418b 100644 --- a/requirements.in +++ b/requirements.in @@ -1 +1 @@ -platformio +platformio>=6.1.17 diff --git a/requirements.txt b/requirements.txt index ee70cd689..4d767b057 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,27 +2,25 @@ # This file is autogenerated by pip-compile with Python 3.11 # by the following command: # -# pip-compile +# pip-compile requirements.in # ajsonrpc==1.2.0 # via platformio -anyio==4.6.0 +anyio==4.8.0 # via starlette -bottle==0.13.1 +bottle==0.13.2 # via platformio -certifi==2024.8.30 +certifi==2025.1.31 # via requests -charset-normalizer==3.3.2 +charset-normalizer==3.4.1 # via requests -click==8.1.7 +click==8.1.8 # via # platformio # uvicorn colorama==0.4.6 - # via - # click - # platformio -h11==0.14.0 + # via platformio +h11==0.16.0 # via # uvicorn # wsproto @@ -30,13 +28,13 @@ idna==3.10 # via # anyio # requests -marshmallow==3.22.0 +marshmallow==3.26.1 # via platformio -packaging==24.1 +packaging==24.2 # via marshmallow -platformio==6.1.16 +platformio==6.1.17 # via -r requirements.in -pyelftools==0.31 +pyelftools==0.32 # via platformio pyserial==3.5 # via platformio @@ -46,13 +44,15 @@ semantic-version==2.10.0 # via platformio sniffio==1.3.1 # via anyio -starlette==0.39.1 +starlette==0.45.3 # via platformio tabulate==0.9.0 # via platformio -urllib3==2.2.3 +typing-extensions==4.12.2 + # via anyio +urllib3==2.3.0 # via requests -uvicorn==0.30.6 +uvicorn==0.34.0 # via platformio wsproto==1.2.0 # via platformio diff --git a/tools/cdata.js b/tools/cdata.js index c5d3c6aa5..e02047e8b 100644 --- a/tools/cdata.js +++ b/tools/cdata.js @@ -17,7 +17,7 @@ const fs = require("node:fs"); const path = require("path"); -const inliner = require("inliner"); +const inline = require("web-resource-inliner"); const zlib = require("node:zlib"); const CleanCSS = require("clean-css"); const minifyHtml = require("html-minifier-terser").minify; @@ -89,7 +89,7 @@ function adoptVersionAndRepo(html) { repoUrl = repoUrl.replace(/^git\+/, ""); repoUrl = repoUrl.replace(/\.git$/, ""); html = html.replaceAll("https://github.com/atuline/WLED", repoUrl); - html = html.replaceAll("https://github.com/Aircoookie/WLED", repoUrl); + html = html.replaceAll("https://github.com/wled-dev/WLED", repoUrl); } let version = packageJson.version; if (version) { @@ -128,21 +128,26 @@ async function minify(str, type = "plain") { async function writeHtmlGzipped(sourceFile, resultFile, page) { console.info("Reading " + sourceFile); - new inliner(sourceFile, async function (error, html) { - if (error) throw error; + inline.html({ + fileContent: fs.readFileSync(sourceFile, "utf8"), + relativeTo: path.dirname(sourceFile), + strict: true, + }, + 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}_L = ${result.length};\n`; - src += `const uint8_t PAGE_${page}[] PROGMEM = {\n${array}\n};\n\n`; - console.info("Writing " + resultFile); - fs.writeFileSync(resultFile, src); - }); + 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}_L = ${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) { diff --git a/usermods/BME68X_v2/BME68X_v2.cpp b/usermods/BME68X_v2/BME68X_v2.cpp index 081d03ff9..dace35ab6 100644 --- a/usermods/BME68X_v2/BME68X_v2.cpp +++ b/usermods/BME68X_v2/BME68X_v2.cpp @@ -2,1116 +2,1113 @@ * @file usermod_BMW68X.h * @author Gabriel A. Sieben (GeoGab) * @brief Usermod for WLED to implement the BME680/BME688 sensor - * @version 1.0.0 - * @date 19 Feb 2024 + * @version 1.0.2 + * @date 28 March 2025 */ -#warning ********************Included USERMOD_BME68X ******************** - -#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.1" // 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 - -/* Debug Print Text Coloring */ -#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 OK INFO_COLUMN "[" ESC_FGCOLOR_GREEN "OK" ESC_STYLE_RESET "]" -#define FAIL INFO_COLUMN "[" ESC_FGCOLOR_RED "FAIL" ESC_STYLE_RESET "]" -#define WARN INFO_COLUMN "[" ESC_FGCOLOR_YELLOW "WARN" ESC_STYLE_RESET "]" -#define 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 + #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) */ -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." 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 DONE)); -} - -/** - * @brief Called by WLED: Main loop called by WLED - * + inline float UsermodBME68X::getTemperature() { + return ValuesPtr->temperature; + } + + /** + * @brief Returns the current humidity + * @return Humididty value (%) */ -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 - * + inline float UsermodBME68X::getHumidity() { + return ValuesPtr->humidity; + } + + /** + * @brief Returns the current pressure + * @return Pressure value (hPa) */ -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 + inline float UsermodBME68X::getPressure() { + return ValuesPtr->pressure; + } + + /** + * @brief Returns the current gas resistance + * @return Gas resistance value (kΩ) */ -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 + inline float UsermodBME68X::getGasResistance() { + return ValuesPtr->gasResistance; + } + + /** + * @brief Returns the current absolute humidity + * @return Absolute humidity value (g/m³) */ -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. + inline float UsermodBME68X::getAbsoluteHumidity() { + return ValuesPtr->absHumidity; + } + + /** + * @brief Returns the current dew point + * @return Dew point (°C or °F as choosen in settings) */ -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 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) + inline float UsermodBME68X::getDewPoint() { + return ValuesPtr->drewPoint; + } + + /** + * @brief Returns the current iaq (Indoor Air Quallity) + * @return Iaq value (0-500) */ -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 + 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) */ -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 + inline float UsermodBME68X::getStaticIaq() { + return ValuesPtr->staticIaq; + } + + /** + * @brief Returns the current co2 + * @return Co2 value (ppm) */ -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) + inline float UsermodBME68X::getCo2() { + return ValuesPtr->co2; + } + + /** + * @brief Returns the current voc (Breath VOC concentration estimate [ppm]) + * @return Voc value (ppm) */ -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() + inline float UsermodBME68X::getVoc() { + return ValuesPtr->Voc; + } + + /** + * @brief Returns the current gas percentage + * @return Gas percentage value (%) */ -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(OK)); -} - -/** - * @brief Called by WLED: Add dropdown and additional infos / structure - * @see Usermod::appendConfigData() - * @see UsermodManager::appendConfigData() + 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) */ -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() + 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) */ -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(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(OK)); - } - else { - DEBUG_PRINTLN(F(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 + 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) */ -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. + 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) */ -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(FAIL); - } - else { - InfoPageStatusLine += " Warning Code : " + String(iaqSensor.bsecStatus); - DEBUG_PRINTLN(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(FAIL); - } - else { - InfoPageStatusLine += "warning code: " + String(iaqSensor.bme68xStatus); - DEBUG_PRINTLN(WARN); - } - } - else { - InfoPageStatusLine += F("OK"); - DEBUG_PRINTLN(OK); - } - } -} - -/** - * @brief Loads the calibration data from the file system of the device + 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) */ -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(FAIL); - } - else { - file.read(bsecState, BSEC_MAX_STATE_BLOB_SIZE); - file.close(); - DEBUG_PRINTLN(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 + 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) */ -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(FAIL); - } - else { - iaqSensor.getState(bsecState); - file.write(bsecState, BSEC_MAX_STATE_BLOB_SIZE); - file.close(); - stateUpdateCounter++; - DEBUG_PRINTF("(saved %d times)" 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); \ No newline at end of file + 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); \ No newline at end of file diff --git a/usermods/BME68X_v2/README.md b/usermods/BME68X_v2/README.md index 7e7a15113..ee2670aa9 100644 --- a/usermods/BME68X_v2/README.md +++ b/usermods/BME68X_v2/README.md @@ -1,65 +1,70 @@ # 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. + +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 +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 % - +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 +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. + +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. +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: +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 = 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. @@ -67,28 +72,29 @@ The IAQ index is therefore only meaningful if IAQ Accuracy = 3. In addition to t 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 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. +- **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. +- **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. +- **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. + +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. @@ -99,8 +105,9 @@ 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(); +- getHumidity(); - getPressure(); - getGasResistance(); - getAbsoluteHumidity(); @@ -118,15 +125,36 @@ Methods also exist to read the read/calculated values from other WLED modules th - 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. diff --git a/usermods/BME68X_v2/library.json.disabled b/usermods/BME68X_v2/library.json similarity index 58% rename from usermods/BME68X_v2/library.json.disabled rename to usermods/BME68X_v2/library.json index 6bd0bb9b2..0bd4e71b2 100644 --- a/usermods/BME68X_v2/library.json.disabled +++ b/usermods/BME68X_v2/library.json @@ -1,6 +1,5 @@ { - "name:": "BME68X_v2", - "build": { "libArchive": false}, + "name:": "BME68X", "dependencies": { "boschsensortec/BSEC Software Library":"^1.8.1492" } diff --git a/usermods/EXAMPLE/usermod_v2_example.cpp b/usermods/EXAMPLE/usermod_v2_example.cpp index be4528dee..fc50833ee 100644 --- a/usermods/EXAMPLE/usermod_v2_example.cpp +++ b/usermods/EXAMPLE/usermod_v2_example.cpp @@ -2,7 +2,7 @@ /* * Usermods allow you to add own functionality to WLED more easily - * See: https://github.com/Aircoookie/WLED/wiki/Add-own-functionality + * 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. diff --git a/usermods/Fix_unreachable_netservices_v2/usermod_Fix_unreachable_netservices.cpp b/usermods/Fix_unreachable_netservices_v2/usermod_Fix_unreachable_netservices.cpp index d1a5776c5..7fb8e9798 100644 --- a/usermods/Fix_unreachable_netservices_v2/usermod_Fix_unreachable_netservices.cpp +++ b/usermods/Fix_unreachable_netservices_v2/usermod_Fix_unreachable_netservices.cpp @@ -8,7 +8,7 @@ * 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/Aircoookie/WLED/wiki/Add-own-functionality + * 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. diff --git a/usermods/MY9291/library.json b/usermods/MY9291/library.json new file mode 100644 index 000000000..96e0bbf93 --- /dev/null +++ b/usermods/MY9291/library.json @@ -0,0 +1,4 @@ +{ + "name:": "MY9291", + "platforms": ["espressif8266"] +} \ No newline at end of file diff --git a/usermods/MY9291/library.json.disabled b/usermods/MY9291/library.json.disabled deleted file mode 100644 index 9324e4a02..000000000 --- a/usermods/MY9291/library.json.disabled +++ /dev/null @@ -1,3 +0,0 @@ -{ - "name:": "MY9291" -} \ No newline at end of file diff --git a/usermods/PIR_sensor_switch/PIR_Highlight_Standby b/usermods/PIR_sensor_switch/PIR_Highlight_Standby index 152388e8b..4ca32bf4e 100644 --- a/usermods/PIR_sensor_switch/PIR_Highlight_Standby +++ b/usermods/PIR_sensor_switch/PIR_Highlight_Standby @@ -42,7 +42,7 @@ * * * Usermods allow you to add own functionality to WLED more easily - * See: https://github.com/Aircoookie/WLED/wiki/Add-own-functionality + * 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. diff --git a/usermods/PIR_sensor_switch/PIR_sensor_switch.cpp b/usermods/PIR_sensor_switch/PIR_sensor_switch.cpp index 20e4bc148..6f09ce5be 100644 --- a/usermods/PIR_sensor_switch/PIR_sensor_switch.cpp +++ b/usermods/PIR_sensor_switch/PIR_sensor_switch.cpp @@ -24,7 +24,7 @@ * Maintained by: @blazoncek * * Usermods allow you to add own functionality to WLED more easily - * See: https://github.com/Aircoookie/WLED/wiki/Add-own-functionality + * 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. diff --git a/usermods/PIR_sensor_switch/readme.md b/usermods/PIR_sensor_switch/readme.md index fac5419f0..be55406df 100644 --- a/usermods/PIR_sensor_switch/readme.md +++ b/usermods/PIR_sensor_switch/readme.md @@ -5,7 +5,7 @@ This usermod-v2 modification allows the connection of a PIR sensor to switch on _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://github.com/Aircoookie/WLED/wiki/Control-a-relay-with-WLED) to keep the power consumption low when it is switched off. +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 diff --git a/usermods/TTGO-T-Display/usermod.cpp b/usermods/TTGO-T-Display/usermod.cpp index cbba07771..d8dcb2999 100644 --- a/usermods/TTGO-T-Display/usermod.cpp +++ b/usermods/TTGO-T-Display/usermod.cpp @@ -1,7 +1,7 @@ /* * This file allows you to add own functionality to WLED more easily - * See: https://github.com/Aircoookie/WLED/wiki/Add-own-functionality + * 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 */ diff --git a/usermods/Temperature/readme.md b/usermods/Temperature/readme.md index 3d65be7eb..b7697edc3 100644 --- a/usermods/Temperature/readme.md +++ b/usermods/Temperature/readme.md @@ -11,11 +11,19 @@ Maintained by @blazoncek ## Installation -Copy the example `platformio_override.ini` to the root directory. This file should be placed in the same directory as `platformio.ini`. +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` - enables this user mod wled00/usermods_list.cpp * `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. @@ -25,28 +33,6 @@ All parameters can be configured at runtime via the Usermods settings page, incl * [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 -### 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_dallas_temperature_C`. - -If you are not using `platformio_override.ini`, you might have to uncomment `OneWire@~2.3.5 under` `[common]` section in `platformio.ini`: - -```ini -# platformio.ini -... -[platformio] -... -; default_envs = esp07 -default_envs = d1_mini -... -[common] -... -lib_deps = - ... - #For Dallas sensor uncomment following - paulstoffregen/OneWire @ ~2.3.8 -``` - ## Change Log 2020-09-12 diff --git a/usermods/TetrisAI_v2/readme.md b/usermods/TetrisAI_v2/readme.md index b56f801a8..5ac802896 100644 --- a/usermods/TetrisAI_v2/readme.md +++ b/usermods/TetrisAI_v2/readme.md @@ -6,7 +6,7 @@ Version 1.0 ## Installation -Just activate the usermod with `-D USERMOD_TETRISAI` and the effect will 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/Aircoookie/WLED/blob/main/tools/WLED_ESP32_4MB_256KB_FS.csv)). +Just activate the usermod with `-D USERMOD_TETRISAI` and the effect will 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` (or `platformio_override.ini`): diff --git a/usermods/audioreactive/audio_reactive.cpp b/usermods/audioreactive/audio_reactive.cpp index a5bf782ad..4b3520562 100644 --- a/usermods/audioreactive/audio_reactive.cpp +++ b/usermods/audioreactive/audio_reactive.cpp @@ -18,7 +18,7 @@ /* * Usermods allow you to add own functionality to WLED more easily - * See: https://github.com/Aircoookie/WLED/wiki/Add-own-functionality + * See: https://github.com/wled-dev/WLED/wiki/Add-own-functionality * * This is an audioreactive v2 usermod. * .... @@ -869,7 +869,7 @@ class AudioReactive : public Usermod { 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 = inoise8(millis(), millis()); // Simulated analog read + micIn = perlin8(millis(), millis()); // Simulated analog read micDataReal = micIn; #else #ifdef ARDUINO_ARCH_ESP32 diff --git a/usermods/buzzer/buzzer.cpp b/usermods/buzzer/buzzer.cpp index e5a071e62..60db08abc 100644 --- a/usermods/buzzer/buzzer.cpp +++ b/usermods/buzzer/buzzer.cpp @@ -10,7 +10,7 @@ /* * Usermods allow you to add own functionality to WLED more easily - * See: https://github.com/Aircoookie/WLED/wiki/Add-own-functionality + * See: https://github.com/wled-dev/WLED/wiki/Add-own-functionality * * Using a usermod: * 1. Copy the usermod into the sketch folder (same folder as wled00.ino) diff --git a/usermods/photoresistor_sensor_mqtt_v1/usermod.cpp b/usermods/photoresistor_sensor_mqtt_v1/usermod.cpp index fff7118f3..bbbefc101 100644 --- a/usermods/photoresistor_sensor_mqtt_v1/usermod.cpp +++ b/usermods/photoresistor_sensor_mqtt_v1/usermod.cpp @@ -1,7 +1,7 @@ #include "wled.h" /* * This v1 usermod file allows you to add own functionality to WLED more easily - * See: https://github.com/Aircoookie/WLED/wiki/Add-own-functionality + * 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) * diff --git a/usermods/pixels_dice_tray/platformio_override.ini.sample b/usermods/pixels_dice_tray/platformio_override.ini.sample index b712f8b2e..6b4fa7768 100644 --- a/usermods/pixels_dice_tray/platformio_override.ini.sample +++ b/usermods/pixels_dice_tray/platformio_override.ini.sample @@ -102,7 +102,7 @@ lib_deps = ${esp32s3.lib_deps} # 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/Aircoookie/WLED/issues/1382 +# 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 diff --git a/usermods/stairway_wipe_basic/stairway_wipe_basic.cpp b/usermods/stairway_wipe_basic/stairway_wipe_basic.cpp index cdb7cd662..cddd655d6 100644 --- a/usermods/stairway_wipe_basic/stairway_wipe_basic.cpp +++ b/usermods/stairway_wipe_basic/stairway_wipe_basic.cpp @@ -2,7 +2,7 @@ /* * Usermods allow you to add own functionality to WLED more easily - * See: https://github.com/Aircoookie/WLED/wiki/Add-own-functionality + * See: https://github.com/wled-dev/WLED/wiki/Add-own-functionality * * This is Stairway-Wipe as a v2 usermod. * diff --git a/usermods/usermod_rotary_brightness_color/README.md b/usermods/usermod_rotary_brightness_color/README.md index 06a3a0f82..feb778dd6 100644 --- a/usermods/usermod_rotary_brightness_color/README.md +++ b/usermods/usermod_rotary_brightness_color/README.md @@ -37,6 +37,7 @@ Open Usermod Settings in WLED to change settings: No special requirements. ## Change Log - -2021-07 -* Upgraded to work with the latest WLED code, and make settings configurable in Usermod Settings +- 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 diff --git a/usermods/usermod_rotary_brightness_color/usermod_rotary_brightness_color.cpp b/usermods/usermod_rotary_brightness_color/usermod_rotary_brightness_color.cpp index 84c11afcd..0a485152f 100644 --- a/usermods/usermod_rotary_brightness_color/usermod_rotary_brightness_color.cpp +++ b/usermods/usermod_rotary_brightness_color/usermod_rotary_brightness_color.cpp @@ -93,9 +93,9 @@ public: } else { - fastled_col.red = col[0]; - fastled_col.green = col[1]; - fastled_col.blue = col[2]; + 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) @@ -104,9 +104,9 @@ public: new_val += 255; // roll-over if smaller than 0 prim_hsv.h = (byte)new_val; hsv2rgb_rainbow(prim_hsv, fastled_col); - col[0] = fastled_col.red; - col[1] = fastled_col.green; - col[2] = fastled_col.blue; + colPri[0] = fastled_col.red; + colPri[1] = fastled_col.green; + colPri[2] = fastled_col.blue; } } else if (Enc_B == LOW) @@ -118,9 +118,9 @@ public: } else { - fastled_col.red = col[0]; - fastled_col.green = col[1]; - fastled_col.blue = col[2]; + 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) @@ -129,9 +129,9 @@ public: new_val += 255; // roll-over if smaller than 0 prim_hsv.h = (byte)new_val; hsv2rgb_rainbow(prim_hsv, fastled_col); - col[0] = fastled_col.red; - col[1] = fastled_col.green; - col[2] = fastled_col.blue; + 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) diff --git a/usermods/usermod_v2_brightness_follow_sun/README.md b/usermods/usermod_v2_brightness_follow_sun/README.md new file mode 100644 index 000000000..25daf0ba2 --- /dev/null +++ b/usermods/usermod_v2_brightness_follow_sun/README.md @@ -0,0 +1,35 @@ +# 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 diff --git a/usermods/usermod_v2_brightness_follow_sun/usermod_v2_brightness_follow_sun.h b/usermods/usermod_v2_brightness_follow_sun/usermod_v2_brightness_follow_sun.h new file mode 100644 index 000000000..99f646b21 --- /dev/null +++ b/usermods/usermod_v2_brightness_follow_sun/usermod_v2_brightness_follow_sun.h @@ -0,0 +1,130 @@ +#pragma once + +#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"; diff --git a/usermods/usermod_v2_rotary_encoder_ui_ALT/usermod_v2_rotary_encoder_ui_ALT.cpp b/usermods/usermod_v2_rotary_encoder_ui_ALT/usermod_v2_rotary_encoder_ui_ALT.cpp index 9d1129841..a0cbc532f 100644 --- a/usermods/usermod_v2_rotary_encoder_ui_ALT/usermod_v2_rotary_encoder_ui_ALT.cpp +++ b/usermods/usermod_v2_rotary_encoder_ui_ALT/usermod_v2_rotary_encoder_ui_ALT.cpp @@ -516,7 +516,7 @@ void RotaryEncoderUIUsermod::setup() loopTime = millis(); - currentCCT = (approximateKelvinFromRGB(RGBW32(col[0], col[1], col[2], col[3])) - 1900) >> 5; + currentCCT = (approximateKelvinFromRGB(RGBW32(colPri[0], colPri[1], colPri[2], colPri[3])) - 1900) >> 5; if (!initDone) sortModesAndPalettes(); @@ -918,17 +918,17 @@ void RotaryEncoderUIUsermod::changeHue(bool increase){ display->updateRedrawTime(); #endif currentHue1 = max(min((increase ? currentHue1+fadeAmount : currentHue1-fadeAmount), 255), 0); - colorHStoRGB(currentHue1*256, currentSat1, col); + colorHStoRGB(currentHue1*256, currentSat1, colPri); stateChanged = true; if (applyToAll) { for (unsigned i=0; iupdateRedrawTime(); #endif currentSat1 = max(min((increase ? currentSat1+fadeAmount : currentSat1-fadeAmount), 255), 0); - colorHStoRGB(currentHue1*256, currentSat1, col); + colorHStoRGB(currentHue1*256, currentSat1, colPri); if (applyToAll) { for (unsigned i=0; iu_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]; - } else { - // add support for no audio data - um_data = simulateSound(SEGMENT.soundSim); - } + 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 (strip.paletteBlend == 1 || strip.paletteBlend == 3) @@ -225,8 +229,7 @@ uint16_t color_wipe(bool rev, bool useRandomColors) { } unsigned ledIndex = (prog * SEGLEN) >> 15; - unsigned rem = 0; - rem = (prog * SEGLEN) * 2; //mod 0xFFFF + uint16_t rem = (prog * SEGLEN) * 2; //mod 0xFFFF by truncating rem /= (SEGMENT.intensity +1); if (rem > 255) rem = 255; @@ -706,7 +709,7 @@ static const char _data_FX_MODE_DISSOLVE_RANDOM[] PROGMEM = "Dissolve Rnd@Repeat * Inspired by www.tweaking4all.com/hardware/arduino/adruino-led-strip-effects/ */ uint16_t mode_sparkle(void) { - if (!SEGMENT.check2) for(unsigned i = 0; i < SEGLEN; i++) { + 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; @@ -1134,7 +1137,7 @@ uint16_t mode_running_random(void) { unsigned z = it % zoneSize; bool nzone = (!z && it != SEGENV.aux1); - for (unsigned i=SEGLEN-1; i > 0; i--) { + for (int i=SEGLEN-1; i >= 0; i--) { if (nzone || z >= zoneSize) { unsigned lastrand = PRNG16 >> 8; int16_t diff = 0; @@ -1215,7 +1218,6 @@ uint16_t mode_dual_larson_scanner(void){ } 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" */ @@ -1243,7 +1245,6 @@ uint16_t mode_comet(void) { } static const char _data_FX_MODE_COMET[] PROGMEM = "Lighthouse@!,Fade rate;!,!;!"; - /* * Fireworks function. */ @@ -1287,7 +1288,6 @@ uint16_t mode_fireworks() { } 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 uint16_t mode_rain() { if (SEGLEN <= 1) return mode_static(); @@ -1322,7 +1322,6 @@ uint16_t mode_rain() { } static const char _data_FX_MODE_RAIN[] PROGMEM = "Rain@!,Spawning rate;!,!;!;12;ix=128,pal=0"; - /* * Fire flicker function */ @@ -1713,7 +1712,7 @@ uint16_t mode_tricolor_fade(void) { } static const char _data_FX_MODE_TRICOLOR_FADE[] PROGMEM = "Tri Fade@!;1,2,3;!"; - +#ifdef WLED_PS_DONT_REPLACE_FX /* * Creates random comets * Custom mode by Keith Lord: https://github.com/kitesurfer1404/WS2812FX/blob/master/src/custom/MultiComet.h @@ -1752,6 +1751,7 @@ uint16_t mode_multi_comet(void) { } static const char _data_FX_MODE_MULTI_COMET[] PROGMEM = "Multi Comet@!,Fade;!,!;!;1"; #undef MAX_COMETS +#endif // WLED_PS_DONT_REPLACE_FX /* * Running random pixels ("Stream 2") @@ -1768,7 +1768,7 @@ uint16_t mode_random_chase(void) { uint32_t color = SEGENV.step; random16_set_seed(SEGENV.aux0); - for (unsigned i = SEGLEN -1; i > 0; i--) { + 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(); @@ -2077,7 +2077,7 @@ uint16_t mode_palette() { } 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"; - +#ifdef WLED_PS_DONT_REPLACE_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 @@ -2164,6 +2164,7 @@ uint16_t mode_fire_2012() { return FRAMETIME; } 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_FX // colored stripes pulsing at a defined Beats-Per-Minute (BPM) uint16_t mode_bpm() { @@ -2181,7 +2182,7 @@ static const char _data_FX_MODE_BPM[] PROGMEM = "Bpm@!;!;!;;sx=64"; uint16_t mode_fillnoise8() { if (SEGENV.call == 0) SEGENV.step = hw_random(); for (unsigned i = 0; i < SEGLEN; i++) { - unsigned index = inoise8(i * SEGLEN, SEGENV.step + i * SEGLEN); + 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 @@ -2201,7 +2202,7 @@ uint16_t mode_noise16_1() { 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 = inoise16(real_x, real_y, real_z) >> 8; // get the noise data and scale it down + 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)); @@ -2219,7 +2220,7 @@ uint16_t mode_noise16_2() { 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 = inoise16(real_x, 0, 4223) >> 8; // get the noise data and scale it down + 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)); @@ -2240,7 +2241,7 @@ uint16_t mode_noise16_3() { 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 = inoise16(real_x, real_y, real_z) >> 8; // get the noise data and scale it down + 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)); @@ -2255,7 +2256,7 @@ static const char _data_FX_MODE_NOISE16_3[] PROGMEM = "Noise 3@!;!;!;;pal=35"; uint16_t mode_noise16_4() { uint32_t stp = (strip.now * SEGMENT.speed) >> 7; for (unsigned i = 0; i < SEGLEN; i++) { - int index = inoise16(uint32_t(i) << 12, stp); + int index = perlin16(uint32_t(i) << 12, stp); SEGMENT.setPixelColor(i, SEGMENT.color_from_palette(index, false, PALETTE_SOLID_WRAP, 0)); } return FRAMETIME; @@ -2358,12 +2359,14 @@ uint16_t mode_meteor() { for (unsigned i = 0; i < SEGLEN; i++) { uint32_t col; if (hw_random8() <= 255 - SEGMENT.intensity) { - if(meteorSmooth) { - 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); + 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); } - else { + 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; @@ -2927,7 +2930,6 @@ uint16_t mode_spots_fade() } static const char _data_FX_MODE_SPOTS_FADE[] PROGMEM = "Spots Fade@Spread,Width,,,,,Overlay;!,!;!"; - //each needs 12 bytes typedef struct Ball { unsigned long lastBounceTime; @@ -3011,11 +3013,11 @@ uint16_t mode_bouncing_balls(void) { } static const char _data_FX_MODE_BOUNCINGBALLS[] PROGMEM = "Bouncing Balls@Gravity,# of balls,,,,,Overlay;!,!,!;!;1;m12=1"; //bar - +#ifdef WLED_PS_DONT_REPLACE_FX /* * bouncing balls on a track track Effect modified from Aircoookie's bouncing balls * Courtesy of pjhatch (https://github.com/pjhatch) - * https://github.com/Aircoookie/WLED/pull/1039 + * https://github.com/wled-dev/WLED/pull/1039 */ // modified for balltrack mode typedef struct RollingBall { @@ -3110,8 +3112,8 @@ static uint16_t rolling_balls(void) { return FRAMETIME; } -static const char _data_FX_MODE_ROLLINGBALLS[] PROGMEM = "Rolling Balls@!,# of balls,,,,Collisions,Overlay,Trails;!,!,!;!;1;m12=1"; //bar - +static const char _data_FX_MODE_ROLLINGBALLS[] PROGMEM = "Rolling Balls@!,# of balls,,,,Collide,Overlay,Trails;!,!,!;!;1;m12=1"; //bar +#endif // WLED_PS_DONT_REPLACE_FX /* * Sinelon stolen from FASTLED examples @@ -3206,7 +3208,6 @@ uint16_t mode_solid_glitter() } 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 { @@ -3243,7 +3244,7 @@ uint16_t mode_popcorn(void) { unsigned numPopcorn = SEGMENT.intensity * usablePopcorns / 255; if (numPopcorn == 0) numPopcorn = 1; - for(unsigned i = 0; i < numPopcorn; i++) { + 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; @@ -3282,7 +3283,6 @@ uint16_t mode_popcorn(void) { } static const char _data_FX_MODE_POPCORN[] PROGMEM = "Popcorn@!,!,,,,,Overlay;!,!,!;!;;m12=1"; //bar - //values close to 100 produce 5Hz flicker, which looks very candle-y //Inspired by https://github.com/avanhanegem/ArduinoCandleEffectNeoPixel //and https://cpldcpu.wordpress.com/2016/01/05/reverse-engineering-a-real-candle/ @@ -3375,7 +3375,7 @@ uint16_t mode_candle_multi() } static const char _data_FX_MODE_CANDLE_MULTI[] PROGMEM = "Candle Multi@!,!;!,!;!;;sx=96,ix=224,pal=0"; - +#ifdef WLED_PS_DONT_REPLACE_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/ @@ -3507,8 +3507,9 @@ uint16_t mode_starburst(void) { } #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_FX - + #ifdef WLED_PS_DONT_REPLACE_FX /* * Exploding fireworks effect * adapted from: http://www.anirama.com/1000leds/1d-fireworks/ @@ -3645,8 +3646,8 @@ uint16_t mode_exploding_fireworks(void) return FRAMETIME; } #undef MAX_SPARKS -static const char _data_FX_MODE_EXPLODING_FIREWORKS[] PROGMEM = "Fireworks 1D@Gravity,Firing side,,,,,,Blur;!,!;!;12;pal=11,ix=128"; - +static const char _data_FX_MODE_EXPLODING_FIREWORKS[] PROGMEM = "Fireworks 1D@Gravity,Firing side;!,!;!;12;pal=11,ix=128"; +#endif // WLED_PS_DONT_REPLACE_FX /* * Drip Effect @@ -3734,7 +3735,6 @@ uint16_t mode_drip(void) } static const char _data_FX_MODE_DRIP[] PROGMEM = "Drip@Gravity,# of drips,,,,,Overlay;!,!;!;;m12=1"; //bar - /* * Tetris or Stacking (falling bricks) Effect * by Blaz Kristan (AKA blazoncek) (https://github.com/blazoncek, https://blaz.at/home) @@ -4118,7 +4118,7 @@ static uint16_t phased_base(uint8_t moder) { // We're making si *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 = (inoise8(i*10 + i*10) /16); // Let's randomize our mod length with some Perlin noise. + 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. @@ -4187,7 +4187,7 @@ uint16_t mode_noisepal(void) { // Slow noise if (SEGMENT.palette > 0) palettes[0] = SEGPALETTE; for (unsigned i = 0; i < SEGLEN; i++) { - unsigned index = inoise8(i*scale, SEGENV.aux0+i*scale); // Get a value from the noise function. I'm using both x and y axis. + 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. } @@ -4282,17 +4282,6 @@ uint16_t mode_chunchun(void) } static const char _data_FX_MODE_CHUNCHUN[] PROGMEM = "Chunchun@!,Gap size;!,!;!"; - -//13 bytes -typedef struct Spotlight { - float speed; - uint8_t colorIdx; - int16_t position; - unsigned long lastUpdateTime; - uint8_t width; - uint8_t type; -} spotlight; - #define SPOT_TYPE_SOLID 0 #define SPOT_TYPE_GRADIENT 1 #define SPOT_TYPE_2X_GRADIENT 2 @@ -4306,6 +4295,17 @@ typedef struct Spotlight { #define SPOT_MAX_COUNT 49 //Number of simultaneous waves #endif +#ifdef WLED_PS_DONT_REPLACE_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 @@ -4429,7 +4429,7 @@ uint16_t mode_dancing_shadows(void) return FRAMETIME; } static const char _data_FX_MODE_DANCING_SHADOWS[] PROGMEM = "Dancing Shadows@!,# of shadows;!;!"; - +#endif // WLED_PS_DONT_REPLACE_FX /* Imitates a washing machine, rotating same waves forward, then pause, then backward. @@ -4798,7 +4798,7 @@ uint16_t mode_perlinmove(void) { if (SEGLEN <= 1) return mode_static(); SEGMENT.fade_out(255-SEGMENT.custom1); for (int i = 0; i < SEGMENT.intensity/16 + 1; i++) { - unsigned locn = inoise16(strip.now*128/(260-SEGMENT.speed)+i*15000, strip.now*128/(260-SEGMENT.speed)); // Get a new pixel location from moving noise. + 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)); } @@ -4852,7 +4852,6 @@ static const char _data_FX_MODE_FLOWSTRIPE[] PROGMEM = "Flow Stripe@Hue speed,Ef #ifndef WLED_DISABLE_2D /////////////////////////////////////////////////////////////////////////////// //*************************** 2D routines *********************************** -#define XY(x,y) SEGMENT.XY(x,y) // Black hole @@ -5057,7 +5056,7 @@ uint16_t mode_2Dfirenoise(void) { // firenoise2d. By Andrew Tuline CRGBPalette16 pal = SEGMENT.check1 ? SEGPALETTE : SEGMENT.loadPalette(pal, 35); for (int j=0; j < cols; j++) { for (int i=0; i < rows; i++) { - indexx = inoise8(j*yscale*rows/255, i*xscale+strip.now/4); // We're moving along our Perlin map. + 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 @@ -5101,6 +5100,7 @@ uint16_t mode_2Dgameoflife(void) { // Written by Ewoud Wijma, inspired by https: const int cols = SEG_W; const int rows = SEG_H; + const auto XY = [&](int x, int y) { return (x%cols) + (y%rows) * cols; }; const unsigned dataSize = sizeof(CRGB) * SEGMENT.length(); // using width*height prevents reallocation if mirroring is enabled const int crcBufferLen = 2; //(SEGMENT.width() + SEGMENT.height())*71/100; // roughly sqrt(2)/2 for better repetition detection (Ewowi) @@ -5374,6 +5374,7 @@ uint16_t mode_2Dmatrix(void) { // Matrix2D. By Jeremy Williams. 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)) return mode_static(); //allocation failed @@ -5448,11 +5449,11 @@ uint16_t mode_2Dmetaballs(void) { // Metaballs by Stefan Petrick. Cannot have float speed = 0.25f * (1+(SEGMENT.speed>>6)); // get some 2 random moving points - int x2 = map(inoise8(strip.now * speed, 25355, 685), 0, 255, 0, cols-1); - int y2 = map(inoise8(strip.now * speed, 355, 11685), 0, 255, 0, rows-1); + 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(inoise8(strip.now * speed, 55355, 6685), 0, 255, 0, cols-1); - int y3 = map(inoise8(strip.now * speed, 25355, 22685), 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); @@ -5508,7 +5509,7 @@ uint16_t mode_2Dnoise(void) { // By Andrew Tuline for (int y = 0; y < rows; y++) { for (int x = 0; x < cols; x++) { - uint8_t pixelHue8 = inoise8(x * scale, y * scale, strip.now / (16 - SEGMENT.speed/16)); + uint8_t pixelHue8 = perlin8(x * scale, y * scale, strip.now / (16 - SEGMENT.speed/16)); SEGMENT.setPixelColorXY(x, y, ColorFromPalette(SEGPALETTE, pixelHue8)); } } @@ -5530,10 +5531,10 @@ uint16_t mode_2DPlasmaball(void) { // By: Stepko https://edito 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 = inoise8(i * 30, t, t); + 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_ = inoise8(t, j * 30, t); + 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); @@ -5578,7 +5579,7 @@ uint16_t mode_2DPolarLights(void) { // By: Kostyantyn Matviyevskyy https for (int x = 0; x < cols; x++) { for (int y = 0; y < rows; y++) { SEGENV.step++; - uint8_t palindex = qsub8(inoise8((SEGENV.step%2) + x * _scale, y * 16 + SEGENV.step % 16, SEGENV.step / _speed), fabsf((float)rows / 2.0f - (float)y) * adjustHeight); + 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)); @@ -5695,7 +5696,8 @@ uint16_t mode_2DSunradiation(void) { // By: ldirko https://edi 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 = (inoise8_raw(i * someVal, j * someVal, t)) / 2; + byte col = ((int16_t)perlin8(i * someVal, j * someVal, t) - 0x7F) / 3; bump[index++] = col; } } @@ -5876,7 +5878,7 @@ uint16_t mode_2Dcrazybees(void) { 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_FX ///////////////////////// // 2D Ghost Rider // ///////////////////////// @@ -6064,7 +6066,7 @@ uint16_t mode_2Dfloatingblobs(void) { } static const char _data_FX_MODE_2DBLOBS[] PROGMEM = "Blobs@!,# blobs,Blur,Trail;!;!;2;c1=8"; #undef MAX_BLOBS - +#endif // WLED_PS_DONT_REPLACE_FX //////////////////////////// // 2D Scrolling text // @@ -6393,10 +6395,10 @@ uint16_t mode_2DWaverly(void) { long t = strip.now / 2; for (int i = 0; i < cols; i++) { - unsigned thisVal = (1 + SEGMENT.intensity/64) * inoise8(i * 45 , t , t)/2; + unsigned thisVal = (1 + SEGMENT.intensity/64) * perlin8(i * 45 , t , t)/2; // use audio if available if (um_data) { - thisVal /= 32; // reduce intensity of inoise8() + thisVal /= 32; // reduce intensity of perlin8() thisVal *= volumeSmth; } int thisMax = map(thisVal, 0, 512, 0, rows); @@ -6475,7 +6477,7 @@ uint16_t mode_gravcenter_base(unsigned mode) { } else if(mode == 2) { //Gravimeter for (int i=0; itopLED > 0) { @@ -6497,7 +6499,7 @@ uint16_t mode_gravcenter_base(unsigned mode) { } else { //Gravcenter for (int i=0; iSEGLEN/2) maxLen = SEGLEN/2; for (unsigned i=(SEGLEN/2-maxLen); i<(SEGLEN/2+maxLen); i++) { - uint8_t index = inoise8(i*volumeSmth+SEGENV.aux0, SEGENV.aux1+i*volumeSmth); // Get a value from the noise function. I'm using both x and y axis. + 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)); } @@ -6647,7 +6649,7 @@ uint16_t mode_noisefire(void) { // Noisefire. By Andrew Tuline. if (SEGENV.call == 0) SEGMENT.fill(BLACK); for (unsigned i = 0; i < SEGLEN; i++) { - unsigned index = inoise8(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. + 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. @@ -6678,7 +6680,7 @@ uint16_t mode_noisemeter(void) { // Noisemeter. By Andrew Tuline. if (maxLen > SEGLEN) maxLen = SEGLEN; for (unsigned i=0; isetMotionBlur(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) + return mode_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 + return FRAMETIME; +} +#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 + +uint16_t mode_particlefireworks(void) { + ParticleSystem2D *PartSys = nullptr; + uint32_t numRockets; + + if (SEGMENT.call == 0) { // initialization + if (!initParticleSystem2D(PartSys, NUMBEROFSOURCES)) + return mode_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) + return mode_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 + return FRAMETIME; +} +#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 +uint16_t 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 + return mode_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) + return mode_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 + return FRAMETIME; +} +#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) +*/ +uint16_t 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.virtualWidth(), 4)) //maximum number of source (PS may limit based on segment size); need 4 additional bytes for time keeping (uint32_t lastcall) + return mode_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) + return mode_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 + + 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) + //still need to render the frame or flickering will occur in transitions + PartSys->updateFire(SEGMENT.intensity, true); // render the fire without updating particles (render only) + return FRAMETIME; //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.virtualHeight() >> 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(4) - 2; // emitting speed (sideways) + PartSys->sources[i].vy = (SEGMENT.virtualHeight() >> 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; + } + } + } + } + + 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, false); // update and render the fire + + return FRAMETIME; +} +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) +*/ +uint16_t mode_particlepit(void) { + ParticleSystem2D *PartSys = nullptr; + + if (SEGMENT.call == 0) { // initialization + if (!initParticleSystem2D(PartSys, 0, 0, true, false)) // init + return mode_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) + return mode_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->setParticleSize(1); // set global size to 1 for advanced rendering + 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 = 0; // use global size + } + 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 + + return FRAMETIME; +} +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=120,c2=130,c3=31,o3=1"; + +/* + Particle Waterfall + Uses palette for particle color, spray source at top emitting particles, many config options + by DedeHai (Damian Schneider) +*/ +uint16_t 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 + return mode_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) + return mode_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 + return FRAMETIME; +} +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) +*/ +uint16_t mode_particlebox(void) { + ParticleSystem2D *PartSys = nullptr; + uint32_t i; + + if (SEGMENT.call == 0) { // initialization + if (!initParticleSystem2D(PartSys, 1)) // init + return mode_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) + return mode_static(); // something went wrong, no data! + + PartSys->updateSystem(); // update system properties (dimensions and data pointers) + PartSys->setParticleSize(SEGMENT.custom3<<3); + 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 + PartSys->setUsedParticles(map(SEGMENT.intensity, 0, 255, 2, 153)); // 1% - 60% + // add in new particles if amount has changed + for (i = 0; i < PartSys->usedParticles; i++) { + if (PartSys->particles[i].ttl < 260) { // initialize handed over particles and 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 + 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 + + return FRAMETIME; +} +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) +*/ +uint16_t 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 + return mode_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) + return mode_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 + return FRAMETIME; +} +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 +uint16_t mode_particleimpact(void) { + ParticleSystem2D *PartSys = nullptr; + uint32_t i = 0; + 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 + return mode_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 (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) + return mode_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 (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 (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 + return FRAMETIME; +} +#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) +*/ +uint16_t 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 + return mode_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) + return mode_static(); // something went wrong, no data! + + // Particle System settings + PartSys->updateSystem(); // update system properties (dimensions and data pointers) + attractor = reinterpret_cast(PartSys->PSdataEnd); + + PartSys->setColorByAge(SEGMENT.check1); + PartSys->setParticleSize(SEGMENT.custom1 >> 1); //set size globally + PartSys->setUsedParticles(map(SEGMENT.intensity, 0, 255, 25, 190)); + + 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 == 0) { + attractor->vx = PartSys->sources[0].source.vy; // set to spray movemement but reverse x and y + attractor->vy = PartSys->sources[0].source.vx; + } + + // 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 % 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; + #ifdef USERMOD_AUDIOREACTIVE + 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; + } + #endif + 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 + return FRAMETIME; +} +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) +*/ +uint16_t 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 + return mode_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) + return mode_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; + + #ifdef USERMOD_AUDIOREACTIVE + 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); + } + } + #else + // change source properties + if (SEGMENT.call % (11 - (SEGMENT.intensity / 25)) == 0) { // every nth frame, cycle color and emit particles + PartSys->sources[0].maxLife = 300; // lifetime in frames. note: could be done in init part, but AR moderequires this to be dynamic + PartSys->sources[0].minLife = 100; + PartSys->sources[0].source.hue++; // = hw_random16(); //change hue of spray source + // PartSys->sources[i].var = SEGMENT.custom3; // emiting variation = nozzle size (custom 3 goes from 0-32) + // spray[j].source.hue = hw_random16(); //set random color for each particle (using palette) + PartSys->angleEmit(PartSys->sources[0], angle, SEGMENT.speed >> 2); + } + #endif + + PartSys->update(); // update and render + return FRAMETIME; +} +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) +*/ +uint16_t mode_particleGEQ(void) { + ParticleSystem2D *PartSys = nullptr; + + if (SEGMENT.call == 0) { // initialization + if (!initParticleSystem2D(PartSys, 1)) + return mode_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) + return mode_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 = PS_P_RADIUS; // start at the bottom (PS_P_RADIUS is minimum position a particle is fully in frame) + 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 + return FRAMETIME; +} + +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 +uint16_t 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 + return mode_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) + return mode_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 + return FRAMETIME; +} +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° +uint16_t 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 + return mode_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) + return mode_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 + return FRAMETIME; +} +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) +*/ +uint16_t 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 + return mode_static(); // allocation failed or not 2D + PartSys->setBounceX(true); + PartSys->setBounceY(true); + PartSys->setWallHardness(255); + PartSys->setWallRoughness(255); + PartSys->setCollisionHardness(255); + } + else + PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS + + if (PartSys == nullptr) + return mode_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; + + #ifdef USERMOD_AUDIOREACTIVE + um_data_t *um_data; + if (UsermodManager::getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) { // get AR data, 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; + } + } + #endif + + PartSys->setMotionBlur(((SEGMENT.custom3) << 3) + 7); + PartSys->update(); // update and render + + return FRAMETIME; +} +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"; +#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) +*/ +uint16_t mode_particleDrip(void) { + ParticleSystem1D *PartSys = nullptr; + //uint8_t numSprays; + if (SEGMENT.call == 0) { // initialization + if (!initParticleSystem1D(PartSys, 4)) // init + return mode_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) + return mode_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 + if (SEGMENT.speed > 200) + PartSys->particleMoveUpdate(PartSys->particles[i], PartSys->particleFlags[i]); + } + + PartSys->update(); // update and render + return FRAMETIME; +} +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 Replacement for "Bbouncing Balls by Aircoookie" + Also replaces rolling balls and juggle (and maybe popcorn) + Uses palette for particle color + by DedeHai (Damian Schneider) +*/ +uint16_t mode_particlePinball(void) { + ParticleSystem1D *PartSys = nullptr; + + if (SEGMENT.call == 0) { // initialization + if (!initParticleSystem1D(PartSys, 1, 128, 0, true)) // init + return mode_static(); // allocation failed or is single pixel + PartSys->sources[0].sourceFlags.collide = true; // seeded particles will collide (if enabled) + PartSys->sources[0].source.x = PS_P_RADIUS_1D; //emit at bottom + PartSys->setKillOutOfBounds(true); // out of bounds particles dont return + PartSys->setUsedParticles(255); // use all available particles for init + SEGENV.aux0 = 1; + SEGENV.aux1 = 5000; //set 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) + return mode_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 , 16)); // 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->setUsedParticles(SEGMENT.intensity); + PartSys->setColorByPosition(SEGMENT.check3); + + 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 ? 5000 : 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 = 260; // 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 (when not switching from a PS) + 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 = SEGMENT.custom1; + } + speedsum += abs(PartSys->particles[i].vx); + } + int32_t avgSpeed = speedsum / PartSys->usedParticles; + int32_t setSpeed = 2 + (SEGMENT.speed >> 3); + 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); + 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].vx == 0 && PartSys->particles[i].x < (PS_P_RADIUS_1D + SEGMENT.custom1)) + PartSys->particles[i].ttl = 0; + if (updateballs) { + PartSys->advPartProps[i].size = SEGMENT.custom1; + 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 = SEGMENT.custom1; + 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 + } + + PartSys->update(); // update and render + return FRAMETIME; +} +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) +*/ +uint16_t mode_particleDancingShadows(void) { + ParticleSystem1D *PartSys = nullptr; + + if (SEGMENT.call == 0) { // initialization + if (!initParticleSystem1D(PartSys, 1)) // init, one source + return mode_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) + return mode_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 + + return FRAMETIME; +} +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) +*/ +uint16_t mode_particleFireworks1D(void) { + ParticleSystem1D *PartSys = nullptr; + uint8_t *forcecounter; + + if (SEGMENT.call == 0) { // initialization + if (!initParticleSystem1D(PartSys, 4, 150, 4, true)) // init + return mode_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) + return mode_static(); // something went wrong, no data! + + // Particle System settings + PartSys->updateSystem(); // update system properties (dimensions and data pointers) + forcecounter = PartSys->PSdataEnd; + PartSys->setParticleSize(SEGMENT.check3); // 1 or 2 pixel rendering + PartSys->setMotionBlur(SEGMENT.custom2); // anable motion blur + + int32_t gravity = (1 + (SEGMENT.speed >> 3)); + if (!SEGMENT.check1) // gravity enabled for sparks + PartSys->setGravity(0); // disable + else + PartSys->setGravity(gravity); // set gravity + + 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(); + PartSys->sources[0].var = 10; // emit variation + PartSys->sources[0].v = -10; // emit speed + PartSys->sources[0].minLife = 100; + PartSys->sources[0].maxLife = 300; + PartSys->sources[0].source.x = 0; // start from bottom + 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].size = 0; // default size + 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 speed = PartSys->sources[0].source.vx; + if (SEGENV.aux0) { // negative speed rocket + rocketgravity = -rocketgravity; + speed = -speed; + } + 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 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 (speed < 0 && PartSys->sources[0].source.ttl > 50) // reached apogee + PartSys->sources[0].source.ttl = min((uint32_t)50, 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) * (200 + SEGMENT.intensity)) / (PartSys->maxX << 2)); // set explosion particle speed + PartSys->sources[0].minLife = 600; + PartSys->sources[0].maxLife = 1300; + PartSys->sources[0].source.ttl = 100 + hw_random16(64 - (SEGMENT.speed >> 2)); // standby time til next launch + PartSys->sources[0].sat = 7 + (SEGMENT.custom3 << 3); //color saturation TODO: replace saturation with something more useful? + PartSys->sources[0].size = hw_random16(64); // 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); + for (uint32_t e = 0; e < explosionsize; e++) { // emit explosion particles + if (SEGMENT.check2) + PartSys->sources[0].source.hue = hw_random16(); //random color for each particle + PartSys->sprayEmit(PartSys->sources[0]); // emit a 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 > 10) PartSys->particles[i].ttl -= 10; //ttl is linked to brightness, this allows to use higher brightness but still a short spark lifespan + else PartSys->particles[i].ttl = 0; + } + + return FRAMETIME; +} +static const char _data_FX_MODE_PS_FIREWORKS1D[] PROGMEM = "PS Fireworks 1D@Gravity,Explosion,Firing side,Blur,Saturation,,Colorful,Smooth;,!;!;1;sx=150,c2=30,c3=31,o2=1"; + +/* + Particle based Sparkle effect + Uses palette for particle color + by DedeHai (Damian Schneider) +*/ +uint16_t 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 + return mode_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) + return mode_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 + + 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); + uint32_t speed = SEGMENT.speed >> 1; + if (SEGMENT.check1) // sparks move (slide option) + PartSys->sources[i].var = SEGMENT.intensity >> 3; + PartSys->sources[i].source.vx = 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 + PartSys->sources[i].size = SEGMENT.check3 ? 120 : 0; + 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; + } + + return FRAMETIME; +} +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) +*/ +uint16_t 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 + return mode_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) + return mode_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, 32); // 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 + } + + // 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 closeToTarget = abs(targetposition - PartSys->particles[i].x) < 3 * PS_P_RADIUS_1D; + if (closeToTarget) { // 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 % colormode)*74; break; // interleved colors (every 2 or 3 particles) + 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; + } + } + if (SEGMENT.check1 && !PartSys->particleFlags[i].reversegrav) // flip color when fallen + PartSys->particles[i].hue += 120; + } + + // re-order particles in case 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); + } + } + + + 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, do not drop more often than every second frame or particles tangle up quite badly + // 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(20, SEGMENT.speed * 20); // map speed slider from 0.1s to 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.virtualLength() + 100; // set countdown + } + if (*direction == 0) // down, start dropping the highest number particle + SEGENV.aux0--; // next particle + else + SEGENV.aux0++; + } + } + else if (SEGMENT.check2) // auto reset + SEGENV.aux1--; // countdown + + PartSys->update(); // update and render + + return FRAMETIME; +} +static const char _data_FX_MODE_PS_HOURGLASS[] PROGMEM = "PS Hourglass@Interval,!,Color,Blur,Gravity,Colorflip,Start,Fast Reset;,!;!;1;pal=34,sx=50,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) +*/ +uint16_t mode_particle1Dspray(void) { + ParticleSystem1D *PartSys = nullptr; + + if (SEGMENT.call == 0) { // initialization + if (!initParticleSystem1D(PartSys, 1)) + return mode_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) + return mode_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 + + return FRAMETIME; +} +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) +*/ +uint16_t 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 + return mode_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) + return mode_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(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 (without collisions) + + //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 + return FRAMETIME; +} +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) +*/ +uint16_t mode_particleChase(void) { + ParticleSystem1D *PartSys = nullptr; + if (SEGMENT.call == 0) { // initialization + if (!initParticleSystem1D(PartSys, 1, 255, 2, true)) // init + return mode_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) + return mode_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, 2, 255 / (1 + (SEGMENT.custom1 >> 6))); // 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 << 5)) / numParticles; // spacing between particles + 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->setParticleSize(SEGMENT.custom1); // if custom1 == 0 this sets rendering size to one pixel + PartSys->update(); // update and render + return FRAMETIME; +} +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) +*/ +uint16_t mode_particleStarburst(void) { + ParticleSystem1D *PartSys = nullptr; + + if (SEGMENT.call == 0) { // initialization + if (!initParticleSystem1D(PartSys, 1, 200, 0, true)) // init + return mode_static(); // allocation failed or is single pixel + PartSys->setKillOutOfBounds(true); + PartSys->enableParticleCollisions(true, 200); + PartSys->sources[0].source.ttl = 1; // set initial stanby 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) + return mode_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 = 250; + 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->setParticleSize(SEGMENT.custom1); // enable advanced size rendering + 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 < 251) + PartSys->advPartProps[i].sat += 1 + (SEGMENT.custom3 >> 2); //note: it should be >> 3, the >> 2 creates overflows resulting in blinking if custom3 > 27, which is a bonus feature + } + + if (SEGMENT.call % 5 == 0) { + PartSys->applyFriction(1); //slow down particles + } + + PartSys->update(); // update and render + return FRAMETIME; +} +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) +*/ +uint16_t 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 + return mode_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) + return mode_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->setParticleSize(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 + + return FRAMETIME; +} +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) +*/ +uint16_t mode_particleFire1D(void) { + ParticleSystem1D *PartSys = nullptr; + + if (SEGMENT.call == 0) { // initialization + if (!initParticleSystem1D(PartSys, 5)) // init + return mode_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) + return mode_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 + + return FRAMETIME; +} +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) +*/ +uint16_t mode_particle1DsonicStream(void) { + ParticleSystem1D *PartSys = nullptr; + + if (SEGMENT.call == 0) { // initialization + if (!initParticleSystem1D(PartSys, 1, 255, 0, true)) // init, no additional data needed + return mode_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) + return mode_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]); + } + } + + return FRAMETIME; +} +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) +*/ +uint16_t mode_particle1DsonicBoom(void) { + ParticleSystem1D *PartSys = nullptr; + if (SEGMENT.call == 0) { // initialization + if (!initParticleSystem1D(PartSys, 1, 255, 0, true)) // init, no additional data needed + return mode_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) + return mode_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; + PartSys->sources[0].size = 1; //SEGMENT.speed>>3; + 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) + return FRAMETIME; +} +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) +*/ +uint16_t mode_particleSpringy(void) { + ParticleSystem1D *PartSys = nullptr; + if (SEGMENT.call == 0) { // initialization + if (!initParticleSystem1D(PartSys, 1, 128, 0, true)) // init + return mode_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) + return mode_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; + 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 + 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 + } + 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->applyForce(PartSys->particles[index], (sin16_t(phase) * amplitude) >> 15, PartSys->advPartProps[index].forcecounter); // apply acceleration + 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 + return FRAMETIME; +} +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 @@ -7678,7 +10418,7 @@ uint8_t WS2812FX::addEffect(uint8_t id, mode_ptr mode_fn, const char *mode_name) _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 + } 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++; @@ -7718,7 +10458,6 @@ void WS2812FX::setupEffectData() { 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_SPARKLE, &mode_sparkle, _data_FX_MODE_SPARKLE); 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); @@ -7739,15 +10478,14 @@ void WS2812FX::setupEffectData() { addEffect(FX_MODE_AURORA, &mode_aurora, _data_FX_MODE_AURORA); 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_COMET, &mode_comet, _data_FX_MODE_COMET); - addEffect(FX_MODE_FIREWORKS, &mode_fireworks, _data_FX_MODE_FIREWORKS); 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_ROLLINGBALLS, &rolling_balls, _data_FX_MODE_ROLLINGBALLS); - 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); @@ -7760,15 +10498,11 @@ void WS2812FX::setupEffectData() { 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_MULTI_COMET, &mode_multi_comet, _data_FX_MODE_MULTI_COMET); 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_PRIDE_2015, &mode_pride_2015, _data_FX_MODE_PRIDE_2015); addEffect(FX_MODE_JUGGLE, &mode_juggle, _data_FX_MODE_JUGGLE); addEffect(FX_MODE_PALETTE, &mode_palette, _data_FX_MODE_PALETTE); - addEffect(FX_MODE_FIRE_2012, &mode_fire_2012, _data_FX_MODE_FIRE_2012); - addEffect(FX_MODE_COLORWAVES, &mode_colorwaves, _data_FX_MODE_COLORWAVES); 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); @@ -7788,23 +10522,31 @@ void WS2812FX::setupEffectData() { 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); + #ifdef WLED_PS_DONT_REPLACE_FX + addEffect(FX_MODE_MULTI_COMET, &mode_multi_comet, _data_FX_MODE_MULTI_COMET); + addEffect(FX_MODE_ROLLINGBALLS, &rolling_balls, _data_FX_MODE_ROLLINGBALLS); + addEffect(FX_MODE_SPARKLE, &mode_sparkle, _data_FX_MODE_SPARKLE); addEffect(FX_MODE_GLITTER, &mode_glitter, _data_FX_MODE_GLITTER); - addEffect(FX_MODE_CANDLE, &mode_candle, _data_FX_MODE_CANDLE); + addEffect(FX_MODE_SOLID_GLITTER, &mode_solid_glitter, _data_FX_MODE_SOLID_GLITTER); addEffect(FX_MODE_STARBURST, &mode_starburst, _data_FX_MODE_STARBURST); + addEffect(FX_MODE_DANCING_SHADOWS, &mode_dancing_shadows, _data_FX_MODE_DANCING_SHADOWS); + 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_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_POPCORN, &mode_popcorn, _data_FX_MODE_POPCORN); - addEffect(FX_MODE_DRIP, &mode_drip, _data_FX_MODE_DRIP); 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_SOLID_GLITTER, &mode_solid_glitter, _data_FX_MODE_SOLID_GLITTER); 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); @@ -7812,10 +10554,8 @@ void WS2812FX::setupEffectData() { 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_DANCING_SHADOWS, &mode_dancing_shadows, _data_FX_MODE_DANCING_SHADOWS); + 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); @@ -7832,27 +10572,20 @@ void WS2812FX::setupEffectData() { 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); @@ -7861,16 +10594,17 @@ void WS2812FX::setupEffectData() { 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_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); @@ -7879,15 +10613,12 @@ void WS2812FX::setupEffectData() { 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); @@ -7895,7 +10626,6 @@ void WS2812FX::setupEffectData() { 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); @@ -7903,8 +10633,43 @@ void WS2812FX::setupEffectData() { 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); +#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 + } diff --git a/wled00/FX.h b/wled00/FX.h index 3b1f8f8f1..6481ff757 100644 --- a/wled00/FX.h +++ b/wled00/FX.h @@ -1,3 +1,4 @@ +#pragma once /* WS2812FX.h - Library for WS2812 LED effects. Harm Aldick - 2016 @@ -8,12 +9,15 @@ 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" #include "const.h" #include "bus_manager.h" @@ -71,18 +75,15 @@ extern byte realtimeMode; // used in getMappedPixelIndex() /* each segment uses 82 bytes of SRAM memory, so if you're application fails because of insufficient memory, decreasing MAX_NUM_SEGMENTS may help */ #ifdef ESP8266 - #define MAX_NUM_SEGMENTS 16 + #define MAX_NUM_SEGMENTS 16 /* How much data bytes all segments combined may allocate */ #define MAX_SEGMENT_DATA 5120 +#elif defined(CONFIG_IDF_TARGET_ESP32S2) + #define MAX_NUM_SEGMENTS 20 + #define MAX_SEGMENT_DATA (MAX_NUM_SEGMENTS*512) // 10k by default (S2 is short on free RAM) #else - #ifndef MAX_NUM_SEGMENTS - #define MAX_NUM_SEGMENTS 32 - #endif - #if defined(ARDUINO_ARCH_ESP32S2) - #define MAX_SEGMENT_DATA (MAX_NUM_SEGMENTS*768) // 24k by default (S2 is short on free RAM) - #else - #define MAX_SEGMENT_DATA (MAX_NUM_SEGMENTS*1280) // 40k by default - #endif + #define MAX_NUM_SEGMENTS 32 // warning: going beyond 32 may consume too much RAM for stable operation + #define MAX_SEGMENT_DATA (MAX_NUM_SEGMENTS*1280) // 40k by default #endif /* How much data bytes each segment should max allocate to leave enough space for other segments, @@ -322,8 +323,37 @@ extern byte realtimeMode; // used in getMappedPixelIndex() #define FX_MODE_WAVESINS 184 #define FX_MODE_ROCKTAVES 185 #define FX_MODE_2DAKEMI 186 - -#define MODE_COUNT 187 +#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 MODE_COUNT 217 #define BLEND_STYLE_FADE 0x00 // universal @@ -480,6 +510,7 @@ typedef struct Segment { uint8_t _prevPaletteBlends; // number of previous palette blends (there are max 255 blends possible) unsigned long _start; // must accommodate millis() uint16_t _dur; + // -> here is one byte of padding Transition(uint16_t dur=750) : _palT(CRGBPalette16(CRGB::Black)) , _prevPaletteBlends(0) @@ -571,11 +602,14 @@ typedef struct Segment { inline uint16_t groupLength() const { return grouping + spacing; } inline uint8_t getLightCapabilities() const { return _capabilities; } inline void deactivate() { setGeometry(0,0); } + inline Segment &clearName() { if (name) free(name); name = nullptr; return *this; } + inline Segment &setName(const String &name) { return setName(name.c_str()); } inline static unsigned getUsedSegmentData() { return Segment::_usedSegmentData; } inline static void addUsedSegmentData(int len) { Segment::_usedSegmentData += len; } #ifndef WLED_DISABLE_MODE_BLEND inline static void modeBlend(bool blend) { _modeBlend = blend; } + inline static bool getmodeBlend(void) { return _modeBlend; } #endif inline static unsigned vLength() { return Segment::_vLength; } inline static unsigned vWidth() { return Segment::_vWidth; } @@ -593,6 +627,7 @@ typedef struct Segment { Segment &setOption(uint8_t n, bool val); Segment &setMode(uint8_t fx, bool loadDefaults = false); Segment &setPalette(uint8_t pal); + Segment &setName(const char* name); uint8_t differs(const Segment& b) const; void refreshLightCapabilities(); @@ -623,6 +658,7 @@ typedef struct Segment { uint8_t currentMode() const; // currently active effect/mode (while in transition) [[gnu::hot]] uint32_t currentColor(uint8_t slot) const; // currently active segment color (blended while in transition) CRGBPalette16 &loadPalette(CRGBPalette16 &tgt, uint8_t pal); + void loadOldPalette(); // loads old FX palette into _currentPalette // 1D strip [[gnu::hot]] uint16_t virtualLength() const; @@ -642,8 +678,10 @@ typedef struct Segment { [[gnu::hot]] uint32_t getPixelColor(int i) const; // 1D support functions (some implement 2D as well) void blur(uint8_t, bool smear = false); + void clear(); void fill(uint32_t c); void fade_out(uint8_t r); + void fadeToSecondaryBy(uint8_t fadeBy); void fadeToBlackBy(uint8_t fadeBy); inline void blendPixelColor(int n, uint32_t color, uint8_t blend) { setPixelColor(n, color_blend(getPixelColor(n), color, blend)); } inline void blendPixelColor(int n, CRGB c, uint8_t blend) { blendPixelColor(n, RGBW32(c.r,c.g,c.b,0), blend); } @@ -674,7 +712,6 @@ typedef struct Segment { } #ifndef WLED_DISABLE_2D inline bool is2D() const { return (width()>1 && height()>1); } - [[gnu::hot]] int XY(int x, int y) const; // support function to get relative index within segment [[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)); } @@ -711,8 +748,7 @@ typedef struct Segment { void wu_pixel(uint32_t x, uint32_t y, CRGB c); inline void fill_solid(CRGB c) { fill(RGBW32(c.r,c.g,c.b,0)); } #else - inline constexpr bool is2D() const { return false; } - inline int XY(int x, int y) const { return x; } + inline bool is2D() const { return false; } inline void setPixelColorXY(int x, int y, uint32_t c) { setPixelColor(x, c); } inline void setPixelColorXY(unsigned x, unsigned y, uint32_t c) { setPixelColor(int(x), c); } inline void setPixelColorXY(int x, int y, byte r, byte g, byte b, byte w = 0) { setPixelColor(x, RGBW32(r,g,b,w)); } @@ -1007,4 +1043,4 @@ class WS2812FX { // 96 bytes extern const char JSON_mode_names[]; extern const char JSON_palette_names[]; -#endif +#endif \ No newline at end of file diff --git a/wled00/FX_2Dfcn.cpp b/wled00/FX_2Dfcn.cpp index 67624bac3..61d14a12b 100644 --- a/wled00/FX_2Dfcn.cpp +++ b/wled00/FX_2Dfcn.cpp @@ -145,14 +145,6 @@ void WS2812FX::setUpMatrix() { #ifndef WLED_DISABLE_2D -// XY(x,y) - gets pixel index within current segment (often used to reference leds[] array element) -int IRAM_ATTR_YN Segment::XY(int x, int y) const -{ - 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) - return isActive() ? (x%vW) + (y%vH) * vW : 0; -} - // raw setColor function without checks (checks are done in setPixelColorXY()) void IRAM_ATTR_YN Segment::_setPixelColorXY_raw(const int& x, const int& y, uint32_t& col) const { @@ -166,16 +158,11 @@ void IRAM_ATTR_YN Segment::_setPixelColorXY_raw(const int& x, const int& y, uint // Apply mirroring if (mirror || mirror_y) { - auto setMirroredPixel = [&](int mx, int my) { - strip.setPixelColorXY(mx, my, col); - }; - const int mirrorX = start + width() - x - 1; const int mirrorY = startY + height() - y - 1; - - if (mirror) setMirroredPixel(transpose ? baseX : mirrorX, transpose ? mirrorY : baseY); - if (mirror_y) setMirroredPixel(transpose ? mirrorX : baseX, transpose ? baseY : mirrorY); - if (mirror && mirror_y) setMirroredPixel(mirrorX, mirrorY); + if (mirror) strip.setPixelColorXY(transpose ? baseX : mirrorX, transpose ? mirrorY : baseY, col); + if (mirror_y) strip.setPixelColorXY(transpose ? mirrorX : baseX, transpose ? baseY : mirrorY, col); + if (mirror && mirror_y) strip.setPixelColorXY(mirrorX, mirrorY, col); } } @@ -697,9 +684,7 @@ void Segment::drawCharacter(unsigned char chr, int16_t x, int16_t y, uint8_t w, case 60: bits = pgm_read_byte_near(&console_font_5x12[(chr * h) + i]); break; // 5x12 font default: return; } - uint32_t c = ColorFromPaletteWLED(grad, (i+1)*255/h, 255, NOBLEND); - // pre-scale color for all pixels - c = color_fade(c, _segBri); + CRGBW c = ColorFromPalette(grad, (i+1)*255/h, _segBri, LINEARBLEND_NOWRAP); _colorScaled = true; for (int j = 0; j= (int)vWidth() || y0 < 0 || y0 >= (int)vHeight()) continue; // drawing off-screen if (((bits>>(j+(8-w))) & 0x01)) { // bit set - setPixelColorXY(x0, y0, c); + setPixelColorXY(x0, y0, c.color32); } } _colorScaled = false; diff --git a/wled00/FX_fcn.cpp b/wled00/FX_fcn.cpp index cf37a46c2..42403fa85 100644 --- a/wled00/FX_fcn.cpp +++ b/wled00/FX_fcn.cpp @@ -11,6 +11,7 @@ */ #include "wled.h" #include "FX.h" +#include "FXparticleSystem.h" // TODO: better define the required function (mem service) in FX.h? #include "palettes.h" /* @@ -269,7 +270,7 @@ void Segment::startTransition(uint16_t dur) { _t->_briT = on ? opacity : 0; _t->_cctT = cct; #ifndef WLED_DISABLE_MODE_BLEND - swapSegenv(_t->_segT); + swapSegenv(_t->_segT); // copy runtime data to temporary _t->_modeT = mode; _t->_segT._dataLenT = 0; _t->_segT._dataT = nullptr; @@ -281,6 +282,13 @@ void Segment::startTransition(uint16_t dur) { _t->_segT._dataLenT = _dataLen; } } + DEBUG_PRINTF_P(PSTR("-- pal: %d, bri: %d, C:[%08X,%08X,%08X], m: %d\n"), + (int)_t->_palTid, + (int)_t->_briT, + _t->_segT._colorT[0], + _t->_segT._colorT[1], + _t->_segT._colorT[2], + (int)_t->_modeT); #else for (size_t i=0; i_colorT[i] = colors[i]; #endif @@ -470,6 +478,12 @@ void Segment::beginDraw() { } } +// loads palette of the old FX during transitions (used by particle system) +void Segment::loadOldPalette(void) { + if(isInTransition()) + loadPalette(_currentPalette, _t->_palTid); +} + // relies on WS2812FX::service() to call it for each frame void Segment::handleRandomPalette() { // is it time to generate a new palette? @@ -495,21 +509,17 @@ void Segment::setGeometry(uint16_t i1, uint16_t i2, uint8_t grp, uint8_t spc, ui #ifndef WLED_DISABLE_2D if (Segment::maxHeight>1) boundsUnchanged &= (startY == i1Y && stopY == i2Y); // 2D #endif + + if (stop && (spc > 0 || m12 != map1D2D)) clear(); +/* if (boundsUnchanged && (!grp || (grouping == grp && spacing == spc)) && (ofs == UINT16_MAX || ofs == offset) && (m12 == map1D2D) ) return; - +*/ stateChanged = true; // send UDP/WS broadcast - if (stop || spc != spacing || m12 != map1D2D) { - _vWidth = virtualWidth(); - _vHeight = virtualHeight(); - _vLength = virtualLength(); - _segBri = currentBri(); - fill(BLACK); // turn old segment range off or clears pixels if changing spacing (requires _vWidth/_vHeight/_vLength/_segBri) - } if (grp) { // prevent assignment of 0 grouping = grp; spacing = spc; @@ -520,10 +530,7 @@ void Segment::setGeometry(uint16_t i1, uint16_t i2, uint8_t grp, uint8_t spc, ui if (ofs < UINT16_MAX) offset = ofs; map1D2D = constrain(m12, 0, 7); - DEBUG_PRINT(F("setUp segment: ")); DEBUG_PRINT(i1); - DEBUG_PRINT(','); DEBUG_PRINT(i2); - DEBUG_PRINT(F(" -> ")); DEBUG_PRINT(i1Y); - DEBUG_PRINT(','); DEBUG_PRINTLN(i2Y); + DEBUG_PRINTF_P(PSTR("Segment geometry: %d,%d -> %d,%d\n"), (int)i1, (int)i2, (int)i1Y, (int)i2Y); markForReset(); if (boundsUnchanged) return; @@ -649,6 +656,20 @@ Segment &Segment::setPalette(uint8_t pal) { 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) name = static_cast(realloc(name, newLen+1)); + else name = static_cast(malloc(newLen+1)); + if (name) strlcpy(name, newName, newLen+1); + name[newLen] = 0; + return *this; + } + } + return clearName(); +} + // 2D matrix unsigned Segment::virtualWidth() const { unsigned groupLen = groupLength(); @@ -666,37 +687,25 @@ unsigned Segment::virtualHeight() const { // Constants for mapping mode "Pinwheel" #ifndef WLED_DISABLE_2D -constexpr int Pinwheel_Steps_Small = 72; // no holes up to 16x16 -constexpr int Pinwheel_Size_Small = 16; // larger than this -> use "Medium" -constexpr int Pinwheel_Steps_Medium = 192; // no holes up to 32x32 -constexpr int Pinwheel_Size_Medium = 32; // larger than this -> use "Big" -constexpr int Pinwheel_Steps_Big = 304; // no holes up to 50x50 -constexpr int Pinwheel_Size_Big = 50; // larger than this -> use "XL" -constexpr int Pinwheel_Steps_XL = 368; -constexpr float Int_to_Rad_Small = (DEG_TO_RAD * 360) / Pinwheel_Steps_Small; // conversion: from 0...72 to Radians -constexpr float Int_to_Rad_Med = (DEG_TO_RAD * 360) / Pinwheel_Steps_Medium; // conversion: from 0...192 to Radians -constexpr float Int_to_Rad_Big = (DEG_TO_RAD * 360) / Pinwheel_Steps_Big; // conversion: from 0...304 to Radians -constexpr float Int_to_Rad_XL = (DEG_TO_RAD * 360) / Pinwheel_Steps_XL; // conversion: from 0...368 to Radians - -constexpr int Fixed_Scale = 512; // fixpoint scaling factor (9bit for fraction) - -// Pinwheel helper function: pixel index to radians -static float getPinwheelAngle(int i, int vW, int vH) { - int maxXY = max(vW, vH); - if (maxXY <= Pinwheel_Size_Small) return float(i) * Int_to_Rad_Small; - if (maxXY <= Pinwheel_Size_Medium) return float(i) * Int_to_Rad_Med; - if (maxXY <= Pinwheel_Size_Big) return float(i) * Int_to_Rad_Big; - // else - return float(i) * Int_to_Rad_XL; -} +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) { - int maxXY = max(vW, vH); - if (maxXY <= Pinwheel_Size_Small) return Pinwheel_Steps_Small; - if (maxXY <= Pinwheel_Size_Medium) return Pinwheel_Steps_Medium; - if (maxXY <= Pinwheel_Size_Big) return Pinwheel_Steps_Big; - // else - return Pinwheel_Steps_XL; + // 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 @@ -754,7 +763,7 @@ bool IRAM_ATTR_YN Segment::isPixelClipped(int i) const { //if (!invert && iInside) return _modeBlend; //if ( invert && !iInside) return _modeBlend; //return !_modeBlend; - return !iInside ^ invert ^ _modeBlend; // thanks @willmmiles (https://github.com/Aircoookie/WLED/pull/3877#discussion_r1554633876) + return !iInside ^ invert ^ _modeBlend; // thanks @willmmiles (https://github.com/wled-dev/WLED/pull/3877#discussion_r1554633876) } #endif return false; @@ -831,55 +840,103 @@ void IRAM_ATTR_YN Segment::setPixelColor(int i, uint32_t col) const for (int x = 0; x <= i; x++) setPixelColorXY(x, i, col); for (int y = 0; y < i; y++) setPixelColorXY(i, y, col); break; - case M12_sPinwheel: { - // i = angle --> 0 - 296 (Big), 0 - 192 (Medium), 0 - 72 (Small) - float centerX = roundf((vW-1) / 2.0f); - float centerY = roundf((vH-1) / 2.0f); - float angleRad = getPinwheelAngle(i, vW, vH); // angle in radians - float cosVal = cos_t(angleRad); - float sinVal = sin_t(angleRad); - - // avoid re-painting the same pixel - int lastX = INT_MIN; // impossible position - int lastY = INT_MIN; // impossible position - // draw line at angle, starting at center and ending at the segment edge - // we use fixed point math for better speed. Starting distance is 0.5 for better rounding - // int_fast16_t and int_fast32_t types changed to int, minimum bits commented - int posx = (centerX + 0.5f * cosVal) * Fixed_Scale; // X starting position in fixed point 18 bit - int posy = (centerY + 0.5f * sinVal) * Fixed_Scale; // Y starting position in fixed point 18 bit - int inc_x = cosVal * Fixed_Scale; // X increment per step (fixed point) 10 bit - int inc_y = sinVal * Fixed_Scale; // Y increment per step (fixed point) 10 bit - - int32_t maxX = vW * Fixed_Scale; // X edge in fixedpoint - int32_t maxY = vH * Fixed_Scale; // Y edge in fixedpoint - - // Odd rays start further from center if prevRay started at center. - static int prevRay = INT_MIN; // previous ray number - if ((i % 2 == 1) && (i - 1 == prevRay || i + 1 == prevRay)) { - int jump = min(vW/3, vH/3); // can add 2 if using medium pinwheel - posx += inc_x * jump; - posy += inc_y * jump; + 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= vW || unsigned(y0) >= 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; } - prevRay = i; - - // draw ray until we hit any edge - while ((posx >= 0) && (posy >= 0) && (posx < maxX) && (posy < maxY)) { - // scale down to integer (compiler will replace division with appropriate bitshift) - int x = posx / Fixed_Scale; - int y = posy / Fixed_Scale; - // set pixel - if (x != lastX || y != lastY) setPixelColorXY(x, y, col); // only paint if pixel position is different - lastX = x; - lastY = y; - // advance to next position - posx += inc_x; - posy += inc_y; - } - break; } - } - _colorScaled = false; - return; + 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) @@ -1011,31 +1068,17 @@ uint32_t IRAM_ATTR_YN Segment::getPixelColor(int i) const break; case M12_sPinwheel: // not 100% accurate, returns pixel at outer edge - // i = angle --> 0 - 296 (Big), 0 - 192 (Medium), 0 - 72 (Small) - float centerX = roundf((vW-1) / 2.0f); - float centerY = roundf((vH-1) / 2.0f); - float angleRad = getPinwheelAngle(i, vW, vH); // angle in radians - float cosVal = cos_t(angleRad); - float sinVal = sin_t(angleRad); - - int posx = (centerX + 0.5f * cosVal) * Fixed_Scale; // X starting position in fixed point 18 bit - int posy = (centerY + 0.5f * sinVal) * Fixed_Scale; // Y starting position in fixed point 18 bit - int inc_x = cosVal * Fixed_Scale; // X increment per step (fixed point) 10 bit - int inc_y = sinVal * Fixed_Scale; // Y increment per step (fixed point) 10 bit - int32_t maxX = vW * Fixed_Scale; // X edge in fixedpoint - int32_t maxY = vH * Fixed_Scale; // Y edge in fixedpoint - - // trace ray from center until we hit any edge - to avoid rounding problems, we use the same method as in setPixelColor - int x = INT_MIN; - int y = INT_MIN; - while ((posx >= 0) && (posy >= 0) && (posx < maxX) && (posy < maxY)) { - // scale down to integer (compiler will replace division with appropriate bitshift) - x = posx / Fixed_Scale; - y = posy / Fixed_Scale; - // advance to next position - posx += inc_x; - posy += inc_y; + int x, y, 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; return getPixelColorXY(x, y); break; } @@ -1117,12 +1160,9 @@ void Segment::refreshLightCapabilities() { } for (unsigned b = 0; b < BusManager::getNumBusses(); b++) { - Bus *bus = BusManager::getBus(b); - if (bus == nullptr || bus->getLength()==0) break; - if (!bus->isOk()) continue; - if (bus->getStart() >= segStopIdx) continue; - if (bus->getStart() + bus->getLength() <= segStartIdx) continue; - + const Bus *bus = BusManager::getBus(b); + if (!bus || !bus->isOk()) break; + if (bus->getStart() >= segStopIdx || bus->getStart() + bus->getLength() <= segStartIdx) continue; 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) @@ -1138,6 +1178,26 @@ void Segment::refreshLightCapabilities() { _capabilities = capabilities; } +/* + * Fills segment with black + */ +void Segment::clear() { + if (!isActive()) return; // not active + unsigned oldVW = _vWidth; + unsigned oldVH = _vHeight; + unsigned oldVL = _vLength; + unsigned oldSB = _segBri; + _vWidth = virtualWidth(); + _vHeight = virtualHeight(); + _vLength = virtualLength(); + _segBri = currentBri(); + fill(BLACK); + _vWidth = oldVW; + _vHeight = oldVH; + _vLength = oldVL; + _segBri = oldSB; +} + /* * Fills segment with color */ @@ -1157,42 +1217,45 @@ void Segment::fill(uint32_t c) { /* * 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) { if (!isActive()) return; // not active const int cols = is2D() ? vWidth() : vLength(); const int rows = vHeight(); // will be 1 for 1D - rate = (255-rate) >> 1; - float mappedRate = 1.0f / (float(rate) + 1.1f); - - uint32_t color = colors[1]; // SEGCOLOR(1); // target color - int w2 = W(color); - int r2 = R(color); - int g2 = G(color); - int b2 = B(color); + rate = (256-rate) >> 1; + const int mappedRate = 256 / (rate + 1); for (int y = 0; y < rows; y++) for (int x = 0; x < cols; x++) { - color = is2D() ? getPixelColorXY(x, y) : getPixelColor(x); + uint32_t color = is2D() ? getPixelColorXY(x, y) : getPixelColor(x); if (color == colors[1]) continue; // already at target color - int w1 = W(color); - int r1 = R(color); - int g1 = G(color); - int b1 = B(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< w1) ? 1 : -1; - rdelta += (r2 == r1) ? 0 : (r2 > r1) ? 1 : -1; - gdelta += (g2 == g1) ? 0 : (g2 > g1) ? 1 : -1; - bdelta += (b2 == b1) ? 0 : (b2 > b1) ? 1 : -1; - - if (is2D()) setPixelColorXY(x, y, r1 + rdelta, g1 + gdelta, b1 + bdelta, w1 + wdelta); - else setPixelColor(x, r1 + rdelta, g1 + gdelta, b1 + bdelta, w1 + wdelta); + for (int y = 0; y < rows; y++) for (int x = 0; x < cols; x++) { + if (is2D()) setPixelColorXY(x, y, color_blend(getPixelColorXY(x,y), colors[1], fadeBy)); + else setPixelColor(x, color_blend(getPixelColor(x), colors[1], fadeBy)); } } @@ -1270,12 +1333,12 @@ uint32_t Segment::color_wheel(uint8_t pos) const { * 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 wrap FastLED palettes will usually wrap back to the start smoothly. Set false to get a hard edge + * @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 wrap, uint8_t mcol, uint8_t pbri) const { +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 < NUM_COLORS ? mcol : 0); // default palette or no RGB support on segment if ((palette == 0 && mcol < NUM_COLORS) || !_isRGB) { @@ -1285,9 +1348,15 @@ uint32_t Segment::color_from_palette(uint16_t i, bool mapping, bool wrap, uint8_ const int vL = vLength(); unsigned paletteIndex = i; if (mapping && vL > 1) paletteIndex = (i*255)/(vL -1); - // paletteBlend: 0 - wrap when moving, 1 - always wrap, 2 - never wrap, 3 - none (undefined) - if (!wrap && strip.paletteBlend != 3) paletteIndex = scale8(paletteIndex, 240); //cut off blend at palette "end" - CRGBW palcol = ColorFromPaletteWLED(_currentPalette, paletteIndex, pbri, (strip.paletteBlend == 3)? NOBLEND:LINEARBLEND); // NOTE: paletteBlend should be global + // 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 (strip.paletteBlend) { // NOTE: paletteBlend should be global + 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; @@ -1311,6 +1380,34 @@ void WS2812FX::finalizeInit() { _hasWhiteChannel = _isOffRefreshRequired = false; + unsigned digitalCount = 0; + #if defined(ARDUINO_ARCH_ESP32) && !defined(CONFIG_IDF_TARGET_ESP32C3) + // determine if it is sensible to use parallel I2S outputs on ESP32 (i.e. more than 5 outputs = 1 I2S + 4 RMT) + unsigned maxLedsOnBus = 0; + for (const auto &bus : busConfigs) { + if (Bus::isDigital(bus.type) && !Bus::is2Pin(bus.type)) { + digitalCount++; + if (bus.count > maxLedsOnBus) maxLedsOnBus = bus.count; + } + } + DEBUG_PRINTF_P(PSTR("Maximum LEDs on a bus: %u\nDigital buses: %u\n"), maxLedsOnBus, digitalCount); + // we may remove 300 LEDs per bus limit when NeoPixelBus is updated beyond 2.9.0 + if (maxLedsOnBus <= 300 && useParallelI2S) BusManager::useParallelOutput(); // must call before creating buses + else useParallelI2S = false; // enforce single I2S + #endif + + // create buses/outputs + unsigned mem = 0; + digitalCount = 0; + for (const auto &bus : busConfigs) { + mem += bus.memUsage(Bus::isDigital(bus.type) && !Bus::is2Pin(bus.type) ? digitalCount++ : 0); // includes global buffer + if (mem <= MAX_LED_MEMORY) { + if (BusManager::add(bus) == -1) break; + } else DEBUG_PRINTF_P(PSTR("Out of LED memory! Bus %d (%d) #%u not created."), (int)bus.type, (int)bus.count, digitalCount); + } + busConfigs.clear(); + busConfigs.shrink_to_fit(); + //if busses failed to load, add default (fresh install, FS issue, ...) if (BusManager::getNumBusses() == 0) { DEBUG_PRINTLN(F("No busses, init default")); @@ -1326,6 +1423,7 @@ void WS2812FX::finalizeInit() { unsigned prevLen = 0; unsigned pinsIndex = 0; + digitalCount = 0; for (unsigned i = 0; i < WLED_MAX_BUSSES+WLED_MIN_VIRTUAL_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 @@ -1390,15 +1488,16 @@ void WS2812FX::finalizeInit() { if (Bus::isPWM(dataType) || Bus::isOnOff(dataType)) count = 1; prevLen += count; BusConfig defCfg = BusConfig(dataType, defPin, start, count, DEFAULT_LED_COLOR_ORDER, false, 0, RGBW_MODE_MANUAL_ONLY, 0, useGlobalLedBuffer); + mem += defCfg.memUsage(Bus::isDigital(dataType) && !Bus::is2Pin(dataType) ? digitalCount++ : 0); if (BusManager::add(defCfg) == -1) break; } } + DEBUG_PRINTF_P(PSTR("LED buffer size: %uB/%uB\n"), mem, BusManager::memUsage()); _length = 0; for (int i=0; igetStart() + bus->getLength() > MAX_LEDS) break; + if (!bus || !bus->isOk() || 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. @@ -1408,7 +1507,9 @@ void WS2812FX::finalizeInit() { // This must be done after all buses have been created, as some kinds (parallel I2S) interact bus->begin(); + bus->setBrightness(bri); } + DEBUG_PRINTF_P(PSTR("Heap after buses: %d\n"), ESP.getFreeHeap()); Segment::maxWidth = _length; Segment::maxHeight = 1; @@ -1437,7 +1538,7 @@ void WS2812FX::service() { _segment_index = 0; for (segment &seg : _segments) { - if (_suspend) return; // immediately stop processing segments if suspend requested during service() + if (_suspend) break; // immediately stop processing segments if suspend requested during service() // process transition (mode changes in the middle of transition) seg.handleTransition(); @@ -1468,6 +1569,11 @@ void WS2812FX::service() { #ifndef WLED_DISABLE_MODE_BLEND Segment::setClippingRect(0, 0); // disable clipping (just in case) if (seg.isInTransition()) { + // a hack to determine if effect has changed + uint8_t m = seg.currentMode(); + Segment::modeBlend(true); // set semaphore + bool sameEffect = (m == seg.currentMode()); + Segment::modeBlend(false); // clear semaphore // set clipping rectangle // new mode is run inside clipping area and old mode outside clipping area unsigned p = seg.progress(); @@ -1476,7 +1582,20 @@ void WS2812FX::service() { unsigned dw = p * w / 0xFFFFU + 1; unsigned dh = p * h / 0xFFFFU + 1; unsigned orgBS = blendingStyle; - if (w*h == 1) blendingStyle = BLEND_STYLE_FADE; // disable belending for single pixel segments (use fade instead) + if (w*h == 1) blendingStyle = BLEND_STYLE_FADE; // disable style for single pixel segments (use fade instead) + else if (sameEffect && (blendingStyle & BLEND_STYLE_PUSH_MASK)) { + // when effect stays the same push will look awful, change it to swipe + switch (blendingStyle) { + case BLEND_STYLE_PUSH_BR: + case BLEND_STYLE_PUSH_TR: + case BLEND_STYLE_PUSH_RIGHT: blendingStyle = BLEND_STYLE_SWIPE_RIGHT; break; + case BLEND_STYLE_PUSH_BL: + case BLEND_STYLE_PUSH_TL: + case BLEND_STYLE_PUSH_LEFT: blendingStyle = BLEND_STYLE_SWIPE_LEFT; break; + case BLEND_STYLE_PUSH_DOWN: blendingStyle = BLEND_STYLE_SWIPE_DOWN; break; + case BLEND_STYLE_PUSH_UP: blendingStyle = BLEND_STYLE_SWIPE_UP; break; + } + } switch (blendingStyle) { case BLEND_STYLE_FAIRY_DUST: // fairy dust (must set entire segment, see isPixelXYClipped()) Segment::setClippingRect(0, w, 0, h); @@ -1522,7 +1641,7 @@ void WS2812FX::service() { Segment::setClippingRect(0, dw, h - dh, h); break; } - frameDelay = (*_mode[seg.currentMode()])(); // run new/current mode + frameDelay = (*_mode[m])(); // run new/current mode // now run old/previous mode Segment::tmpsegd_t _tmpSegData; Segment::modeBlend(true); // set semaphore @@ -1555,8 +1674,8 @@ void WS2812FX::service() { if (doShow) { yield(); Segment::handleRandomPalette(); // slowly transition random palette; move it into for loop when each segment has individual random palette - show(); _lastServiceShow = nowUp; // update timestamp, for precise FPS control + if (!_suspend) 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); @@ -1691,8 +1810,8 @@ uint16_t WS2812FX::getLengthPhysical() const { //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++) { - Bus *bus = BusManager::getBus(b); - if (bus == nullptr || bus->getLength()==0) break; + const Bus *bus = BusManager::getBus(b); + if (!bus || !bus->isOk()) break; if (bus->hasRGB() && bus->hasWhite()) return true; } return false; @@ -1701,8 +1820,8 @@ bool WS2812FX::hasRGBWBus() const { bool WS2812FX::hasCCTBus() const { if (cctFromRgb && !correctWB) return false; for (size_t b = 0; b < BusManager::getNumBusses(); b++) { - Bus *bus = BusManager::getBus(b); - if (bus == nullptr || bus->getLength()==0) break; + const Bus *bus = BusManager::getBus(b); + if (!bus || !bus->isOk()) break; if (bus->hasCCT()) return true; } return false; @@ -1755,10 +1874,11 @@ void WS2812FX::makeAutoSegments(bool forceReset) { #endif for (size_t i = s; i < BusManager::getNumBusses(); i++) { - Bus* b = BusManager::getBus(i); + const Bus *bus = BusManager::getBus(i); + if (!bus || !bus->isOk()) break; - segStarts[s] = b->getStart(); - segStops[s] = segStarts[s] + b->getLength(); + 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 @@ -1848,7 +1968,8 @@ 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; @@ -1945,12 +2066,17 @@ bool WS2812FX::deserializeMap(unsigned n) { if (!isFile || !requestJSONBufferLock(7)) return false; - if (!readObjectFromFile(fileName, nullptr, pDoc)) { + StaticJsonDocument<64> filter; + filter[F("width")] = true; + filter[F("height")] = true; + if (!readObjectFromFile(fileName, nullptr, pDoc, &filter)) { DEBUG_PRINT(F("ERROR Invalid ledmap in ")); DEBUG_PRINTLN(fileName); releaseJSONBufferLock(); return false; // if file does not load properly then exit } + suspend(); + 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 (isMatrix && n == 0 && (!root[F("width")].isNull() || !root[F("height")].isNull())) { @@ -1963,16 +2089,52 @@ bool WS2812FX::deserializeMap(unsigned n) { if (customMappingTable) { DEBUG_PRINT(F("Reading LED map from ")); DEBUG_PRINTLN(fileName); + 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 > 16384) index = 0xFFFF; + 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); } @@ -1990,4 +2152,4 @@ const char JSON_palette_names[] PROGMEM = R"=====([ "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" -])====="; +])====="; \ No newline at end of file diff --git a/wled00/FXparticleSystem.cpp b/wled00/FXparticleSystem.cpp new file mode 100644 index 000000000..fadc98763 --- /dev/null +++ b/wled00/FXparticleSystem.cpp @@ -0,0 +1,1931 @@ +/* + 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 void fast_color_add(CRGB &c1, const CRGB &c2, uint8_t scale = 255); // fast and accurate color adding with scaling (scales c2 before adding) +static void fast_color_scale(CRGB &c, const uint8_t scale); // fast scaling function using 32bit variable and pointer. note: keep 'scale' within 0-255 +//static CRGB *allocateCRGBbuffer(uint32_t length); +#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 + 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 + } + +} + +// 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) { + 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,const bool renderonly) { + if (!renderonly) + 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 = (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) { + if (particlesize < 2) // only allow motion blurring on default particle sizes or advanced size (cannot combine motion blur with normal blurring used for particlesize, would require another buffer) + motionBlur = bluramount; +} + +void ParticleSystem2D::setSmearBlur(uint8_t bluramount) { + smearBlur = bluramount; +} + + +// render size using smearing (see blur function) +void ParticleSystem2D::setParticleSize(uint8_t size) { + particlesize = size; + particleHardRadius = PS_P_MINHARDRADIUS; // ~1 pixel + if (particlesize > 1) { + particleHardRadius = max(particleHardRadius, (uint32_t)particlesize); // radius used for wall collisions & particle collisions + motionBlur = 0; // disable motion blur if particle size is set + } + else if (particlesize == 0) + particleHardRadius = particleHardRadius >> 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; + particles[emitIndex].vx = emitter.vx + hw_random16(emitter.var << 1) - emitter.var; // random(-var, var) + particles[emitIndex].vy = emitter.vy + hw_random16(emitter.var << 1) - emitter.var; // random(-var, var) + 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) + 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 for positive and negative numbers 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; // used to check out of bounds + 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 (advancedproperties) { //using individual particle size? + setParticleSize(particlesize); // updates default particleHardRadius + if (advancedproperties->size > PS_P_MINHARDRADIUS) { + particleHardRadius += (advancedproperties->size - PS_P_MINHARDRADIUS); // update radius + renderradius = particleHardRadius; + } + } + // 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 for positive and negative numbers 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 for positive and negative numbers 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 for positive and negative numbers 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() { + CRGB baseRGB; + uint32_t brightness; // particle brightness, fades if dying + + 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++) { + 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(CRGB)); + } + + // 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)) + 20; + brightness = min(brightness, (uint32_t)255); + baseRGB = ColorFromPaletteWLED(SEGPALETTE, brightness, 255); + } + else { + brightness = min((particles[i].ttl << 1), (int)255); + baseRGB = ColorFromPaletteWLED(SEGPALETTE, particles[i].hue, 255); + if (particles[i].sat < 255) { + CHSV32 baseHSV; + rgb2hsv((uint32_t((byte(baseRGB.r) << 16) | (byte(baseRGB.g) << 8) | (byte(baseRGB.b)))), baseHSV); // convert to HSV + baseHSV.s = particles[i].sat; // set the saturation + uint32_t tempcolor; + hsv2rgb(baseHSV, tempcolor); // convert back to RGB + baseRGB = (CRGB)tempcolor; + } + } + renderParticle(i, brightness, baseRGB, particlesettings.wrapX, particlesettings.wrapY); + } + + if (particlesize > 1) { + uint32_t passes = particlesize / 64 + 1; // number of blur passes, four passes max + uint32_t bluramount = particlesize; + uint32_t bitshift = 0; + for (uint32_t i = 0; i < passes; i++) { + if (i == 2) // for the last two passes, use higher amount of blur (results in a nicer brightness gradient with soft edges) + bitshift = 1; + blur2D(framebuffer, maxXpixel + 1, maxYpixel + 1, bluramount << bitshift, bluramount << bitshift); + bluramount -= 64; + } + } + + // apply 2D blur to rendered frame + if (smearBlur) { + blur2D(framebuffer, maxXpixel + 1, maxYpixel + 1, smearBlur, smearBlur); + } + + // transfer the framebuffer to the segment + for (int y = 0; y <= maxYpixel; y++) { + int index = y * (maxXpixel + 1); // current row index for 1D buffer + for (int x = 0; x <= maxXpixel; x++) { + SEGMENT.setPixelColorXY(x, y, framebuffer[index++]); + } + } +} + +// calculate pixel positions and brightness distribution and render the particle to local buffer or global buffer +__attribute__((optimize("O2"))) void ParticleSystem2D::renderParticle(const uint32_t particleindex, const uint8_t brightness, const CRGB& color, const bool wrapX, const bool wrapY) { + if (particlesize == 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) { + fast_color_add(framebuffer[x + (maxYpixel - y) * (maxXpixel + 1)], color, brightness); + } + return; + } + 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) + 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 + 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 + + if (advPartProps && advPartProps[particleindex].size > 0) { //render particle to a bigger size + CRGB renderbuffer[100]; // 10x10 pixel buffer + memset(renderbuffer, 0, sizeof(renderbuffer)); // clear buffer + //particle size to pixels: < 64 is 4x4, < 128 is 6x6, < 192 is 8x8, bigger is 10x10 + //first, render the pixel to the center of the renderbuffer, then apply 2D blurring + fast_color_add(renderbuffer[4 + (4 * 10)], color, pxlbrightness[0]); // oCrder is: bottom left, bottom right, top right, top left + fast_color_add(renderbuffer[5 + (4 * 10)], color, pxlbrightness[1]); + fast_color_add(renderbuffer[5 + (5 * 10)], color, pxlbrightness[2]); + fast_color_add(renderbuffer[4 + (5 * 10)], color, pxlbrightness[3]); + uint32_t rendersize = 2; // initialize render size, minimum is 4x4 pixels, it is incremented int he loop below to start with 4 + uint32_t offset = 4; // offset to zero coordinate to write/read data in renderbuffer (actually needs to be 3, is decremented in the loop below) + uint32_t maxsize = advPartProps[particleindex].size; + uint32_t xsize = maxsize; + uint32_t ysize = maxsize; + if (advPartSize) { // use advanced size control + if (advPartSize[particleindex].asymmetry > 0) + getParticleXYsize(&advPartProps[particleindex], &advPartSize[particleindex], xsize, ysize); + maxsize = (xsize > ysize) ? xsize : ysize; // choose the bigger of the two + } + maxsize = maxsize/64 + 1; // number of blur passes depends on maxsize, four passes max + uint32_t bitshift = 0; + for (uint32_t i = 0; i < maxsize; i++) { + if (i == 2) //for the last two passes, use higher amount of blur (results in a nicer brightness gradient with soft edges) + bitshift = 1; + rendersize += 2; + offset--; + blur2D(renderbuffer, rendersize, rendersize, xsize << bitshift, ysize << bitshift, offset, offset, true); + xsize = xsize > 64 ? xsize - 64 : 0; + ysize = ysize > 64 ? ysize - 64 : 0; + } + + // calculate origin coordinates to render the particle to in the framebuffer + uint32_t xfb_orig = x - (rendersize>>1) + 1 - offset; + uint32_t yfb_orig = y - (rendersize>>1) + 1 - offset; + uint32_t xfb, yfb; // coordinates in frame buffer to write to note: by making this uint, only overflow has to be checked (spits a warning though) + + //note on y-axis flip: WLED has the y-axis defined from top to bottom, so y coordinates must be flipped. doing this in the buffer xfer clashes with 1D/2D combined rendering, which does not invert y + // transferring the 1D buffer in inverted fashion will flip the x-axis of overlaid 2D FX, so the y-axis flip is done here so the buffer is flipped in y, giving correct results + + // transfer particle renderbuffer to framebuffer + for (uint32_t xrb = offset; xrb < rendersize + offset; xrb++) { + xfb = xfb_orig + xrb; + if (xfb > (uint32_t)maxXpixel) { + if (wrapX) { // wrap x to the other side if required + if (xfb > (uint32_t)maxXpixel << 1) // xfb is "negative", handle it + xfb = (maxXpixel + 1) + (int32_t)xfb; // this always overflows to within bounds + else + xfb = xfb % (maxXpixel + 1); // note: without the above "negative" check, this works only for powers of 2 + } + else + continue; + } + + for (uint32_t yrb = offset; yrb < rendersize + offset; yrb++) { + yfb = yfb_orig + yrb; + if (yfb > (uint32_t)maxYpixel) { + if (wrapY) {// wrap y to the other side if required + if (yfb > (uint32_t)maxYpixel << 1) // yfb is "negative", handle it + yfb = (maxYpixel + 1) + (int32_t)yfb; // this always overflows to within bounds + else + yfb = yfb % (maxYpixel + 1); // note: without the above "negative" check, this works only for powers of 2 + } + else + continue; + } + fast_color_add(framebuffer[xfb + (maxYpixel - yfb) * (maxXpixel + 1)], renderbuffer[xrb + yrb * 10]); + } + } + } else { // 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 (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 + } + } + 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 (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 + } + } + 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 + } + } + for (uint32_t i = 0; i < 4; i++) { + if (pixelvalid[i]) + fast_color_add(framebuffer[pixco[i].x + (maxYpixel - pixco[i].y) * (maxXpixel + 1)], color, pxlbrightness[i]); // order is: bottom left, bottom right, top right, top left + } + } +} + +// 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) + constexpr int BIN_WIDTH = 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 (advPartProps) //may be using individual particle size + overlap += 512; // add 2 * max radius (approximately) + 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 + (BIN_WIDTH - 1)) / BIN_WIDTH; // number of bins in x direction + 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 + uint16_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 * BIN_WIDTH - overlap; // note: first bin will extend to negative, but that is ok as out of bounds particles are ignored + int32_t binEnd = binStart + BIN_WIDTH + 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 + } + + 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 (advPartProps) { //may be using individual particle size + setParticleSize(particlesize); // updates base particleHardRadius + collDistSq = (particleHardRadius << 1) + (((uint32_t)advPartProps[idx_i].size + (uint32_t)advPartProps[idx_j].size) >> 1); // collision distance note: not 100% clear why the >> 1 is needed, but it is. + collDistSq = collDistSq * collDistSq; // square it for faster comparison + } + 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); + } + } + } + } + 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) +__attribute__((optimize("O2"))) void ParticleSystem2D::collideParticles(PSparticle &particle1, PSparticle &particle2, int32_t dx, int32_t dy, const uint32_t collDistSq) { + 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 used to avoid floats. + // 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 for positive and negative numbers and this needs to be accurate! the trick is: only shift positive numers + // Calculate new velocities after collision + int32_t surfacehardness = 1 + max(collisionHardness, (int32_t)PS_P_MINSURFACEHARDNESS); // 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) & 32767)) >> 15; // note: extracting sign bit and adding rounding value to correct for asymmetry in right shifts + int32_t yimpulse = (impulse * dy + ((dy >> 31) & 32767)) >> 15; + #else + int32_t ximpulse = (impulse * dx) / 32767; + int32_t yimpulse = (impulse * dy) / 32767; + #endif + 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, it works best if not moved but given a little velocity, it tends to oscillate less this way + // when hard pushing by offsetting position, they sink into each other under gravity + // a problem with giving velocity is, that on harder collisions, this adds up as it is not dampened enough, so add friction in the FX if required + if (distanceSquared < collDistSq && dotProduct > -250) { // too close and also slow, push them apart + int32_t notsorandom = dotProduct & 0x01; //dotprouct LSB should be somewhat random, so no need to calculate a random number + int32_t pushamount = 1 + ((250 + dotProduct) >> 6); // the closer dotproduct is to zero, the closer the particles are + int32_t push = 0; + if (dx < 0) // particle 1 is on the right + push = pushamount; + else if (dx > 0) + push = -pushamount; + else { // on the same x coordinate, shift it a little so they do not stack + if (notsorandom) + particle1.x++; // move it so pile collapses + else + particle1.x--; + } + particle1.vx += push; + push = 0; + if (dy < 0) + push = pushamount; + else if (dy > 0) + push = -pushamount; + else { // dy==0 + if (notsorandom) + particle1.y++; // move it so pile collapses + else + particle1.y--; + } + particle1.vy += push; + + // note: pushing may push particles out of frame, if bounce is active, it will move it back as position will be limited to within frame, if bounce is disabled: bye bye + if (collisionHardness < 5) { // if they are very soft, stop slow particles completely to make them stick to each other + particle1.vx = 0; + particle1.vy = 0; + particle2.vx = 0; + particle2.vy = 0; + //push them apart + particle1.x += push; + particle1.y += push; + } + } + } +} + +// 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"); + // DEBUG_PRINT(F("*** PS pointers ***")); + // DEBUG_PRINTF_P(PSTR("this PS %p "), this); + // 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. + particleFlags = reinterpret_cast(this + 1); // pointer to particle flags + particles = reinterpret_cast(particleFlags + numParticles); // pointer to particles + sources = reinterpret_cast(particles + numParticles); // pointer to source(s) at data+sizeof(ParticleSystem2D) + framebuffer = reinterpret_cast(sources + numSources); // pointer to framebuffer + // align pointer after framebuffer + uintptr_t p = reinterpret_cast(framebuffer + (maxXpixel+1)*(maxYpixel+1)); + p = (p + 3) & ~0x03; // align to 4-byte boundary + PSdataEnd = reinterpret_cast(p); // pointer to first available byte after the PS for FX additional data + 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 + +} + +// blur a matrix in x and y direction, blur can be asymmetric in x and y +// for speed, 1D array and 32bit variables are used, make sure to limit them to 8bit (0-255) or result is undefined +// to blur a subset of the buffer, change the xsize/ysize and set xstart/ystart to the desired starting coordinates (default start is 0/0) +// subset blurring only works on 10x10 buffer (single particle rendering), if other sizes are needed, buffer width must be passed as parameter +void blur2D(CRGB *colorbuffer, uint32_t xsize, uint32_t ysize, uint32_t xblur, uint32_t yblur, uint32_t xstart, uint32_t ystart, bool isparticle) { + CRGB seeppart, carryover; + uint32_t seep = xblur >> 1; + uint32_t width = xsize; // width of the buffer, used to calculate the index of the pixel + + if (isparticle) { //first and last row are always black in first pass of particle rendering + ystart++; + ysize--; + width = 10; // buffer size is 10x10 + } + + for (uint32_t y = ystart; y < ystart + ysize; y++) { + carryover = BLACK; + uint32_t indexXY = xstart + y * width; + for (uint32_t x = xstart; x < xstart + xsize; x++) { + seeppart = colorbuffer[indexXY]; // create copy of current color + fast_color_scale(seeppart, seep); // scale it and seep to neighbours + if (x > 0) { + fast_color_add(colorbuffer[indexXY - 1], seeppart); + if (carryover) // note: check adds overhead but is faster on average + fast_color_add(colorbuffer[indexXY], carryover); + } + carryover = seeppart; + indexXY++; // next pixel in x direction + } + } + + if (isparticle) { // first and last row are now smeared + ystart--; + ysize++; + } + + seep = yblur >> 1; + for (uint32_t x = xstart; x < xstart + xsize; x++) { + carryover = BLACK; + uint32_t indexXY = x + ystart * width; + for (uint32_t y = ystart; y < ystart + ysize; y++) { + seeppart = colorbuffer[indexXY]; // create copy of current color + fast_color_scale(seeppart, seep); // scale it and seep to neighbours + if (y > 0) { + fast_color_add(colorbuffer[indexXY - width], seeppart); + if (carryover) // note: check adds overhead but is faster on average + fast_color_add(colorbuffer[indexXY], carryover); + } + carryover = seeppart; + indexXY += width; // next pixel in y direction + } + } +} + +//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) +#ifdef ESP8266 + uint32_t particlelimit = ESP8266_MAXPARTICLES; // maximum number of paticles allowed (based on one segment of 16x16 and 4k effect ram) +#elif ARDUINO_ARCH_ESP32S2 + uint32_t particlelimit = ESP32S2_MAXPARTICLES; // maximum number of paticles allowed (based on one segment of 32x32 and 24k effect ram) +#else + uint32_t particlelimit = ESP32_MAXPARTICLES; // maximum number of paticles allowed (based on two segments of 32x32 and 40k effect ram) +#endif + 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) { +#ifdef ESP8266 + int numberofSources = min((pixels) / 8, (uint32_t)requestedsources); + numberofSources = max(1, min(numberofSources, ESP8266_MAXSOURCES)); // limit +#elif ARDUINO_ARCH_ESP32S2 + int numberofSources = min((pixels) / 6, (uint32_t)requestedsources); + numberofSources = max(1, min(numberofSources, ESP32S2_MAXSOURCES)); // limit +#else + int numberofSources = min((pixels) / 4, (uint32_t)requestedsources); + numberofSources = max(1, min(numberofSources, ESP32_MAXSOURCES)); // limit +#endif + // 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 += sizeof(CRGB) * SEGMENT.virtualLength(); // virtualLength is witdh * height + requiredmemory += additionalbytes + 3; // add 3 to ensure there is room for stuffing bytes + //requiredmemory = (requiredmemory + 3) & ~0x03; // align memory block to next 4-byte boundary + PSPRINTLN("mem alloc: " + String(requiredmemory)); + 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) return false; // only for 2D + uint32_t cols = SEGMENT.virtualWidth(); + uint32_t rows = SEGMENT.virtualHeight(); + uint32_t pixels = cols * rows; + + uint32_t numparticles = calculateNumberOfParticles2D(pixels, advanced, sizecontrol); + PSPRINT(" segmentsize:" + String(cols) + " x " + String(rows)); + PSPRINT(" request numparticles:" + String(numparticles)); + uint32_t numsources = calculateNumberOfSources2D(pixels, requestedsources); + if (!allocateParticleSystemMemory2D(numparticles, numsources, advanced, sizecontrol, additionalbytes)) + { + DEBUG_PRINT(F("PS init failed: memory depleted")); + return false; + } + + PartSys = new (SEGENV.data) ParticleSystem2D(cols, rows, numparticles, numsources, advanced, sizecontrol); // particle system constructor + + PSPRINTLN("******init done, pointers:"); + 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 + } + + if (isadvanced) { + for (uint32_t i = 0; i < numParticles; i++) { + advPartProps[i].sat = 255; // set full saturation (for particles that are transferred from non-advanced system) + } + } +} + +// 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(); + + //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; // speed improvement: multiplication is faster than division + 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 = (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), bigger sizes require adanced properties +void ParticleSystem1D::setParticleSize(const uint8_t size) { + particlesize = size > 0 ? 1 : 0; // TODO: add support for global sizes? see note above (motion blur) + particleHardRadius = PS_P_MINHARDRADIUS_1D >> (!particlesize); // 2 pixel sized particles or single pixel sized particles +} + +// 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; + 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; // 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 (advancedproperties) { // using individual particle size? + if (advancedproperties->size > 1) + particleHardRadius = PS_P_MINHARDRADIUS_1D + (advancedproperties->size >> 1); + else // single pixel particles use half the collision distance for walls + particleHardRadius = PS_P_MINHARDRADIUS_1D >> 1; + renderradius = particleHardRadius; // 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 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() { + CRGB baseRGB; + uint32_t brightness; // particle brightness, fades if dying + + #ifdef ESP8266 // no local buffer on ESP8266 + if (motionBlur) + SEGMENT.fadeToBlackBy(255 - motionBlur); + else + SEGMENT.fill(BLACK); // clear the buffer before rendering to it + #else + if (motionBlur) { // blurring active + for (int32_t x = 0; x <= maxXpixel; x++) { + fast_color_scale(framebuffer[x], motionBlur); + } + } + else { // no blurring: clear buffer + memset(framebuffer, 0, (maxXpixel+1) * sizeof(CRGB)); + } + #endif + // 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); + + if (advPartProps) { //saturation is advanced property in 1D system + if (advPartProps[i].sat < 255) { + CHSV32 baseHSV; + rgb2hsv((uint32_t((byte(baseRGB.r) << 16) | (byte(baseRGB.g) << 8) | (byte(baseRGB.b)))), baseHSV); // convert to HSV + baseHSV.s = advPartProps[i].sat; // set the saturation + uint32_t tempcolor; + hsv2rgb(baseHSV, tempcolor); // convert back to RGB + baseRGB = (CRGB)tempcolor; + } + } + renderParticle(i, brightness, baseRGB, particlesettings.wrap); + } + // apply smear-blur to rendered frame + if (smearBlur) { + #ifdef ESP8266 + SEGMENT.blur(smearBlur, true); // no local buffer on ESP8266 + #else + blur1D(framebuffer, maxXpixel + 1, smearBlur, 0); + #endif + } + + // add background color + uint32_t bg_color = SEGCOLOR(1); + if (bg_color > 0) { //if not black + CRGB bg_color_crgb = bg_color; // convert to CRGB + for (int32_t i = 0; i <= maxXpixel; i++) { + #ifdef ESP8266 // no local buffer on ESP8266 + SEGMENT.addPixelColor(i, bg_color, true); + #else + fast_color_add(framebuffer[i], bg_color_crgb); + #endif + } + } + + #ifndef ESP8266 + // transfer the frame-buffer to segment + for (int x = 0; x <= maxXpixel; x++) { + SEGMENT.setPixelColor(x, framebuffer[x]); + } + #endif +} + +// calculate pixel positions and brightness distribution and render the particle to local buffer or global buffer +__attribute__((optimize("O2"))) void ParticleSystem1D::renderParticle(const uint32_t particleindex, const uint8_t brightness, const CRGB &color, const bool wrap) { + uint32_t size = particlesize; + if (advPartProps) { // use advanced size properties + size = advPartProps[particleindex].size; + } + 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 + #ifdef ESP8266 // no local buffer on ESP8266 + SEGMENT.addPixelColor(x, color.scale8(brightness), true); + #else + fast_color_add(framebuffer[x], color, brightness); + #endif + } + return; + } + //render larger particles + 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; + + // check if particle has advanced size properties and buffer is available + if (advPartProps && advPartProps[particleindex].size > 1) { + CRGB renderbuffer[10]; // 10 pixel buffer + memset(renderbuffer, 0, sizeof(renderbuffer)); // clear buffer + //render particle to a bigger size + //particle size to pixels: 2 - 63 is 4 pixels, < 128 is 6pixels, < 192 is 8 pixels, bigger is 10 pixels + //first, render the pixel to the center of the renderbuffer, then apply 1D blurring + fast_color_add(renderbuffer[4], color, pxlbrightness[0]); + fast_color_add(renderbuffer[5], color, pxlbrightness[1]); + uint32_t rendersize = 2; // initialize render size, minimum is 4 pixels, it is incremented int he loop below to start with 4 + uint32_t offset = 4; // offset to zero coordinate to write/read data in renderbuffer (actually needs to be 3, is decremented in the loop below) + uint32_t blurpasses = size/64 + 1; // number of blur passes depends on size, four passes max + uint32_t bitshift = 0; + for (uint32_t i = 0; i < blurpasses; i++) { + if (i == 2) //for the last two passes, use higher amount of blur (results in a nicer brightness gradient with soft edges) + bitshift = 1; + rendersize += 2; + offset--; + blur1D(renderbuffer, rendersize, size << bitshift, offset); + size = size > 64 ? size - 64 : 0; + } + + // calculate origin coordinates to render the particle to in the framebuffer + uint32_t xfb_orig = x - (rendersize>>1) + 1 - offset; //note: using uint is fine + uint32_t xfb; // coordinates in frame buffer to write to note: by making this uint, only overflow has to be checked + + // transfer particle renderbuffer to framebuffer + for (uint32_t xrb = offset; xrb < rendersize+offset; xrb++) { + xfb = xfb_orig + xrb; + if (xfb > (uint32_t)maxXpixel) { + if (wrap) { // wrap x to the other side if required + if (xfb > (uint32_t)maxXpixel << 1) // xfb is "negative" + xfb = (maxXpixel + 1) + (int32_t)xfb; // this always overflows to within bounds + else + xfb = xfb % (maxXpixel + 1); // note: without the above "negative" check, this works only for powers of 2 + } + else + continue; + } + #ifdef ESP8266 // no local buffer on ESP8266 + SEGMENT.addPixelColor(xfb, renderbuffer[xrb], true); + #else + fast_color_add(framebuffer[xfb], renderbuffer[xrb]); + #endif + } + } + else { // standard rendering (2 pixels per particle) + // check if any pixels are out of frame + if (x < 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 + } + 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; + } + for (uint32_t i = 0; i < 2; i++) { + if (pxlisinframe[i]) { + #ifdef ESP8266 // no local buffer on ESP8266 + SEGMENT.addPixelColor(pixco[i], color.scale8((uint8_t)pxlbrightness[i]), true); + #else + fast_color_add(framebuffer[pixco[i]], color, pxlbrightness[i]); + #endif + } + } + } + +} + +// detect collisions in an array of particles and handle them +void ParticleSystem1D::handleCollisions() { + uint32_t collisiondistance = particleHardRadius << 1; + // 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) + constexpr int BIN_WIDTH = 32 * PS_P_RADIUS_1D; // width of each bin, a compromise between speed and accuracy (larger bins are faster but collapse more) + int32_t overlap = particleHardRadius << 1; // overlap bins to include edge particles to neighbouring bins + if (advPartProps) //may be using individual particle size + overlap += 256; // add 2 * max radius (approximately) + 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 + (BIN_WIDTH - 1)) / BIN_WIDTH; // calculate number of bins + uint16_t binIndices[maxBinParticles]; // array to store indices of particles in a bin + uint32_t binParticleCount; // number of particles in the current bin + uint16_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 * BIN_WIDTH - overlap; // note: first bin will extend to negative, but that is ok as out of bounds particles are ignored + int32_t binEnd = binStart + BIN_WIDTH + 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]; + if (advPartProps) { // use advanced size properties + collisiondistance = (PS_P_MINHARDRADIUS_1D << particlesize) + ((advPartProps[idx_i].size + advPartProps[idx_j].size) >> 1); + } + int32_t dx = (particles[idx_j].x + particles[idx_j].vx) - (particles[idx_i].x + particles[idx_i].vx); // distance between particles with lookahead + uint32_t dx_abs = abs(dx); + if (dx_abs <= collisiondistance) { // collide if close + collideParticles(particles[idx_i], particleFlags[idx_i], particles[idx_j], particleFlags[idx_j], dx, dx_abs, collisiondistance); + } + } + } + } + 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) +__attribute__((optimize("O2"))) void ParticleSystem1D::collideParticles(PSparticle1D &particle1, const PSparticleFlags1D &particle1flags, PSparticle1D &particle2, const PSparticleFlags1D &particle2flags, const int32_t dx, const uint32_t dx_abs, const uint32_t collisiondistance) { + int32_t dv = particle2.vx - particle1.vx; + int32_t dotProduct = (dx * dv); // is always negative if moving towards each other + + if (dotProduct < 0) { // particles are moving towards each other + uint32_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 + particle1.vx += impulse; + particle2.vx -= impulse; + + // if one of the particles is fixed, transfer the impulse back so it bounces + if (particle1flags.fixed) + particle2.vx = -particle1.vx; + else if (particle2flags.fixed) + particle1.vx = -particle2.vx; + + 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) + 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 + particle2.vx = ((int32_t)particle2.vx * coeff + (((int32_t)particle2.vx >> 31) & 0xFF)) >> 8; + #else // division is faster on ESP32, S2 and S3 + particle1.vx = ((int32_t)particle1.vx * coeff) / 255; + particle2.vx = ((int32_t)particle2.vx * coeff) / 255; + #endif + } + } + + if (dx_abs < (collisiondistance - 8) && abs(dv) < 5) { // overlapping and moving slowly + // particles have volume, push particles apart if they are too close + // behaviour is different than in 2D, we need pixel accurate stacking here, push the top particle + // note: like in 2D, pushing by a distance makes softer piles collapse, giving particles speed prevents that and looks nicer + int32_t pushamount = 1; + if (dx < 0) // particle2.x < particle1.x + pushamount = -pushamount; + particle1.vx -= pushamount; + particle2.vx += pushamount; + + if (dx_abs < collisiondistance >> 1) { // too close, force push particles so they dont collapse + pushamount = 1 + ((collisiondistance - dx_abs) >> 3); // note: push amount found by experimentation + + if (particle1.x < (maxX >> 1)) { // lower half, push particle with larger x in positive direction + if (dx < 0 && !particle1flags.fixed) { // particle2.x < particle1.x -> push particle 1 + particle1.vx++;// += pushamount; + particle1.x += pushamount; + } + else if (!particle2flags.fixed) { // particle1.x < particle2.x -> push particle 2 + particle2.vx++;// += pushamount; + particle2.x += pushamount; + } + } + else { // upper half, push particle with smaller x + if (dx < 0 && !particle2flags.fixed) { // particle2.x < particle1.x -> push particle 2 + particle2.vx--;// -= pushamount; + particle2.x -= pushamount; + } + else if (!particle1flags.fixed) { // particle1.x < particle2.x -> push particle 1 + particle1.vx--;// -= pushamount; + particle1.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.virtualLength()); // 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. + particleFlags = reinterpret_cast(this + 1); // pointer to particle flags + particles = reinterpret_cast(particleFlags + numParticles); // pointer to particles + sources = reinterpret_cast(particles + numParticles); // pointer to source(s) + #ifdef ESP8266 // no local buffer on ESP8266 + PSdataEnd = reinterpret_cast(sources + numSources); + #else + framebuffer = reinterpret_cast(sources + numSources); // pointer to framebuffer + // align pointer after framebuffer to 4bytes + uintptr_t p = reinterpret_cast(framebuffer + (maxXpixel+1)); + p = (p + 3) & ~0x03; // align to 4-byte boundary + PSdataEnd = reinterpret_cast(p); // pointer to first available byte after the PS for FX additional data + #endif + if (isadvanced) { + advPartProps = reinterpret_cast(PSdataEnd); + PSdataEnd = reinterpret_cast(advPartProps + numParticles); + } + #ifdef WLED_DEBUG_PS + PSPRINTLN(" PS Pointers: "); + PSPRINT(" PS : 0x"); + Serial.println((uintptr_t)this, HEX); + PSPRINT(" Sources : 0x"); + Serial.println((uintptr_t)sources, HEX); + PSPRINT(" Particles : 0x"); + Serial.println((uintptr_t)particles, 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) +#ifdef ESP8266 + uint32_t particlelimit = ESP8266_MAXPARTICLES_1D; // maximum number of paticles allowed +#elif ARDUINO_ARCH_ESP32S2 + uint32_t particlelimit = ESP32S2_MAXPARTICLES_1D; // maximum number of paticles allowed +#else + uint32_t particlelimit = ESP32_MAXPARTICLES_1D; // maximum number of paticles allowed +#endif + 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 < 20 ? 20 : numberofParticles; // 20 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 + return numberofParticles; +} + +uint32_t calculateNumberOfSources1D(const uint32_t requestedsources) { +#ifdef ESP8266 + int numberofSources = max(1, min((int)requestedsources,ESP8266_MAXSOURCES_1D)); // limit +#elif ARDUINO_ARCH_ESP32S2 + int numberofSources = max(1, min((int)requestedsources, ESP32S2_MAXSOURCES_1D)); // limit +#else + int numberofSources = max(1, min((int)requestedsources, ESP32_MAXSOURCES_1D)); // limit +#endif + // 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 ESP8266 // no local buffer on ESP8266 + requiredmemory += sizeof(CRGB) * SEGMENT.virtualLength(); + #endif + requiredmemory += additionalbytes + 3; // add 3 to ensure room for stuffing bytes to make it 4 byte aligned + 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) return false; // single pixel not supported + uint32_t numparticles = calculateNumberOfParticles1D(fractionofparticles, advanced); + uint32_t numsources = calculateNumberOfSources1D(requestedsources); + if (!allocateParticleSystemMemory1D(numparticles, numsources, advanced, additionalbytes)) { + DEBUG_PRINT(F("PS init failed: memory depleted")); + return false; + } + PartSys = new (SEGENV.data) ParticleSystem1D(SEGMENT.virtualLength(), numparticles, numsources, advanced); // particle system constructor + return true; +} + +// blur a 1D buffer, sub-size blurring can be done using start and size +// for speed, 32bit variables are used, make sure to limit them to 8bit (0-255) or result is undefined +// to blur a subset of the buffer, change the size and set start to the desired starting coordinates +void blur1D(CRGB *colorbuffer, uint32_t size, uint32_t blur, uint32_t start) +{ + CRGB seeppart, carryover; + uint32_t seep = blur >> 1; + carryover = BLACK; + for (uint32_t x = start; x < start + size; x++) { + seeppart = colorbuffer[x]; // create copy of current color + fast_color_scale(seeppart, seep); // scale it and seep to neighbours + if (x > 0) { + fast_color_add(colorbuffer[x-1], seeppart); + if (carryover) // note: check adds overhead but is faster on average + fast_color_add(colorbuffer[x], carryover); // is black on first pass + } + carryover = seeppart; + } +} +#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 +} + +// fastled color adding is very inaccurate in color preservation (but it is fast) +// a better color add function is implemented in colors.cpp but it uses 32bit RGBW. to use it colors need to be shifted just to then be shifted back by that function, which is slow +// this is a fast version for RGB (no white channel, PS does not handle white) and with native CRGB including scaling of second color +// note: result is stored in c1, not using a return value is faster as the CRGB struct does not need to be copied upon return +// note2: function is mainly used to add scaled colors, so checking if one color is black is slower +// note3: scale is 255 when using blur, checking for that makes blur faster + __attribute__((optimize("O2"))) static void fast_color_add(CRGB &c1, const CRGB &c2, const uint8_t scale) { + uint32_t r, g, b; + if (scale < 255) { + r = c1.r + ((c2.r * scale) >> 8); + g = c1.g + ((c2.g * scale) >> 8); + b = c1.b + ((c2.b * scale) >> 8); + } else { + r = c1.r + c2.r; + g = c1.g + c2.g; + b = c1.b + c2.b; + } + + // note: this chained comparison is the fastest method for max of 3 values (faster than std:max() or using xor) + uint32_t max = (r > g) ? ((r > b) ? r : b) : ((g > b) ? g : b); + if (max <= 255) { + c1.r = r; // save result to c1 + c1.g = g; + c1.b = b; + } else { + uint32_t newscale = (255U << 16) / max; + c1.r = (r * newscale) >> 16; + c1.g = (g * newscale) >> 16; + c1.b = (b * newscale) >> 16; + } +} + +// faster than fastled color scaling as it does in place scaling + __attribute__((optimize("O2"))) static void fast_color_scale(CRGB &c, const uint8_t scale) { + c.r = ((c.r * scale) >> 8); + c.g = ((c.g * scale) >> 8); + c.b = ((c.b * scale) >> 8); +} + +#endif // !(defined(WLED_DISABLE_PARTICLESYSTEM2D) && defined(WLED_DISABLE_PARTICLESYSTEM1D)) diff --git a/wled00/FXparticleSystem.h b/wled00/FXparticleSystem.h new file mode 100644 index 000000000..695a3a028 --- /dev/null +++ b/wled00/FXparticleSystem.h @@ -0,0 +1,390 @@ +/* + 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) +#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 +#define ESP8266_MAXPARTICLES 256 // enough up to 16x16 pixels +#define ESP8266_MAXSOURCES 24 +#define ESP32S2_MAXPARTICLES 1024 // enough up to 32x32 pixels +#define ESP32S2_MAXSOURCES 64 +#define ESP32_MAXPARTICLES 2048 // enough up to 64x32 pixels +#define ESP32_MAXSOURCES 128 + +// 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 + 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) +} 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, const bool renderonly); // update function for fire, if renderonly is set, particles are not updated (required to fix transitions with frameskips) + 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' + //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 CRGB& 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(); + [[gnu::hot]] void collideParticles(PSparticle &particle1, PSparticle &particle2, const int32_t dx, const int32_t dy, const uint32_t collDistSq); + 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 + CRGB *framebuffer; // local frame buffer for rendering + 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) + 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 +}; + +void blur2D(CRGB *colorbuffer, const uint32_t xsize, uint32_t ysize, const uint32_t xblur, const uint32_t yblur, const uint32_t xstart = 0, uint32_t ystart = 0, const bool isparticle = false); +// 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); +#endif // WLED_DISABLE_PARTICLESYSTEM2D + +//////////////////////// +// 1D Particle System // +//////////////////////// +#ifndef WLED_DISABLE_PARTICLESYSTEM1D +// memory allocation +#define ESP8266_MAXPARTICLES_1D 320 +#define ESP8266_MAXSOURCES_1D 16 +#define ESP32S2_MAXPARTICLES_1D 1300 +#define ESP32S2_MAXSOURCES_1D 32 +#define ESP32_MAXPARTICLES_1D 2600 +#define ESP32_MAXSOURCES_1D 64 + +// 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 + uint8_t forcecounter; +} 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); //size 0 = 1 pixel, size 1 = 2 pixels, is overruled by advanced particle size + 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' + +private: + //rendering functions + void render(void); + [[gnu::hot]] void renderParticle(const uint32_t particleindex, const uint8_t brightness, const CRGB &color, const bool wrap); + + //paricle physics applied by system if flags are set + void applyGravity(); // applies gravity to all particles + void handleCollisions(); + [[gnu::hot]] void collideParticles(PSparticle1D &particle1, const PSparticleFlags1D &particle1flags, PSparticle1D &particle2, const PSparticleFlags1D &particle2flags, const int32_t dx, const uint32_t dx_abs, const 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 + #ifndef ESP8266 + CRGB *framebuffer; // local frame buffer for rendering + #endif + 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 + 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); +void blur1D(CRGB *colorbuffer, uint32_t size, uint32_t blur, uint32_t start); +#endif // WLED_DISABLE_PARTICLESYSTEM1D diff --git a/wled00/bus_manager.cpp b/wled00/bus_manager.cpp index 3abf61412..cee34c2ea 100644 --- a/wled00/bus_manager.cpp +++ b/wled00/bus_manager.cpp @@ -21,10 +21,12 @@ #endif #include "const.h" #include "pin_manager.h" -#include "bus_wrapper.h" #include "bus_manager.h" +#include "bus_wrapper.h" +#include extern bool cctICused; +extern bool useParallelI2S; //colors.cpp uint32_t colorBalanceFromKelvin(uint16_t kelvin, uint32_t rgb); @@ -32,28 +34,6 @@ uint32_t colorBalanceFromKelvin(uint16_t kelvin, uint32_t rgb); //udp.cpp uint8_t realtimeBroadcast(uint8_t type, IPAddress client, uint16_t length, const uint8_t* buffer, uint8_t bri=255, bool isRGBW=false); -// enable additional debug output -#if defined(WLED_DEBUG_HOST) - #include "net_debug.h" - #define DEBUGOUT NetDebug -#else - #define DEBUGOUT Serial -#endif - -#ifdef WLED_DEBUG - #ifndef ESP8266 - #include - #endif - #define DEBUG_PRINT(x) DEBUGOUT.print(x) - #define DEBUG_PRINTLN(x) DEBUGOUT.println(x) - #define DEBUG_PRINTF(x...) DEBUGOUT.printf(x) - #define DEBUG_PRINTF_P(x...) DEBUGOUT.printf_P(x) -#else - #define DEBUG_PRINT(x) - #define DEBUG_PRINTLN(x) - #define DEBUG_PRINTF(x...) - #define DEBUG_PRINTF_P(x...) -#endif //color mangling macros #define RGBW32(r,g,b,w) (uint32_t((byte(w) << 24) | (byte(r) << 16) | (byte(g) << 8) | (byte(b)))) @@ -63,19 +43,20 @@ uint8_t realtimeBroadcast(uint8_t type, IPAddress client, uint16_t length, const #define W(c) (byte((c) >> 24)) +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 (unsigned i = 0; i < count(); i++) { - if (pix >= _mappings[i].start && pix < (_mappings[i].start + _mappings[i].len)) { - return _mappings[i].colorOrder | ((_mappings[i].colorOrder >> 4) ? 0 : (defaultColorOrder & 0xF0)); - } + 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; } @@ -91,7 +72,7 @@ void Bus::calculateCCT(uint32_t c, uint8_t &ww, uint8_t &cw) { } else { cct = (approximateKelvinFromRGB(c) - 1900) >> 5; // convert K (from RGB value) to relative format } - + //0 - linear (CCT 127 = 50% warm, 50% cold), 127 - additive CCT blending (CCT 127 = 100% warm, 100% cold) if (cct < _cctBlend) ww = 255; else ww = ((255-cct) * 255) / (255 - _cctBlend); @@ -118,44 +99,52 @@ uint32_t Bus::autoWhiteCalc(uint32_t c) const { return RGBW32(r, g, b, w); } -uint8_t *Bus::allocateData(size_t size) { - if (_data) free(_data); // should not happen, but for safety - return _data = (uint8_t *)(size>0 ? calloc(size, sizeof(uint8_t)) : nullptr); -} - -BusDigital::BusDigital(const BusConfig &bc, uint8_t nr, const ColorOrderMap &com) +BusDigital::BusDigital(const BusConfig &bc, uint8_t nr) : 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) -, _colorOrderMap(com) +, _data(nullptr) { - if (!isDigital(bc.type) || !bc.count) return; - if (!PinManager::allocatePin(bc.pins[0], true, PinOwner::BusDigital)) return; + DEBUGBUS_PRINTLN(F("Bus: Creating digital bus.")); + if (!isDigital(bc.type) || !bc.count) { DEBUGBUS_PRINTLN(F("Not digial or empty bus!")); return; } + if (!PinManager::allocatePin(bc.pins[0], true, PinOwner::BusDigital)) { DEBUGBUS_PRINTLN(F("Pin 0 allocated!")); return; } _frequencykHz = 0U; _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 } _iType = PolyBus::getI(bc.type, _pins, nr); - if (_iType == I_NONE) return; + if (_iType == I_NONE) { DEBUGBUS_PRINTLN(F("Incorrect iType!")); return; } _hasRgb = hasRGB(bc.type); _hasWhite = hasWhite(bc.type); _hasCCT = hasCCT(bc.type); - if (bc.doubleBuffer && !allocateData(bc.count * Bus::getNumberOfChannels(bc.type))) return; - //_buffering = bc.doubleBuffer; + if (bc.doubleBuffer) { + _data = (uint8_t*)calloc(_len, Bus::getNumberOfChannels(_type)); + if (!_data) DEBUGBUS_PRINTLN(F("Bus: Buffer allocation failed!")); + } 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, nr); - _valid = (_busPtr != nullptr); - DEBUG_PRINTF_P(PSTR("%successfully inited strip %u (len %u) with type %u and pins %u,%u (itype %u). mA=%d/%d\n"), _valid?"S":"Uns", nr, bc.count, bc.type, _pins[0], is2Pin(bc.type)?_pins[1]:255, _iType, _milliAmpsPerLed, _milliAmpsMax); + _valid = (_busPtr != nullptr) && bc.count > 0; + DEBUGBUS_PRINTF_P(PSTR("Bus: %successfully inited #%u (len:%u, type:%u (RGB:%d, W:%d, CCT:%d), pins:%u,%u [itype:%u] mA=%d/%d)\n"), + _valid?"S":"Uns", + (int)nr, + (int)bc.count, + (int)bc.type, + (int)_hasRgb, (int)_hasWhite, (int)_hasCCT, + (unsigned)_pins[0], is2Pin(bc.type)?(unsigned)_pins[1]:255U, + (unsigned)_iType, + (int)_milliAmpsPerLed, (int)_milliAmpsMax + ); } //DISCLAIMER @@ -166,7 +155,7 @@ BusDigital::BusDigital(const BusConfig &bc, uint8_t nr, const ColorOrderMap &com //I am NOT to be held liable for burned down garages or houses! // To disable brightness limiter we either set output max current to 0 or single LED current to 0 -uint8_t BusDigital::estimateCurrentAndLimitBri() { +uint8_t BusDigital::estimateCurrentAndLimitBri() const { bool useWackyWS2815PowerModel = false; byte actualMilliampsPerLed = _milliAmpsPerLed; @@ -179,7 +168,7 @@ uint8_t BusDigital::estimateCurrentAndLimitBri() { actualMilliampsPerLed = 12; // from testing an actual strip } - size_t powerBudget = (_milliAmpsMax - MA_FOR_ESP/BusManager::getNumBusses()); //80/120mA for ESP power + unsigned powerBudget = (_milliAmpsMax - MA_FOR_ESP/BusManager::getNumBusses()); //80/120mA for ESP power if (powerBudget > getLength()) { //each LED uses about 1mA in standby, exclude that from power budget powerBudget -= getLength(); } else { @@ -204,26 +193,25 @@ uint8_t BusDigital::estimateCurrentAndLimitBri() { } // powerSum has all the values of channels summed (max would be getLength()*765 as white is excluded) so convert to milliAmps - busPowerSum = (busPowerSum * actualMilliampsPerLed) / 765; - _milliAmpsTotal = busPowerSum * _bri / 255; + BusDigital::_milliAmpsTotal = (busPowerSum * actualMilliampsPerLed * _bri) / (765*255); uint8_t newBri = _bri; - if (busPowerSum * _bri / 255 > powerBudget) { //scale brightness down to stay in current limit - float scale = (float)(powerBudget * 255) / (float)(busPowerSum * _bri); - if (scale >= 1.0f) return _bri; - _milliAmpsTotal = ceilf((float)_milliAmpsTotal * scale); - uint8_t scaleB = min((int)(scale * 255), 255); - newBri = unsigned(_bri * scaleB) / 256 + 1; + if (BusDigital::_milliAmpsTotal > powerBudget) { + //scale brightness down to stay in current limit + unsigned scaleB = powerBudget * 255 / BusDigital::_milliAmpsTotal; + newBri = (_bri * scaleB) / 256 + 1; + BusDigital::_milliAmpsTotal = powerBudget; + //_milliAmpsTotal = (busPowerSum * actualMilliampsPerLed * newBri) / (765*255); } return newBri; } void BusDigital::show() { - _milliAmpsTotal = 0; + BusDigital::_milliAmpsTotal = 0; if (!_valid) return; uint8_t cctWW = 0, cctCW = 0; - unsigned newBri = estimateCurrentAndLimitBri(); // will fill _milliAmpsTotal + unsigned newBri = estimateCurrentAndLimitBri(); // will fill _milliAmpsTotal (TODO: could use PolyBus::CalcTotalMilliAmpere()) if (newBri < _bri) PolyBus::setBrightness(_busPtr, _iType, newBri); // limit brightness to stay within current limits if (_data) { @@ -249,6 +237,7 @@ void BusDigital::show() { // TODO: there is an issue if CCT is calculated from RGB value (_cct==-1), we cannot do that with double buffer Bus::_cct = _data[offset+channels-1]; Bus::calculateCCT(c, cctWW, cctCW); + if (_type == TYPE_WS2812_WWA) c = RGBW32(cctWW, cctCW, 0, W(c)); // may need swapping } unsigned pix = i; if (_reversed) pix = _len - pix -1; @@ -272,7 +261,7 @@ void BusDigital::show() { } } } - PolyBus::show(_busPtr, _iType, !_data); // faster if buffer consistency is not important (use !_buffering this causes 20% FPS drop) + PolyBus::show(_busPtr, _iType, !_data); // faster if buffer consistency is not important // restore bus brightness to its original value // this is done right after show, so this is only OK if LED updates are completed before show() returns // or async show has a separate buffer (ESP32 RMT and I2S are ok) @@ -334,8 +323,8 @@ void IRAM_ATTR BusDigital::setPixelColor(unsigned pix, uint32_t c) { uint8_t cctWW = 0, cctCW = 0; Bus::calculateCCT(c, cctWW, cctCW); wwcw = (cctCW<<8) | cctWW; + if (_type == TYPE_WS2812_WWA) c = RGBW32(cctWW, cctCW, 0, W(c)); // may need swapping } - PolyBus::setPixelColor(_busPtr, _iType, pix, c, co, wwcw); } } @@ -367,23 +356,31 @@ uint32_t IRAM_ATTR BusDigital::getPixelColor(unsigned pix) const { 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; } } -uint8_t BusDigital::getPins(uint8_t* pinArray) const { +unsigned 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; } +unsigned BusDigital::getBusSize() const { + return sizeof(BusDigital) + (isOk() ? PolyBus::getDataSize(_busPtr, _iType) + (_data ? _len * getNumberOfChannels() : 0) : 0); +} + 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/Aircoookie/WLED/pull/4056 +// credit @willmmiles & @netmindz https://github.com/wled-dev/WLED/pull/4056 std::vector BusDigital::getLEDTypes() { return { {TYPE_WS2812_RGB, "D", PSTR("WS281x")}, @@ -399,8 +396,8 @@ std::vector BusDigital::getLEDTypes() { {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("WS2811 CCT")}, // not implemented - //{TYPE_WS2812_WWA, "D", PSTR("WS2811 WWA")}, // not implemented + //{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")}, @@ -415,12 +412,13 @@ void BusDigital::begin() { } void BusDigital::cleanup() { - DEBUG_PRINTLN(F("Digital Cleanup.")); + DEBUGBUS_PRINTLN(F("Digital Cleanup.")); PolyBus::cleanup(_busPtr, _iType); + free(_data); + _data = nullptr; _iType = I_NONE; _valid = false; _busPtr = nullptr; - if (_data != nullptr) freeData(); PinManager::deallocatePin(_pins[1], PinOwner::BusDigital); PinManager::deallocatePin(_pins[0], PinOwner::BusDigital); } @@ -444,7 +442,7 @@ void BusDigital::cleanup() { #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 + #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 @@ -492,9 +490,8 @@ BusPwm::BusPwm(const BusConfig &bc) _hasRgb = hasRGB(bc.type); _hasWhite = hasWhite(bc.type); _hasCCT = hasCCT(bc.type); - _data = _pwmdata; // avoid malloc() and use stack _valid = true; - DEBUG_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]); + 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) { @@ -556,7 +553,7 @@ uint32_t BusPwm::getPixelColor(unsigned pix) const { void BusPwm::show() { if (!_valid) return; - const unsigned numPins = getPins(); + const size_t numPins = getPins(); #ifdef ESP8266 const unsigned analogPeriod = F_CPU / _frequency; const unsigned maxBri = analogPeriod; // compute to clock cycle accuracy @@ -564,9 +561,9 @@ void BusPwm::show() { 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/Aircoookie/WLED/pull/4115 and https://github.com/zalatnaicsongor/WLED/pull/1) + // https://github.com/wled-dev/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 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 @@ -582,7 +579,7 @@ void BusPwm::show() { [[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 + // 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 @@ -623,14 +620,14 @@ void BusPwm::show() { } } -uint8_t BusPwm::getPins(uint8_t* pinArray) const { +unsigned 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/Aircoookie/WLED/pull/4056 +// credit @willmmiles & @netmindz https://github.com/wled-dev/WLED/pull/4056 std::vector BusPwm::getLEDTypes() { return { {TYPE_ANALOG_1CH, "A", PSTR("PWM White")}, @@ -643,7 +640,7 @@ std::vector BusPwm::getLEDTypes() { } void BusPwm::deallocatePins() { - unsigned numPins = getPins(); + size_t numPins = getPins(); for (unsigned i = 0; i < numPins; i++) { PinManager::deallocatePin(_pins[i], PinOwner::BusPwm); if (!PinManager::isPinOk(_pins[i])) continue; @@ -661,7 +658,7 @@ void BusPwm::deallocatePins() { BusOnOff::BusOnOff(const BusConfig &bc) : Bus(bc.type, bc.start, bc.autoWhite, 1, bc.reversed) -, _onoffdata(0) +, _data(0) { if (!Bus::isOnOff(bc.type)) return; @@ -674,9 +671,8 @@ BusOnOff::BusOnOff(const BusConfig &bc) _hasRgb = false; _hasWhite = false; _hasCCT = false; - _data = &_onoffdata; // avoid malloc() and use stack _valid = true; - DEBUG_PRINTF_P(PSTR("%successfully inited On/Off strip with pin %u\n"), _valid?"S":"Uns", _pin); + 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) { @@ -686,26 +682,26 @@ void BusOnOff::setPixelColor(unsigned pix, uint32_t c) { uint8_t g = G(c); uint8_t b = B(c); uint8_t w = W(c); - _data[0] = bool(r|g|b|w) && bool(_bri) ? 0xFF : 0; + _data = bool(r|g|b|w) && bool(_bri) ? 0xFF : 0; } uint32_t BusOnOff::getPixelColor(unsigned pix) const { if (!_valid) return 0; - return RGBW32(_data[0], _data[0], _data[0], _data[0]); + return RGBW32(_data, _data, _data, _data); } void BusOnOff::show() { if (!_valid) return; - digitalWrite(_pin, _reversed ? !(bool)_data[0] : (bool)_data[0]); + digitalWrite(_pin, _reversed ? !(bool)_data : (bool)_data); } -uint8_t BusOnOff::getPins(uint8_t* pinArray) const { +unsigned BusOnOff::getPins(uint8_t* pinArray) const { if (!_valid) return 0; if (pinArray) pinArray[0] = _pin; return 1; } -// credit @willmmiles & @netmindz https://github.com/Aircoookie/WLED/pull/4056 +// credit @willmmiles & @netmindz https://github.com/wled-dev/WLED/pull/4056 std::vector BusOnOff::getLEDTypes() { return { {TYPE_ONOFF, "", PSTR("On/Off")}, @@ -735,8 +731,9 @@ BusNetwork::BusNetwork(const BusConfig &bc) _hasCCT = false; _UDPchannels = _hasWhite + 3; _client = IPAddress(bc.pins[0],bc.pins[1],bc.pins[2],bc.pins[3]); - _valid = (allocateData(_len * _UDPchannels) != nullptr); - DEBUG_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]); + _data = (uint8_t*)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) { @@ -763,12 +760,12 @@ void BusNetwork::show() { _broadcastLock = false; } -uint8_t BusNetwork::getPins(uint8_t* pinArray) const { +unsigned BusNetwork::getPins(uint8_t* pinArray) const { if (pinArray) for (unsigned i = 0; i < 4; i++) pinArray[i] = _client[i]; return 4; } -// credit @willmmiles & @netmindz https://github.com/Aircoookie/WLED/pull/4056 +// credit @willmmiles & @netmindz https://github.com/wled-dev/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 @@ -779,55 +776,80 @@ std::vector BusNetwork::getLEDTypes() { //{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/Aircoookie/WLED/pull/4123) + //{TYPE_USERMOD, "VVVVV", PSTR("Usermod (virtual)")}, // 5 data fields (see https://github.com/wled-dev/WLED/pull/4123) }; } void BusNetwork::cleanup() { + DEBUGBUS_PRINTLN(F("Virtual Cleanup.")); + free(_data); + _data = nullptr; _type = I_NONE; _valid = false; - freeData(); } //utility to get the approx. memory usage of a given BusConfig -uint32_t BusManager::memUsage(const BusConfig &bc) { - if (Bus::isOnOff(bc.type) || Bus::isPWM(bc.type)) return OUTPUT_MAX_PINS; - - unsigned len = bc.count + bc.skipAmount; - unsigned channels = Bus::getNumberOfChannels(bc.type); - unsigned multiplier = 1; - if (Bus::isDigital(bc.type)) { // digital types - if (Bus::is16bit(bc.type)) len *= 2; // 16-bit LEDs - #ifdef ESP8266 - if (bc.pins[0] == 3) { //8266 DMA uses 5x the mem - multiplier = 5; - } - #else //ESP32 RMT uses double buffer, parallel I2S uses 8x buffer (3 times) - multiplier = PolyBus::isParallelI2S1Output() ? 24 : 2; - #endif +unsigned BusConfig::memUsage(unsigned nr) const { + if (Bus::isVirtual(type)) { + return sizeof(BusNetwork) + (count * Bus::getNumberOfChannels(type)); + } else if (Bus::isDigital(type)) { + return sizeof(BusDigital) + PolyBus::memUsage(count + skipAmount, PolyBus::getI(type, pins, nr)) + doubleBuffer * (count + skipAmount) * Bus::getNumberOfChannels(type); + } else if (Bus::isOnOff(type)) { + return sizeof(BusOnOff); + } else { + return sizeof(BusPwm); } - return (len * multiplier + bc.doubleBuffer * (bc.count + bc.skipAmount)) * channels; } -uint32_t BusManager::memUsage(unsigned maxChannels, unsigned maxCount, unsigned minBuses) { - //ESP32 RMT uses double buffer, parallel I2S uses 8x buffer (3 times) - unsigned multiplier = PolyBus::isParallelI2S1Output() ? 3 : 2; - return (maxChannels * maxCount * minBuses * multiplier); + +unsigned BusManager::memUsage() { + // when ESP32, S2 & S3 use parallel I2S only the largest bus determines the total memory requirements for back buffers + // front buffers are always allocated per bus + unsigned size = 0; + unsigned maxI2S = 0; + #if !defined(CONFIG_IDF_TARGET_ESP32C3) && !defined(ESP8266) + unsigned digitalCount = 0; + #if defined(CONFIG_IDF_TARGET_ESP32S2) || defined(CONFIG_IDF_TARGET_ESP32S3) + #define MAX_RMT 4 + #else + #define MAX_RMT 8 + #endif + #endif + for (const auto &bus : busses) { + unsigned busSize = bus->getBusSize(); + #if !defined(CONFIG_IDF_TARGET_ESP32C3) && !defined(ESP8266) + if (bus->isDigital() && !bus->is2Pin()) digitalCount++; + if (PolyBus::isParallelI2S1Output() && digitalCount > MAX_RMT) { + unsigned i2sCommonSize = 3 * bus->getLength() * bus->getNumberOfChannels() * (bus->is16bit()+1); + if (i2sCommonSize > maxI2S) maxI2S = i2sCommonSize; + busSize -= i2sCommonSize; + } + #endif + size += busSize; + } + return size + maxI2S; } int BusManager::add(const BusConfig &bc) { + DEBUGBUS_PRINTF_P(PSTR("Bus: Adding bus (%d - %d >= %d)\n"), getNumBusses(), getNumVirtualBusses(), WLED_MAX_BUSSES); if (getNumBusses() - getNumVirtualBusses() >= WLED_MAX_BUSSES) return -1; + unsigned numDigital = 0; + for (const auto &bus : busses) if (bus->isDigital() && !bus->is2Pin()) numDigital++; if (Bus::isVirtual(bc.type)) { - busses[numBusses] = new BusNetwork(bc); + busses.push_back(make_unique(bc)); + //busses.push_back(new BusNetwork(bc)); } else if (Bus::isDigital(bc.type)) { - busses[numBusses] = new BusDigital(bc, numBusses, colorOrderMap); + busses.push_back(make_unique(bc, numDigital)); + //busses.push_back(new BusDigital(bc, numDigital)); } else if (Bus::isOnOff(bc.type)) { - busses[numBusses] = new BusOnOff(bc); + busses.push_back(make_unique(bc)); + //busses.push_back(new BusOnOff(bc)); } else { - busses[numBusses] = new BusPwm(bc); + busses.push_back(make_unique(bc)); + //busses.push_back(new BusPwm(bc)); } - return numBusses++; + return busses.size(); } // credit @willmmiles @@ -843,7 +865,7 @@ static String LEDTypesToJson(const std::vector& types) { return json; } -// credit @willmmiles & @netmindz https://github.com/Aircoookie/WLED/pull/4056 +// credit @willmmiles & @netmindz https://github.com/wled-dev/WLED/pull/4056 String BusManager::getLEDTypesJSONString() { String json = "["; json += LEDTypesToJson(BusDigital::getLEDTypes()); @@ -856,18 +878,21 @@ String BusManager::getLEDTypesJSONString() { } void BusManager::useParallelOutput() { - _parallelOutputs = 8; // hardcoded since we use NPB I2S x8 methods + DEBUGBUS_PRINTLN(F("Bus: Enabling parallel I2S.")); PolyBus::setParallelI2S1Output(); } +bool BusManager::hasParallelOutput() { + return PolyBus::isParallelI2S1Output(); +} + //do not call this method from system context (network callback) void BusManager::removeAll() { - DEBUG_PRINTLN(F("Removing all.")); + DEBUGBUS_PRINTLN(F("Removing all.")); //prevents crashes due to deleting busses while in use. while (!canAllShow()) yield(); - for (unsigned i = 0; i < numBusses; i++) delete busses[i]; - numBusses = 0; - _parallelOutputs = 1; + //for (auto &bus : busses) delete bus; // needed when not using std::unique_ptr C++ >11 + busses.clear(); PolyBus::setParallelI2S1Output(false); } @@ -878,7 +903,9 @@ void BusManager::removeAll() { void BusManager::esp32RMTInvertIdle() { bool idle_out; unsigned rmt = 0; - for (unsigned u = 0; u < numBusses(); u++) { + unsigned u = 0; + for (auto &bus : busses) { + if (bus->getLength()==0 || !bus->isDigital() || bus->is2Pin()) continue; #if defined(CONFIG_IDF_TARGET_ESP32C3) // 2 RMT, only has 1 I2S but NPB does not support it ATM if (u > 1) return; rmt = u; @@ -889,11 +916,11 @@ void BusManager::esp32RMTInvertIdle() { if (u > 3) return; rmt = u; #else - if (u < _parallelOutputs) continue; - if (u >= _parallelOutputs + 8) return; // only 8 RMT channels - rmt = u - _parallelOutputs; + unsigned numI2S = !PolyBus::isParallelI2S1Output(); // if using parallel I2S, RMT is used 1st + if (numI2S > u) continue; + if (u > 7 + numI2S) return; + rmt = u - numI2S; #endif - if (busses[u]->getLength()==0 || !busses[u]->isDigital() || busses[u]->is2Pin()) continue; //assumes that bus number to rmt channel mapping stays 1:1 rmt_channel_t ch = static_cast(rmt); rmt_idle_level_t lvl; @@ -902,6 +929,7 @@ void BusManager::esp32RMTInvertIdle() { else if (lvl == RMT_IDLE_LEVEL_LOW) lvl = RMT_IDLE_LEVEL_HIGH; else continue; rmt_set_idle_level(ch, idle_out, lvl); + u++ } } #endif @@ -910,12 +938,12 @@ void BusManager::on() { #ifdef ESP8266 //Fix for turning off onboard LED breaking bus if (PinManager::getPinOwner(LED_BUILTIN) == PinOwner::BusDigital) { - for (unsigned i = 0; i < numBusses; i++) { + for (auto &bus : busses) { uint8_t pins[2] = {255,255}; - if (busses[i]->isDigital() && busses[i]->getPins(pins)) { + if (bus->isDigital() && bus->getPins(pins)) { if (pins[0] == LED_BUILTIN || pins[1] == LED_BUILTIN) { - BusDigital *bus = static_cast(busses[i]); - bus->begin(); + BusDigital &b = static_cast(*bus); + b.begin(); break; } } @@ -932,7 +960,7 @@ void BusManager::off() { // 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 (unsigned i = 0; i < numBusses; i++) if (busses[i]->isOffRefreshRequired()) return; + for (const auto &bus : busses) if (bus->isOffRefreshRequired()) return; pinMode(LED_BUILTIN, OUTPUT); digitalWrite(LED_BUILTIN, HIGH); } @@ -943,30 +971,18 @@ void BusManager::off() { } void BusManager::show() { - _milliAmpsUsed = 0; - for (unsigned i = 0; i < numBusses; i++) { - busses[i]->show(); - _milliAmpsUsed += busses[i]->getUsedCurrent(); - } -} - -void BusManager::setStatusPixel(uint32_t c) { - for (unsigned i = 0; i < numBusses; i++) { - busses[i]->setStatusPixel(c); + _gMilliAmpsUsed = 0; + for (auto &bus : busses) { + bus->show(); + _gMilliAmpsUsed += bus->getUsedCurrent(); } } void IRAM_ATTR BusManager::setPixelColor(unsigned pix, uint32_t c) { - for (unsigned i = 0; i < numBusses; i++) { - unsigned bstart = busses[i]->getStart(); - if (pix < bstart || pix >= bstart + busses[i]->getLength()) continue; - busses[i]->setPixelColor(pix - bstart, c); - } -} - -void BusManager::setBrightness(uint8_t b) { - for (unsigned i = 0; i < numBusses; i++) { - busses[i]->setBrightness(b); + for (auto &bus : busses) { + unsigned bstart = bus->getStart(); + if (pix < bstart || pix >= bstart + bus->getLength()) continue; + bus->setPixelColor(pix - bstart, c); } } @@ -980,34 +996,23 @@ void BusManager::setSegmentCCT(int16_t cct, bool allowWBCorrection) { } uint32_t BusManager::getPixelColor(unsigned pix) { - for (unsigned i = 0; i < numBusses; i++) { - unsigned bstart = busses[i]->getStart(); - if (!busses[i]->containsPixel(pix)) continue; - return busses[i]->getPixelColor(pix - bstart); + for (auto &bus : busses) { + unsigned bstart = bus->getStart(); + if (!bus->containsPixel(pix)) continue; + return bus->getPixelColor(pix - bstart); } return 0; } bool BusManager::canAllShow() { - for (unsigned i = 0; i < numBusses; i++) { - if (!busses[i]->canShow()) return false; - } + for (const auto &bus : busses) if (!bus->canShow()) return false; return true; } -Bus* BusManager::getBus(uint8_t busNr) { - if (busNr >= numBusses) return nullptr; - return busses[busNr]; -} +ColorOrderMap& BusManager::getColorOrderMap() { return _colorOrderMap; } -//semi-duplicate of strip.getLengthTotal() (though that just returns strip._length, calculated in finalizeInit()) -uint16_t BusManager::getTotalLength() { - unsigned len = 0; - for (unsigned i=0; igetLength(); - return len; -} -bool PolyBus::useParallelI2S = false; +bool PolyBus::_useParallelI2S = false; // Bus static member definition int16_t Bus::_cct = -1; @@ -1016,9 +1021,7 @@ uint8_t Bus::_gAWM = 255; uint16_t BusDigital::_milliAmpsTotal = 0; -uint8_t BusManager::numBusses = 0; -Bus* BusManager::busses[WLED_MAX_BUSSES+WLED_MIN_VIRTUAL_BUSSES]; -ColorOrderMap BusManager::colorOrderMap = {}; -uint16_t BusManager::_milliAmpsUsed = 0; -uint16_t BusManager::_milliAmpsMax = ABL_MILLIAMPS_DEFAULT; -uint8_t BusManager::_parallelOutputs = 1; +std::vector> BusManager::busses; +//std::vector BusManager::busses; +uint16_t BusManager::_gMilliAmpsUsed = 0; +uint16_t BusManager::_gMilliAmpsMax = ABL_MILLIAMPS_DEFAULT; diff --git a/wled00/bus_manager.h b/wled00/bus_manager.h index 9aed01308..0570cc2d6 100644 --- a/wled00/bus_manager.h +++ b/wled00/bus_manager.h @@ -1,3 +1,4 @@ +#pragma once #ifndef BusManager_h #define BusManager_h @@ -8,6 +9,42 @@ #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); @@ -69,59 +106,59 @@ class Bus { : _type(type) , _bri(255) , _start(start) - , _len(len) + , _len(std::max(len,(uint16_t)1)) , _reversed(reversed) , _valid(false) , _needsRefresh(refresh) - , _data(nullptr) // keep data access consistent across all types of buses { _autoWhiteMode = Bus::hasWhite(type) ? aw : RGBW_MODE_MANUAL_ONLY; }; - virtual ~Bus() {} //throw the bus under the bus + virtual ~Bus() {} //throw the bus under the bus (derived class needs to freeData()) - virtual void begin() {}; + virtual void begin() {}; virtual void show() = 0; - virtual bool canShow() const { return true; } - virtual void setStatusPixel(uint32_t c) {} + 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 uint8_t getPins(uint8_t* pinArray = nullptr) const { return 0; } - virtual uint16_t getLength() const { return isOk() ? _len : 0; } - virtual uint8_t getColorOrder() const { return COL_ORDER_RGB; } - virtual uint8_t 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 void setBrightness(uint8_t b) { _bri = b; }; + virtual void setColorOrder(uint8_t co) {} + virtual uint32_t getPixelColor(unsigned pix) const { return 0; } + virtual unsigned getPins(uint8_t* pinArray = nullptr) const { return 0; } + virtual uint16_t getLength() const { return isOk() ? _len : 0; } + 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 unsigned getBusSize() const { return sizeof(Bus); } - 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); } - 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 uint32_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; } + 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); } + 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 unsigned 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 uint32_t getNumberOfPins(uint8_t type) { return isVirtual(type) ? 4 : isPWM(type) ? numPWMPins(type) : is2Pin(type) + 1; } // credit @PaoloTK - static constexpr uint32_t getNumberOfChannels(uint8_t type) { return hasWhite(type) + 3*hasRGB(type) + hasCCT(type); } + static inline std::vector getLEDTypes() { return {{TYPE_NONE, "", PSTR("None")}}; } // not used. just for reference for derived classes + static constexpr unsigned getNumberOfPins(uint8_t type) { return isVirtual(type) ? 4 : isPWM(type) ? numPWMPins(type) : is2Pin(type) + 1; } // credit @PaoloTK + static constexpr unsigned 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); } @@ -153,7 +190,7 @@ class Bus { static inline uint8_t getGlobalAWMode() { return _gAWM; } static inline void setCCT(int16_t cct) { _cct = cct; } static inline uint8_t getCCTBlend() { return _cctBlend; } - static inline void setCCTBlend(uint8_t b) { + static inline void setCCTBlend(uint8_t b) { _cctBlend = (std::min((int)b,100) * 127) / 100; //compile-time limiter for hardware that can't power both white channels at max #ifdef WLED_MAX_CCT_BLEND @@ -176,7 +213,6 @@ class Bus { bool _hasCCT;// : 1; //} __attribute__ ((packed)); uint8_t _autoWhiteMode; - uint8_t *_data; // global Auto White Calculation override static uint8_t _gAWM; // _cct has the following menaings (see calculateCCT() & BusManager::setSegmentCCT()): @@ -191,14 +227,12 @@ class Bus { static uint8_t _cctBlend; uint32_t autoWhiteCalc(uint32_t c) const; - uint8_t *allocateData(size_t size = 1); - void freeData() { if (_data != nullptr) free(_data); _data = nullptr; } }; class BusDigital : public Bus { public: - BusDigital(const BusConfig &bc, uint8_t nr, const ColorOrderMap &com); + BusDigital(const BusConfig &bc, uint8_t nr); ~BusDigital() { cleanup(); } void show() override; @@ -209,27 +243,28 @@ class BusDigital : public Bus { void setColorOrder(uint8_t colorOrder) override; [[gnu::hot]] uint32_t getPixelColor(unsigned pix) const override; uint8_t getColorOrder() const override { return _colorOrder; } - uint8_t getPins(uint8_t* pinArray = nullptr) const override; - uint8_t skippedLeds() const override { return _skip; } + unsigned 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; } + unsigned getBusSize() const override; 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 _skip; + uint8_t _colorOrder; + uint8_t _pins[2]; + uint8_t _iType; uint16_t _frequencykHz; - uint8_t _milliAmpsPerLed; + uint8_t _milliAmpsPerLed; uint16_t _milliAmpsMax; - void * _busPtr; - const ColorOrderMap &_colorOrderMap; + uint8_t *_data; + void *_busPtr; static uint16_t _milliAmpsTotal; // is overwitten/recalculated on each show() @@ -244,7 +279,7 @@ class BusDigital : public Bus { return c; } - uint8_t estimateCurrentAndLimitBri(); + uint8_t estimateCurrentAndLimitBri() const; }; @@ -255,16 +290,17 @@ class BusPwm : public Bus { void setPixelColor(unsigned pix, uint32_t c) override; uint32_t getPixelColor(unsigned pix) const override; //does no index check - uint8_t getPins(uint8_t* pinArray = nullptr) const override; + unsigned getPins(uint8_t* pinArray = nullptr) const override; uint16_t getFrequency() const override { return _frequency; } + unsigned getBusSize() const override { return sizeof(BusPwm); } void show() override; - void cleanup() { deallocatePins(); } + inline void cleanup() { deallocatePins(); } static std::vector getLEDTypes(); private: uint8_t _pins[OUTPUT_MAX_PINS]; - uint8_t _pwmdata[OUTPUT_MAX_PINS]; + uint8_t _data[OUTPUT_MAX_PINS]; #ifdef ARDUINO_ARCH_ESP32 uint8_t _ledcStart; #endif @@ -282,15 +318,16 @@ class BusOnOff : public Bus { void setPixelColor(unsigned pix, uint32_t c) override; uint32_t getPixelColor(unsigned pix) const override; - uint8_t getPins(uint8_t* pinArray) const override; + unsigned getPins(uint8_t* pinArray) const override; + unsigned getBusSize() const override { return sizeof(BusOnOff); } void show() override; - void cleanup() { PinManager::deallocatePin(_pin, PinOwner::BusOnOff); } + inline void cleanup() { PinManager::deallocatePin(_pin, PinOwner::BusOnOff); } static std::vector getLEDTypes(); private: uint8_t _pin; - uint8_t _onoffdata; + uint8_t _data; }; @@ -300,9 +337,10 @@ class BusNetwork : public Bus { ~BusNetwork() { cleanup(); } bool canShow() const override { return !_broadcastLock; } // this should be a return value from UDP routine if it is still sending data out - void setPixelColor(unsigned pix, uint32_t c) override; - uint32_t getPixelColor(unsigned pix) const override; - uint8_t getPins(uint8_t* pinArray = nullptr) const override; + [[gnu::hot]] void setPixelColor(unsigned pix, uint32_t c) override; + [[gnu::hot]] uint32_t getPixelColor(unsigned pix) const override; + unsigned getPins(uint8_t* pinArray = nullptr) const override; + unsigned getBusSize() const override { return sizeof(BusNetwork) + (isOk() ? _len * _UDPchannels : 0); } void show() override; void cleanup(); @@ -313,6 +351,7 @@ class BusNetwork : public Bus { uint8_t _UDPtype; uint8_t _UDPchannels; bool _broadcastLock; + uint8_t *_data; }; @@ -333,7 +372,7 @@ struct BusConfig { uint16_t milliAmpsMax; 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, bool dblBfr=false, uint8_t maPerLed=LED_MILLIAMPS_DEFAULT, uint16_t maMax=ABL_MILLIAMPS_DEFAULT) - : count(len) + : count(std::max(len,(uint16_t)1)) , start(pstart) , colorOrder(pcolorOrder) , reversed(rev) @@ -348,6 +387,16 @@ struct BusConfig { 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)\n"), + (int)start, (int)(start+len), + (int)type, + (int)colorOrder, + (int)reversed, + (int)skipAmount, + (int)autoWhite, + (int)frequency, + (int)milliAmpsPerLed, (int)milliAmpsMax + ); } //validates start and length and extends total if needed @@ -361,6 +410,8 @@ struct BusConfig { if (start + count > total) total = start + count; return true; } + + unsigned memUsage(unsigned nr = 0) const; }; @@ -374,61 +425,58 @@ struct BusConfig { #endif #endif -class BusManager { - public: - BusManager() {}; +namespace BusManager { - //utility to get the approx. memory usage of a given BusConfig - static uint32_t memUsage(const BusConfig &bc); - static uint32_t memUsage(unsigned channels, unsigned count, unsigned buses = 1); - static uint16_t currentMilliamps() { return _milliAmpsUsed + MA_FOR_ESP; } - static uint16_t ablMilliampsMax() { return _milliAmpsMax; } + extern std::vector> busses; + //extern std::vector busses; + extern uint16_t _gMilliAmpsUsed; + extern uint16_t _gMilliAmpsMax; - static int add(const BusConfig &bc); - static void useParallelOutput(); // workaround for inaccessible PolyBus + #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; + } - //do not call this method from system context (network callback) - static void removeAll(); + size_t memUsage(); + 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;} - static void on(); - static void off(); + void useParallelOutput(); // workaround for inaccessible PolyBus + bool hasParallelOutput(); // workaround for inaccessible PolyBus - static void show(); - static bool canAllShow(); - static void setStatusPixel(uint32_t c); - [[gnu::hot]] static void setPixelColor(unsigned pix, uint32_t c); - static void setBrightness(uint8_t 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() - static void setSegmentCCT(int16_t cct, bool allowWBCorrection = false); - static inline void setMilliampsMax(uint16_t max) { _milliAmpsMax = max;} - [[gnu::hot]] static uint32_t getPixelColor(unsigned pix); - static inline int16_t getSegmentCCT() { return Bus::getCCT(); } + //do not call this method from system context (network callback) + void removeAll(); + int add(const BusConfig &bc); - static Bus* getBus(uint8_t busNr); + void on(); + void off(); - //semi-duplicate of strip.getLengthTotal() (though that just returns strip._length, calculated in finalizeInit()) - static uint16_t getTotalLength(); - static inline uint8_t getNumBusses() { return numBusses; } - static String getLEDTypesJSONString(); + [[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(); } - static inline ColorOrderMap& getColorOrderMap() { return colorOrderMap; } - - private: - static uint8_t numBusses; - static Bus* busses[WLED_MAX_BUSSES+WLED_MIN_VIRTUAL_BUSSES]; - static ColorOrderMap colorOrderMap; - static uint16_t _milliAmpsUsed; - static uint16_t _milliAmpsMax; - static uint8_t _parallelOutputs; - - #ifdef ESP32_DATA_IDLE_HIGH - static void esp32RMTInvertIdle() ; - #endif - static uint8_t getNumVirtualBusses() { - int j = 0; - for (int i=0; iisVirtual()) j++; - return j; - } + //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 diff --git a/wled00/bus_wrapper.h b/wled00/bus_wrapper.h index d2a18c9d8..577aaeb82 100644 --- a/wled00/bus_wrapper.h +++ b/wled00/bus_wrapper.h @@ -1,23 +1,9 @@ +#pragma once #ifndef BusWrapper_h #define BusWrapper_h +//#define NPB_CONF_4STEP_CADENCE #include "NeoPixelBusLg.h" -#include "bus_manager.h" - -// temporary - these defines should actually be set in platformio.ini -// C3: I2S0 and I2S1 methods not supported (has one I2S bus) -// S2: I2S1 methods not supported (has one I2S bus) -// S3: I2S0 and I2S1 methods not supported yet (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(WLED_NO_I2S0_PIXELBUS) && (defined(CONFIG_IDF_TARGET_ESP32S3) || defined(CONFIG_IDF_TARGET_ESP32C3)) -#define WLED_NO_I2S0_PIXELBUS -#endif -#if !defined(WLED_NO_I2S1_PIXELBUS) && (defined(CONFIG_IDF_TARGET_ESP32S3) || defined(CONFIG_IDF_TARGET_ESP32C3) || defined(CONFIG_IDF_TARGET_ESP32S2)) -#define WLED_NO_I2S1_PIXELBUS -#endif -// temporary end //Hardware SPI Pins #define P_8266_HS_MOSI 13 @@ -55,110 +41,98 @@ #define I_8266_DM_TM2_3 19 #define I_8266_BB_TM2_3 20 //UCS8903 (RGB) -#define I_8266_U0_UCS_3 49 -#define I_8266_U1_UCS_3 50 -#define I_8266_DM_UCS_3 51 -#define I_8266_BB_UCS_3 52 +#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 53 -#define I_8266_U1_UCS_4 54 -#define I_8266_DM_UCS_4 55 -#define I_8266_BB_UCS_4 56 +#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 66 -#define I_8266_U1_FW6_5 67 -#define I_8266_DM_FW6_5 68 -#define I_8266_BB_FW6_5 69 +#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 81 -#define I_8266_U1_APA106_3 82 -#define I_8266_DM_APA106_3 83 -#define I_8266_BB_APA106_3 84 +#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 89 -#define I_8266_U1_2805_5 90 -#define I_8266_DM_2805_5 91 -#define I_8266_BB_2805_5 92 +#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 99 -#define I_8266_U1_TM1914_3 100 -#define I_8266_DM_TM1914_3 101 -#define I_8266_BB_TM1914_3 102 +#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 103 -#define I_8266_U1_SM16825_5 104 -#define I_8266_DM_SM16825_5 105 -#define I_8266_BB_SM16825_5 106 +#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 21 -#define I_32_I0_NEO_3 22 -#define I_32_I1_NEO_3 23 +#define I_32_RN_NEO_3 1 +#define I_32_I2_NEO_3 2 //RGBW -#define I_32_RN_NEO_4 25 -#define I_32_I0_NEO_4 26 -#define I_32_I1_NEO_4 27 +#define I_32_RN_NEO_4 5 +#define I_32_I2_NEO_4 6 //400Kbps -#define I_32_RN_400_3 29 -#define I_32_I0_400_3 30 -#define I_32_I1_400_3 31 +#define I_32_RN_400_3 9 +#define I_32_I2_400_3 10 //TM1814 (RGBW) -#define I_32_RN_TM1_4 33 -#define I_32_I0_TM1_4 34 -#define I_32_I1_TM1_4 35 +#define I_32_RN_TM1_4 13 +#define I_32_I2_TM1_4 14 //TM1829 (RGB) -#define I_32_RN_TM2_3 36 -#define I_32_I0_TM2_3 37 -#define I_32_I1_TM2_3 38 +#define I_32_RN_TM2_3 17 +#define I_32_I2_TM2_3 18 //UCS8903 (RGB) -#define I_32_RN_UCS_3 57 -#define I_32_I0_UCS_3 58 -#define I_32_I1_UCS_3 59 +#define I_32_RN_UCS_3 21 +#define I_32_I2_UCS_3 22 //UCS8904 (RGBW) -#define I_32_RN_UCS_4 60 -#define I_32_I0_UCS_4 61 -#define I_32_I1_UCS_4 62 +#define I_32_RN_UCS_4 25 +#define I_32_I2_UCS_4 26 //FW1906 GRBCW -#define I_32_RN_FW6_5 63 -#define I_32_I0_FW6_5 64 -#define I_32_I1_FW6_5 65 +#define I_32_RN_FW6_5 29 +#define I_32_I2_FW6_5 30 //APA106 -#define I_32_RN_APA106_3 85 -#define I_32_I0_APA106_3 86 -#define I_32_I1_APA106_3 87 +#define I_32_RN_APA106_3 33 +#define I_32_I2_APA106_3 34 //WS2805 (RGBCW) -#define I_32_RN_2805_5 93 -#define I_32_I0_2805_5 94 -#define I_32_I1_2805_5 95 +#define I_32_RN_2805_5 37 +#define I_32_I2_2805_5 38 //TM1914 (RGB) -#define I_32_RN_TM1914_3 96 -#define I_32_I0_TM1914_3 97 -#define I_32_I1_TM1914_3 98 +#define I_32_RN_TM1914_3 41 +#define I_32_I2_TM1914_3 42 //SM16825 (RGBCW) -#define I_32_RN_SM16825_5 107 -#define I_32_I0_SM16825_5 108 -#define I_32_I1_SM16825_5 109 +#define I_32_RN_SM16825_5 45 +#define I_32_I2_SM16825_5 46 //APA102 -#define I_HS_DOT_3 39 //hardware SPI -#define I_SS_DOT_3 40 //soft SPI +#define I_HS_DOT_3 101 //hardware SPI +#define I_SS_DOT_3 102 //soft SPI //LPD8806 -#define I_HS_LPD_3 41 -#define I_SS_LPD_3 42 +#define I_HS_LPD_3 103 +#define I_SS_LPD_3 104 //WS2801 -#define I_HS_WS1_3 43 -#define I_SS_WS1_3 44 +#define I_HS_WS1_3 105 +#define I_SS_WS1_3 106 //P9813 -#define I_HS_P98_3 45 -#define I_SS_P98_3 46 +#define I_HS_P98_3 107 +#define I_SS_P98_3 108 //LPD6803 -#define I_HS_LPO_3 47 -#define I_SS_LPO_3 48 +#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 @@ -230,66 +204,95 @@ /*** 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 + //RGB -#define B_32_RN_NEO_3 NeoPixelBusLg -#define B_32_I0_NEO_3 NeoPixelBusLg -#define B_32_I1_NEO_3 NeoPixelBusLg -#define B_32_I1_NEO_3P NeoPixelBusLg // parallel I2S +#define B_32_RN_NEO_3 NeoPixelBusLg // ESP32, S2, S3, C3 +//#define B_32_IN_NEO_3 NeoPixelBusLg // ESP32 (dynamic I2S selection) +#define B_32_I2_NEO_3 NeoPixelBusLg // ESP32, S2, S3 (automatic I2S selection, see typedef above) +#define B_32_IP_NEO_3 NeoPixelBusLg // parallel I2S (ESP32, S2, S3) //RGBW #define B_32_RN_NEO_4 NeoPixelBusLg -#define B_32_I0_NEO_4 NeoPixelBusLg -#define B_32_I1_NEO_4 NeoPixelBusLg -#define B_32_I1_NEO_4P NeoPixelBusLg // parallel I2S +#define B_32_I2_NEO_4 NeoPixelBusLg +#define B_32_IP_NEO_4 NeoPixelBusLg // parallel I2S //400Kbps #define B_32_RN_400_3 NeoPixelBusLg -#define B_32_I0_400_3 NeoPixelBusLg -#define B_32_I1_400_3 NeoPixelBusLg -#define B_32_I1_400_3P NeoPixelBusLg // parallel I2S +#define B_32_I2_400_3 NeoPixelBusLg +#define B_32_IP_400_3 NeoPixelBusLg // parallel I2S //TM1814 (RGBW) #define B_32_RN_TM1_4 NeoPixelBusLg -#define B_32_I0_TM1_4 NeoPixelBusLg -#define B_32_I1_TM1_4 NeoPixelBusLg -#define B_32_I1_TM1_4P NeoPixelBusLg // parallel I2S +#define B_32_I2_TM1_4 NeoPixelBusLg +#define B_32_IP_TM1_4 NeoPixelBusLg // parallel I2S //TM1829 (RGB) #define B_32_RN_TM2_3 NeoPixelBusLg -#define B_32_I0_TM2_3 NeoPixelBusLg -#define B_32_I1_TM2_3 NeoPixelBusLg -#define B_32_I1_TM2_3P NeoPixelBusLg // parallel I2S +#define B_32_I2_TM2_3 NeoPixelBusLg +#define B_32_IP_TM2_3 NeoPixelBusLg // parallel I2S //UCS8903 #define B_32_RN_UCS_3 NeoPixelBusLg -#define B_32_I0_UCS_3 NeoPixelBusLg -#define B_32_I1_UCS_3 NeoPixelBusLg -#define B_32_I1_UCS_3P NeoPixelBusLg // parallel I2S +#define B_32_I2_UCS_3 NeoPixelBusLg +#define B_32_IP_UCS_3 NeoPixelBusLg // parallel I2S //UCS8904 #define B_32_RN_UCS_4 NeoPixelBusLg -#define B_32_I0_UCS_4 NeoPixelBusLg -#define B_32_I1_UCS_4 NeoPixelBusLg -#define B_32_I1_UCS_4P NeoPixelBusLg// parallel I2S +#define B_32_I2_UCS_4 NeoPixelBusLg +#define B_32_IP_UCS_4 NeoPixelBusLg// parallel I2S //APA106 #define B_32_RN_APA106_3 NeoPixelBusLg -#define B_32_I0_APA106_3 NeoPixelBusLg -#define B_32_I1_APA106_3 NeoPixelBusLg -#define B_32_I1_APA106_3P NeoPixelBusLg // parallel I2S +#define B_32_I2_APA106_3 NeoPixelBusLg +#define B_32_IP_APA106_3 NeoPixelBusLg // parallel I2S //FW1906 GRBCW #define B_32_RN_FW6_5 NeoPixelBusLg -#define B_32_I0_FW6_5 NeoPixelBusLg -#define B_32_I1_FW6_5 NeoPixelBusLg -#define B_32_I1_FW6_5P NeoPixelBusLg // parallel I2S +#define B_32_I2_FW6_5 NeoPixelBusLg +#define B_32_IP_FW6_5 NeoPixelBusLg // parallel I2S //WS2805 RGBWC #define B_32_RN_2805_5 NeoPixelBusLg -#define B_32_I0_2805_5 NeoPixelBusLg -#define B_32_I1_2805_5 NeoPixelBusLg -#define B_32_I1_2805_5P NeoPixelBusLg // parallel I2S +#define B_32_I2_2805_5 NeoPixelBusLg +#define B_32_IP_2805_5 NeoPixelBusLg // parallel I2S //TM1914 (RGB) #define B_32_RN_TM1914_3 NeoPixelBusLg -#define B_32_I0_TM1914_3 NeoPixelBusLg -#define B_32_I1_TM1914_3 NeoPixelBusLg -#define B_32_I1_TM1914_3P NeoPixelBusLg // parallel I2S +#define B_32_I2_TM1914_3 NeoPixelBusLg +#define B_32_IP_TM1914_3 NeoPixelBusLg // parallel I2S //Sm16825 (RGBWC) #define B_32_RN_SM16825_5 NeoPixelBusLg -#define B_32_I0_SM16825_5 NeoPixelBusLg -#define B_32_I1_SM16825_5 NeoPixelBusLg -#define B_32_I1_SM16825_5P NeoPixelBusLg // parallel I2S +#define B_32_I2_SM16825_5 NeoPixelBusLg +#define B_32_IP_SM16825_5 NeoPixelBusLg // parallel I2S #endif //APA102 @@ -328,11 +331,11 @@ //handles pointer type conversion for all possible bus types class PolyBus { private: - static bool useParallelI2S; + static bool _useParallelI2S; public: - static inline void setParallelI2S1Output(bool b = true) { useParallelI2S = b; } - static inline bool isParallelI2S1Output(void) { return useParallelI2S; } + static inline void setParallelI2S1Output(bool b = true) { _useParallelI2S = b; } + static inline bool isParallelI2S1Output(void) { return _useParallelI2S; } // initialize SPI bus speed for DotStar methods template @@ -436,34 +439,19 @@ class PolyBus { 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 WLED_NO_I2S1_PIXELBUS - case I_32_I1_NEO_3: if (useParallelI2S) (static_cast(busPtr))->Begin(); else (static_cast(busPtr))->Begin(); break; - case I_32_I1_NEO_4: if (useParallelI2S) (static_cast(busPtr))->Begin(); else (static_cast(busPtr))->Begin(); break; - case I_32_I1_400_3: if (useParallelI2S) (static_cast(busPtr))->Begin(); else (static_cast(busPtr))->Begin(); break; - case I_32_I1_TM1_4: if (useParallelI2S) beginTM1814(busPtr); else beginTM1814(busPtr); break; - case I_32_I1_TM2_3: if (useParallelI2S) (static_cast(busPtr))->Begin(); else (static_cast(busPtr))->Begin(); break; - case I_32_I1_UCS_3: if (useParallelI2S) (static_cast(busPtr))->Begin(); else (static_cast(busPtr))->Begin(); break; - case I_32_I1_UCS_4: if (useParallelI2S) (static_cast(busPtr))->Begin(); else (static_cast(busPtr))->Begin(); break; - case I_32_I1_FW6_5: if (useParallelI2S) (static_cast(busPtr))->Begin(); else (static_cast(busPtr))->Begin(); break; - case I_32_I1_APA106_3: if (useParallelI2S) (static_cast(busPtr))->Begin(); else (static_cast(busPtr))->Begin(); break; - case I_32_I1_2805_5: if (useParallelI2S) (static_cast(busPtr))->Begin(); else (static_cast(busPtr))->Begin(); break; - case I_32_I1_TM1914_3: if (useParallelI2S) beginTM1914(busPtr); else beginTM1914(busPtr); break; - case I_32_I1_SM16825_5: if (useParallelI2S) (static_cast(busPtr))->Begin(); else (static_cast(busPtr))->Begin(); break; - #endif - // I2S0 bus - #ifndef WLED_NO_I2S0_PIXELBUS - case I_32_I0_NEO_3: (static_cast(busPtr))->Begin(); break; - case I_32_I0_NEO_4: (static_cast(busPtr))->Begin(); break; - case I_32_I0_400_3: (static_cast(busPtr))->Begin(); break; - case I_32_I0_TM1_4: beginTM1814(busPtr); break; - case I_32_I0_TM2_3: (static_cast(busPtr))->Begin(); break; - case I_32_I0_UCS_3: (static_cast(busPtr))->Begin(); break; - case I_32_I0_UCS_4: (static_cast(busPtr))->Begin(); break; - case I_32_I0_FW6_5: (static_cast(busPtr))->Begin(); break; - case I_32_I0_APA106_3: (static_cast(busPtr))->Begin(); break; - case I_32_I0_2805_5: (static_cast(busPtr))->Begin(); break; - case I_32_I0_TM1914_3: beginTM1914(busPtr); break; - case I_32_I0_SM16825_5: (static_cast(busPtr))->Begin(); break; + #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; @@ -484,8 +472,8 @@ class PolyBus { #if defined(ARDUINO_ARCH_ESP32) && !(defined(CONFIG_IDF_TARGET_ESP32S2) || defined(CONFIG_IDF_TARGET_ESP32S3) || defined(CONFIG_IDF_TARGET_ESP32C3)) // NOTE: "channel" is only used on ESP32 (and its variants) for RMT channel allocation // since 0.15.0-b3 I2S1 is favoured for classic ESP32 and moved to position 0 (channel 0) so we need to subtract 1 for correct RMT allocation - if (useParallelI2S && channel > 7) channel -= 8; // accommodate parallel I2S1 which is used 1st on classic ESP32 - else if (channel > 0) channel--; // accommodate I2S1 which is used as 1st bus on classic ESP32 + if (!_useParallelI2S && channel > 0) channel--; // accommodate I2S1 which is used as 1st bus on classic ESP32 + // if user selected parallel I2S, RMT is used 1st (8 channels) followed by parallel I2S (8 channels) #endif void* busPtr = nullptr; switch (busType) { @@ -555,34 +543,19 @@ class PolyBus { case I_32_RN_TM1914_3: busPtr = new B_32_RN_TM1914_3(len, pins[0], (NeoBusChannel)channel); break; case I_32_RN_SM16825_5: busPtr = new B_32_RN_SM16825_5(len, pins[0], (NeoBusChannel)channel); break; // I2S1 bus or paralell buses - #ifndef WLED_NO_I2S1_PIXELBUS - case I_32_I1_NEO_3: if (useParallelI2S) busPtr = new B_32_I1_NEO_3P(len, pins[0]); else busPtr = new B_32_I1_NEO_3(len, pins[0]); break; - case I_32_I1_NEO_4: if (useParallelI2S) busPtr = new B_32_I1_NEO_4P(len, pins[0]); else busPtr = new B_32_I1_NEO_4(len, pins[0]); break; - case I_32_I1_400_3: if (useParallelI2S) busPtr = new B_32_I1_400_3P(len, pins[0]); else busPtr = new B_32_I1_400_3(len, pins[0]); break; - case I_32_I1_TM1_4: if (useParallelI2S) busPtr = new B_32_I1_TM1_4P(len, pins[0]); else busPtr = new B_32_I1_TM1_4(len, pins[0]); break; - case I_32_I1_TM2_3: if (useParallelI2S) busPtr = new B_32_I1_TM2_3P(len, pins[0]); else busPtr = new B_32_I1_TM2_3(len, pins[0]); break; - case I_32_I1_UCS_3: if (useParallelI2S) busPtr = new B_32_I1_UCS_3P(len, pins[0]); else busPtr = new B_32_I1_UCS_3(len, pins[0]); break; - case I_32_I1_UCS_4: if (useParallelI2S) busPtr = new B_32_I1_UCS_4P(len, pins[0]); else busPtr = new B_32_I1_UCS_4(len, pins[0]); break; - case I_32_I1_APA106_3: if (useParallelI2S) busPtr = new B_32_I1_APA106_3P(len, pins[0]); else busPtr = new B_32_I1_APA106_3(len, pins[0]); break; - case I_32_I1_FW6_5: if (useParallelI2S) busPtr = new B_32_I1_FW6_5P(len, pins[0]); else busPtr = new B_32_I1_FW6_5(len, pins[0]); break; - case I_32_I1_2805_5: if (useParallelI2S) busPtr = new B_32_I1_2805_5P(len, pins[0]); else busPtr = new B_32_I1_2805_5(len, pins[0]); break; - case I_32_I1_TM1914_3: if (useParallelI2S) busPtr = new B_32_I1_TM1914_3P(len, pins[0]); else busPtr = new B_32_I1_TM1914_3(len, pins[0]); break; - case I_32_I1_SM16825_5: if (useParallelI2S) busPtr = new B_32_I1_SM16825_5P(len, pins[0]); else busPtr = new B_32_I1_SM16825_5(len, pins[0]); break; - #endif - // I2S0 bus - #ifndef WLED_NO_I2S0_PIXELBUS - case I_32_I0_NEO_3: busPtr = new B_32_I0_NEO_3(len, pins[0]); break; - case I_32_I0_NEO_4: busPtr = new B_32_I0_NEO_4(len, pins[0]); break; - case I_32_I0_400_3: busPtr = new B_32_I0_400_3(len, pins[0]); break; - case I_32_I0_TM1_4: busPtr = new B_32_I0_TM1_4(len, pins[0]); break; - case I_32_I0_TM2_3: busPtr = new B_32_I0_TM2_3(len, pins[0]); break; - case I_32_I0_UCS_3: busPtr = new B_32_I0_UCS_3(len, pins[0]); break; - case I_32_I0_UCS_4: busPtr = new B_32_I0_UCS_4(len, pins[0]); break; - case I_32_I0_APA106_3: busPtr = new B_32_I0_APA106_3(len, pins[0]); break; - case I_32_I0_FW6_5: busPtr = new B_32_I0_FW6_5(len, pins[0]); break; - case I_32_I0_2805_5: busPtr = new B_32_I0_2805_5(len, pins[0]); break; - case I_32_I0_TM1914_3: busPtr = new B_32_I0_TM1914_3(len, pins[0]); break; - case I_32_I0_SM16825_5: busPtr = new B_32_I0_SM16825_5(len, pins[0]); break; + #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) @@ -669,34 +642,19 @@ class PolyBus { 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 WLED_NO_I2S1_PIXELBUS - case I_32_I1_NEO_3: if (useParallelI2S) (static_cast(busPtr))->Show(consistent); else (static_cast(busPtr))->Show(consistent); break; - case I_32_I1_NEO_4: if (useParallelI2S) (static_cast(busPtr))->Show(consistent); else (static_cast(busPtr))->Show(consistent); break; - case I_32_I1_400_3: if (useParallelI2S) (static_cast(busPtr))->Show(consistent); else (static_cast(busPtr))->Show(consistent); break; - case I_32_I1_TM1_4: if (useParallelI2S) (static_cast(busPtr))->Show(consistent); else (static_cast(busPtr))->Show(consistent); break; - case I_32_I1_TM2_3: if (useParallelI2S) (static_cast(busPtr))->Show(consistent); else (static_cast(busPtr))->Show(consistent); break; - case I_32_I1_UCS_3: if (useParallelI2S) (static_cast(busPtr))->Show(consistent); else (static_cast(busPtr))->Show(consistent); break; - case I_32_I1_UCS_4: if (useParallelI2S) (static_cast(busPtr))->Show(consistent); else (static_cast(busPtr))->Show(consistent); break; - case I_32_I1_APA106_3: if (useParallelI2S) (static_cast(busPtr))->Show(consistent); else (static_cast(busPtr))->Show(consistent); break; - case I_32_I1_FW6_5: if (useParallelI2S) (static_cast(busPtr))->Show(consistent); else (static_cast(busPtr))->Show(consistent); break; - case I_32_I1_2805_5: if (useParallelI2S) (static_cast(busPtr))->Show(consistent); else (static_cast(busPtr))->Show(consistent); break; - case I_32_I1_TM1914_3: if (useParallelI2S) (static_cast(busPtr))->Show(consistent); else (static_cast(busPtr))->Show(consistent); break; - case I_32_I1_SM16825_5: if (useParallelI2S) (static_cast(busPtr))->Show(consistent); else (static_cast(busPtr))->Show(consistent); break; - #endif - // I2S0 bus - #ifndef WLED_NO_I2S0_PIXELBUS - case I_32_I0_NEO_3: (static_cast(busPtr))->Show(consistent); break; - case I_32_I0_NEO_4: (static_cast(busPtr))->Show(consistent); break; - case I_32_I0_400_3: (static_cast(busPtr))->Show(consistent); break; - case I_32_I0_TM1_4: (static_cast(busPtr))->Show(consistent); break; - case I_32_I0_TM2_3: (static_cast(busPtr))->Show(consistent); break; - case I_32_I0_UCS_3: (static_cast(busPtr))->Show(consistent); break; - case I_32_I0_UCS_4: (static_cast(busPtr))->Show(consistent); break; - case I_32_I0_APA106_3: (static_cast(busPtr))->Show(consistent); break; - case I_32_I0_FW6_5: (static_cast(busPtr))->Show(consistent); break; - case I_32_I0_2805_5: (static_cast(busPtr))->Show(consistent); break; - case I_32_I0_TM1914_3: (static_cast(busPtr))->Show(consistent); break; - case I_32_I0_SM16825_5: (static_cast(busPtr))->Show(consistent); break; + #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; @@ -743,6 +701,7 @@ class PolyBus { 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; @@ -779,34 +738,19 @@ class PolyBus { 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 WLED_NO_I2S1_PIXELBUS - case I_32_I1_NEO_3: if (useParallelI2S) return (static_cast(busPtr))->CanShow(); else return (static_cast(busPtr))->CanShow(); break; - case I_32_I1_NEO_4: if (useParallelI2S) return (static_cast(busPtr))->CanShow(); else return (static_cast(busPtr))->CanShow(); break; - case I_32_I1_400_3: if (useParallelI2S) return (static_cast(busPtr))->CanShow(); else return (static_cast(busPtr))->CanShow(); break; - case I_32_I1_TM1_4: if (useParallelI2S) return (static_cast(busPtr))->CanShow(); else return (static_cast(busPtr))->CanShow(); break; - case I_32_I1_TM2_3: if (useParallelI2S) return (static_cast(busPtr))->CanShow(); else return (static_cast(busPtr))->CanShow(); break; - case I_32_I1_UCS_3: if (useParallelI2S) return (static_cast(busPtr))->CanShow(); else return (static_cast(busPtr))->CanShow(); break; - case I_32_I1_UCS_4: if (useParallelI2S) return (static_cast(busPtr))->CanShow(); else return (static_cast(busPtr))->CanShow(); break; - case I_32_I1_APA106_3: if (useParallelI2S) return (static_cast(busPtr))->CanShow(); else return (static_cast(busPtr))->CanShow(); break; - case I_32_I1_FW6_5: if (useParallelI2S) return (static_cast(busPtr))->CanShow(); else return (static_cast(busPtr))->CanShow(); break; - case I_32_I1_2805_5: if (useParallelI2S) return (static_cast(busPtr))->CanShow(); else return (static_cast(busPtr))->CanShow(); break; - case I_32_I1_TM1914_3: if (useParallelI2S) return (static_cast(busPtr))->CanShow(); else return (static_cast(busPtr))->CanShow(); break; - case I_32_I1_SM16825_5: if (useParallelI2S) return (static_cast(busPtr))->CanShow(); else return (static_cast(busPtr))->CanShow(); break; - #endif - // I2S0 bus - #ifndef WLED_NO_I2S0_PIXELBUS - case I_32_I0_NEO_3: return (static_cast(busPtr))->CanShow(); break; - case I_32_I0_NEO_4: return (static_cast(busPtr))->CanShow(); break; - case I_32_I0_400_3: return (static_cast(busPtr))->CanShow(); break; - case I_32_I0_TM1_4: return (static_cast(busPtr))->CanShow(); break; - case I_32_I0_TM2_3: return (static_cast(busPtr))->CanShow(); break; - case I_32_I0_UCS_3: return (static_cast(busPtr))->CanShow(); break; - case I_32_I0_UCS_4: return (static_cast(busPtr))->CanShow(); break; - case I_32_I0_APA106_3: return (static_cast(busPtr))->CanShow(); break; - case I_32_I0_FW6_5: return (static_cast(busPtr))->CanShow(); break; - case I_32_I0_2805_5: return (static_cast(busPtr))->CanShow(); break; - case I_32_I0_TM1914_3: return (static_cast(busPtr))->CanShow(); break; - case I_32_I0_SM16825_5: return (static_cast(busPtr))->CanShow(); break; + #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; @@ -823,7 +767,7 @@ class PolyBus { return true; } - static void setPixelColor(void* busPtr, uint8_t busType, uint16_t pix, uint32_t c, uint8_t co, uint16_t wwcw = 0) { + [[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; @@ -916,34 +860,19 @@ class PolyBus { 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 WLED_NO_I2S1_PIXELBUS - case I_32_I1_NEO_3: if (useParallelI2S) (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); else (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; - case I_32_I1_NEO_4: if (useParallelI2S) (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); else (static_cast(busPtr))->SetPixelColor(pix, col); break; - case I_32_I1_400_3: if (useParallelI2S) (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); else (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; - case I_32_I1_TM1_4: if (useParallelI2S) (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); else (static_cast(busPtr))->SetPixelColor(pix, col); break; - case I_32_I1_TM2_3: if (useParallelI2S) (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); else (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; - case I_32_I1_UCS_3: if (useParallelI2S) (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); else (static_cast(busPtr))->SetPixelColor(pix, Rgb48Color(RgbColor(col))); break; - case I_32_I1_UCS_4: if (useParallelI2S) (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); else (static_cast(busPtr))->SetPixelColor(pix, Rgbw64Color(col)); break; - case I_32_I1_APA106_3: if (useParallelI2S) (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); else (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; - case I_32_I1_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_I1_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_I1_TM1914_3: if (useParallelI2S) (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); else (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; - case I_32_I1_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 - // I2S0 bus - #ifndef WLED_NO_I2S0_PIXELBUS - case I_32_I0_NEO_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; - case I_32_I0_NEO_4: (static_cast(busPtr))->SetPixelColor(pix, col); break; - case I_32_I0_400_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; - case I_32_I0_TM1_4: (static_cast(busPtr))->SetPixelColor(pix, col); break; - case I_32_I0_TM2_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; - case I_32_I0_UCS_3: (static_cast(busPtr))->SetPixelColor(pix, Rgb48Color(RgbColor(col))); break; - case I_32_I0_UCS_4: (static_cast(busPtr))->SetPixelColor(pix, Rgbw64Color(col)); break; - case I_32_I0_APA106_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; - case I_32_I0_FW6_5: (static_cast(busPtr))->SetPixelColor(pix, RgbwwColor(col.R, col.G, col.B, cctWW, cctCW)); break; - case I_32_I0_2805_5: (static_cast(busPtr))->SetPixelColor(pix, RgbwwColor(col.R, col.G, col.B, cctWW, cctCW)); break; - case I_32_I0_TM1914_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; - case I_32_I0_SM16825_5: (static_cast(busPtr))->SetPixelColor(pix, Rgbww80Color(col.R*257, col.G*257, col.B*257, cctWW*257, cctCW*257)); break; + #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, RgbColor(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, RgbColor(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, 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, RgbColor(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; @@ -1027,34 +956,19 @@ class PolyBus { case I_32_RN_TM1914_3: (static_cast(busPtr))->SetLuminance(b); break; case I_32_RN_SM16825_5: (static_cast(busPtr))->SetLuminance(b); break; // I2S1 bus or paralell buses - #ifndef WLED_NO_I2S1_PIXELBUS - case I_32_I1_NEO_3: if (useParallelI2S) (static_cast(busPtr))->SetLuminance(b); else (static_cast(busPtr))->SetLuminance(b); break; - case I_32_I1_NEO_4: if (useParallelI2S) (static_cast(busPtr))->SetLuminance(b); else (static_cast(busPtr))->SetLuminance(b); break; - case I_32_I1_400_3: if (useParallelI2S) (static_cast(busPtr))->SetLuminance(b); else (static_cast(busPtr))->SetLuminance(b); break; - case I_32_I1_TM1_4: if (useParallelI2S) (static_cast(busPtr))->SetLuminance(b); else (static_cast(busPtr))->SetLuminance(b); break; - case I_32_I1_TM2_3: if (useParallelI2S) (static_cast(busPtr))->SetLuminance(b); else (static_cast(busPtr))->SetLuminance(b); break; - case I_32_I1_UCS_3: if (useParallelI2S) (static_cast(busPtr))->SetLuminance(b); else (static_cast(busPtr))->SetLuminance(b); break; - case I_32_I1_UCS_4: if (useParallelI2S) (static_cast(busPtr))->SetLuminance(b); else (static_cast(busPtr))->SetLuminance(b); break; - case I_32_I1_APA106_3: if (useParallelI2S) (static_cast(busPtr))->SetLuminance(b); else (static_cast(busPtr))->SetLuminance(b); break; - case I_32_I1_FW6_5: if (useParallelI2S) (static_cast(busPtr))->SetLuminance(b); else (static_cast(busPtr))->SetLuminance(b); break; - case I_32_I1_2805_5: if (useParallelI2S) (static_cast(busPtr))->SetLuminance(b); else (static_cast(busPtr))->SetLuminance(b); break; - case I_32_I1_TM1914_3: if (useParallelI2S) (static_cast(busPtr))->SetLuminance(b); else (static_cast(busPtr))->SetLuminance(b); break; - case I_32_I1_SM16825_5: if (useParallelI2S) (static_cast(busPtr))->SetLuminance(b); else (static_cast(busPtr))->SetLuminance(b); break; - #endif - // I2S0 bus - #ifndef WLED_NO_I2S0_PIXELBUS - case I_32_I0_NEO_3: (static_cast(busPtr))->SetLuminance(b); break; - case I_32_I0_NEO_4: (static_cast(busPtr))->SetLuminance(b); break; - case I_32_I0_400_3: (static_cast(busPtr))->SetLuminance(b); break; - case I_32_I0_TM1_4: (static_cast(busPtr))->SetLuminance(b); break; - case I_32_I0_TM2_3: (static_cast(busPtr))->SetLuminance(b); break; - case I_32_I0_UCS_3: (static_cast(busPtr))->SetLuminance(b); break; - case I_32_I0_UCS_4: (static_cast(busPtr))->SetLuminance(b); break; - case I_32_I0_APA106_3: (static_cast(busPtr))->SetLuminance(b); break; - case I_32_I0_FW6_5: (static_cast(busPtr))->SetLuminance(b); break; - case I_32_I0_2805_5: (static_cast(busPtr))->SetLuminance(b); break; - case I_32_I0_TM1914_3: (static_cast(busPtr))->SetLuminance(b); break; - case I_32_I0_SM16825_5: (static_cast(busPtr))->SetLuminance(b); break; + #ifndef CONFIG_IDF_TARGET_ESP32C3 + case I_32_I2_NEO_3: if (_useParallelI2S) (static_cast(busPtr))->SetLuminance(b); else (static_cast(busPtr))->SetLuminance(b); break; + case I_32_I2_NEO_4: if (_useParallelI2S) (static_cast(busPtr))->SetLuminance(b); else (static_cast(busPtr))->SetLuminance(b); break; + case I_32_I2_400_3: if (_useParallelI2S) (static_cast(busPtr))->SetLuminance(b); else (static_cast(busPtr))->SetLuminance(b); break; + case I_32_I2_TM1_4: if (_useParallelI2S) (static_cast(busPtr))->SetLuminance(b); else (static_cast(busPtr))->SetLuminance(b); break; + case I_32_I2_TM2_3: if (_useParallelI2S) (static_cast(busPtr))->SetLuminance(b); else (static_cast(busPtr))->SetLuminance(b); break; + case I_32_I2_UCS_3: if (_useParallelI2S) (static_cast(busPtr))->SetLuminance(b); else (static_cast(busPtr))->SetLuminance(b); break; + case I_32_I2_UCS_4: if (_useParallelI2S) (static_cast(busPtr))->SetLuminance(b); else (static_cast(busPtr))->SetLuminance(b); break; + case I_32_I2_APA106_3: if (_useParallelI2S) (static_cast(busPtr))->SetLuminance(b); else (static_cast(busPtr))->SetLuminance(b); break; + case I_32_I2_FW6_5: if (_useParallelI2S) (static_cast(busPtr))->SetLuminance(b); else (static_cast(busPtr))->SetLuminance(b); break; + case I_32_I2_2805_5: if (_useParallelI2S) (static_cast(busPtr))->SetLuminance(b); else (static_cast(busPtr))->SetLuminance(b); break; + case I_32_I2_TM1914_3: if (_useParallelI2S) (static_cast(busPtr))->SetLuminance(b); else (static_cast(busPtr))->SetLuminance(b); break; + case I_32_I2_SM16825_5: if (_useParallelI2S) (static_cast(busPtr))->SetLuminance(b); else (static_cast(busPtr))->SetLuminance(b); break; #endif #endif case I_HS_DOT_3: (static_cast(busPtr))->SetLuminance(b); break; @@ -1070,7 +984,7 @@ class PolyBus { } } - static uint32_t getPixelColor(void* busPtr, uint8_t busType, uint16_t pix, uint8_t co) { + [[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; @@ -1139,34 +1053,19 @@ class PolyBus { 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 WLED_NO_I2S1_PIXELBUS - case I_32_I1_NEO_3: col = (useParallelI2S) ? (static_cast(busPtr))->GetPixelColor(pix) : (static_cast(busPtr))->GetPixelColor(pix); break; - case I_32_I1_NEO_4: col = (useParallelI2S) ? (static_cast(busPtr))->GetPixelColor(pix) : (static_cast(busPtr))->GetPixelColor(pix); break; - case I_32_I1_400_3: col = (useParallelI2S) ? (static_cast(busPtr))->GetPixelColor(pix) : (static_cast(busPtr))->GetPixelColor(pix); break; - case I_32_I1_TM1_4: col = (useParallelI2S) ? (static_cast(busPtr))->GetPixelColor(pix) : (static_cast(busPtr))->GetPixelColor(pix); break; - case I_32_I1_TM2_3: col = (useParallelI2S) ? (static_cast(busPtr))->GetPixelColor(pix) : (static_cast(busPtr))->GetPixelColor(pix); break; - case I_32_I1_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_I1_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_I1_APA106_3: col = (useParallelI2S) ? (static_cast(busPtr))->GetPixelColor(pix) : (static_cast(busPtr))->GetPixelColor(pix); break; - case I_32_I1_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_I1_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_I1_TM1914_3: col = (useParallelI2S) ? (static_cast(busPtr))->GetPixelColor(pix) : (static_cast(busPtr))->GetPixelColor(pix); break; - case I_32_I1_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 - // I2S0 bus - #ifndef WLED_NO_I2S0_PIXELBUS - case I_32_I0_NEO_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; - case I_32_I0_NEO_4: col = (static_cast(busPtr))->GetPixelColor(pix); break; - case I_32_I0_400_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; - case I_32_I0_TM1_4: col = (static_cast(busPtr))->GetPixelColor(pix); break; - case I_32_I0_TM2_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; - case I_32_I0_UCS_3: { Rgb48Color c = (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R/257,c.G/257,c.B/257,0); } break; - case I_32_I0_UCS_4: { Rgbw64Color c = (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R/257,c.G/257,c.B/257,c.W/257); } break; - case I_32_I0_APA106_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; - case I_32_I0_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_I0_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_I0_TM1914_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; - case I_32_I0_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 + #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; @@ -1269,34 +1168,19 @@ class PolyBus { 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 WLED_NO_I2S1_PIXELBUS - case I_32_I1_NEO_3: if (useParallelI2S) delete (static_cast(busPtr)); else delete (static_cast(busPtr)); break; - case I_32_I1_NEO_4: if (useParallelI2S) delete (static_cast(busPtr)); else delete (static_cast(busPtr)); break; - case I_32_I1_400_3: if (useParallelI2S) delete (static_cast(busPtr)); else delete (static_cast(busPtr)); break; - case I_32_I1_TM1_4: if (useParallelI2S) delete (static_cast(busPtr)); else delete (static_cast(busPtr)); break; - case I_32_I1_TM2_3: if (useParallelI2S) delete (static_cast(busPtr)); else delete (static_cast(busPtr)); break; - case I_32_I1_UCS_3: if (useParallelI2S) delete (static_cast(busPtr)); else delete (static_cast(busPtr)); break; - case I_32_I1_UCS_4: if (useParallelI2S) delete (static_cast(busPtr)); else delete (static_cast(busPtr)); break; - case I_32_I1_APA106_3: if (useParallelI2S) delete (static_cast(busPtr)); else delete (static_cast(busPtr)); break; - case I_32_I1_FW6_5: if (useParallelI2S) delete (static_cast(busPtr)); else delete (static_cast(busPtr)); break; - case I_32_I1_2805_5: if (useParallelI2S) delete (static_cast(busPtr)); else delete (static_cast(busPtr)); break; - case I_32_I1_TM1914_3: if (useParallelI2S) delete (static_cast(busPtr)); else delete (static_cast(busPtr)); break; - case I_32_I1_SM16825_5: if (useParallelI2S) delete (static_cast(busPtr)); else delete (static_cast(busPtr)); break; - #endif - // I2S0 bus - #ifndef WLED_NO_I2S0_PIXELBUS - case I_32_I0_NEO_3: delete (static_cast(busPtr)); break; - case I_32_I0_NEO_4: delete (static_cast(busPtr)); break; - case I_32_I0_400_3: delete (static_cast(busPtr)); break; - case I_32_I0_TM1_4: delete (static_cast(busPtr)); break; - case I_32_I0_TM2_3: delete (static_cast(busPtr)); break; - case I_32_I0_UCS_3: delete (static_cast(busPtr)); break; - case I_32_I0_UCS_4: delete (static_cast(busPtr)); break; - case I_32_I0_APA106_3: delete (static_cast(busPtr)); break; - case I_32_I0_FW6_5: delete (static_cast(busPtr)); break; - case I_32_I0_2805_5: delete (static_cast(busPtr)); break; - case I_32_I0_TM1914_3: delete (static_cast(busPtr)); break; - case I_32_I0_SM16825_5: delete (static_cast(busPtr)); break; + #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; @@ -1312,8 +1196,178 @@ class PolyBus { } } + static unsigned getDataSize(void* busPtr, uint8_t busType) { + unsigned size = 0; + switch (busType) { + case I_NONE: break; + #ifdef ESP8266 + case I_8266_U0_NEO_3: size = (static_cast(busPtr))->PixelsSize()*2; break; + case I_8266_U1_NEO_3: size = (static_cast(busPtr))->PixelsSize()*2; 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()*2; break; + case I_8266_U0_NEO_4: size = (static_cast(busPtr))->PixelsSize()*2; break; + case I_8266_U1_NEO_4: size = (static_cast(busPtr))->PixelsSize()*2; 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()*2; break; + case I_8266_U0_400_3: size = (static_cast(busPtr))->PixelsSize()*2; break; + case I_8266_U1_400_3: size = (static_cast(busPtr))->PixelsSize()*2; 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()*2; break; + case I_8266_U0_TM1_4: size = (static_cast(busPtr))->PixelsSize()*2; break; + case I_8266_U1_TM1_4: size = (static_cast(busPtr))->PixelsSize()*2; 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()*2; break; + case I_8266_U0_TM2_3: size = (static_cast(busPtr))->PixelsSize()*2; break; + case I_8266_U1_TM2_3: size = (static_cast(busPtr))->PixelsSize()*2; 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()*2; break; + case I_8266_U0_UCS_3: size = (static_cast(busPtr))->PixelsSize()*2; break; + case I_8266_U1_UCS_3: size = (static_cast(busPtr))->PixelsSize()*2; 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()*2; break; + case I_8266_U0_UCS_4: size = (static_cast(busPtr))->PixelsSize()*2; break; + case I_8266_U1_UCS_4: size = (static_cast(busPtr))->PixelsSize()*2; 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()*2; break; + case I_8266_U0_APA106_3: size = (static_cast(busPtr))->PixelsSize()*2; break; + case I_8266_U1_APA106_3: size = (static_cast(busPtr))->PixelsSize()*2; 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()*2; break; + case I_8266_U0_FW6_5: size = (static_cast(busPtr))->PixelsSize()*2; break; + case I_8266_U1_FW6_5: size = (static_cast(busPtr))->PixelsSize()*2; 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()*2; break; + case I_8266_U0_2805_5: size = (static_cast(busPtr))->PixelsSize()*2; break; + case I_8266_U1_2805_5: size = (static_cast(busPtr))->PixelsSize()*2; 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()*2; break; + case I_8266_U0_TM1914_3: size = (static_cast(busPtr))->PixelsSize()*2; break; + case I_8266_U1_TM1914_3: size = (static_cast(busPtr))->PixelsSize()*2; 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()*2; break; + case I_8266_U0_SM16825_5: size = (static_cast(busPtr))->PixelsSize()*2; break; + case I_8266_U1_SM16825_5: size = (static_cast(busPtr))->PixelsSize()*2; 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()*2; 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) + #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: size = (size + count)*2; break; // 4 channels + case I_8266_U1_NEO_4: size = (size + count)*2; break; // 4 channels + case I_8266_BB_NEO_4: size = (size + count)*2; break; // 4 channels + case I_8266_U0_TM1_4: size = (size + count)*2; break; // 4 channels + case I_8266_U1_TM1_4: size = (size + count)*2; break; // 4 channels + case I_8266_BB_TM1_4: size = (size + count)*2; break; // 4 channels + case I_8266_U0_UCS_3: size *= 4; break; // 16 bit + case I_8266_U1_UCS_3: size *= 4; break; // 16 bit + case I_8266_BB_UCS_3: size *= 4; break; // 16 bit + case I_8266_U0_UCS_4: size = (size + count)*2*2; break; // 16 bit 4 channels + case I_8266_U1_UCS_4: size = (size + count)*2*2; break; // 16 bit 4 channels + case I_8266_BB_UCS_4: size = (size + count)*2*2; break; // 16 bit 4 channels + case I_8266_U0_FW6_5: size = (size + 2*count)*2; break; // 5 channels + case I_8266_U1_FW6_5: size = (size + 2*count)*2; break; // 5channels + case I_8266_BB_FW6_5: size = (size + 2*count)*2; break; // 5 channels + case I_8266_U0_2805_5: size = (size + 2*count)*2; break; // 5 channels + case I_8266_U1_2805_5: size = (size + 2*count)*2; break; // 5 channels + case I_8266_BB_2805_5: size = (size + 2*count)*2; break; // 5 channels + case I_8266_U0_SM16825_5: size = (size + 2*count)*2*2; break; // 16 bit 5 channels + case I_8266_U1_SM16825_5: size = (size + 2*count)*2*2; break; // 16 bit 5 channels + case I_8266_BB_SM16825_5: size = (size + 2*count)*2*2; break; // 16 bit 5 channels + // DMA methods have front + DMA buffer = ((1+(3+1)) * channels) + case I_8266_DM_NEO_3: size *= 5; break; + case I_8266_DM_NEO_4: size = (size + count)*5; break; + case I_8266_DM_400_3: size *= 5; break; + case I_8266_DM_TM1_4: size = (size + count)*5; break; + case I_8266_DM_TM2_3: size *= 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_APA106_3: size *= 5; break; + case I_8266_DM_FW6_5: size = (size + 2*count)*5; break; + case I_8266_DM_2805_5: size = (size + 2*count)*5; break; + case I_8266_DM_TM1914_3: size *= 5; break; + case I_8266_DM_SM16825_5: size = (size + 2*count)*2*5; break; + #endif + #ifdef ARDUINO_ARCH_ESP32 + // RMT buses (1x front and 1x back buffer) + case I_32_RN_NEO_4: size = (size + count)*2; break; + case I_32_RN_TM1_4: size = (size + count)*2; break; + case I_32_RN_UCS_3: size *= 2*2; break; + case I_32_RN_UCS_4: size = (size + count)*2*2; break; + case I_32_RN_FW6_5: size = (size + 2*count)*2; break; + case I_32_RN_2805_5: size = (size + 2*count)*2; break; + case I_32_RN_SM16825_5: size = (size + 2*count)*2*2; break; + // I2S1 bus or paralell buses (individual 1x front and 1 DMA (3x or 4x pixel count) or common back DMA buffers) + #ifndef CONFIG_IDF_TARGET_ESP32C3 + case I_32_I2_NEO_3: size *= 4; break; + case I_32_I2_NEO_4: size = (size + count)*4; break; + case I_32_I2_400_3: size *= 4; break; + case I_32_I2_TM1_4: size = (size + count)*4; break; + case I_32_I2_TM2_3: size *= 4; break; + case I_32_I2_UCS_3: size *= 2*4; break; + case I_32_I2_UCS_4: size = (size + count)*2*4; break; + case I_32_I2_APA106_3: size *= 4; break; + case I_32_I2_FW6_5: size = (size + 2*count)*4; break; + case I_32_I2_2805_5: size = (size + 2*count)*4; break; + case I_32_I2_TM1914_3: size *= 4; break; + case I_32_I2_SM16825_5: size = (size + 2*count)*2*4; break; + #endif + #endif + // everything else uses 2 buffers + default: size *= 2; break; + } + return size; + } + //gives back the internal type index (I_XX_XXX_X above) for the input - static uint8_t getI(uint8_t busType, uint8_t* pins, uint8_t num = 0) { + static uint8_t getI(uint8_t busType, const uint8_t* pins, uint8_t num = 0) { if (!Bus::isDigital(busType)) return I_NONE; if (Bus::is2Pin(busType)) { //SPI LED chips bool isHSPI = false; @@ -1372,26 +1426,33 @@ class PolyBus { uint8_t offset = 0; // 0 = RMT (num 1-8), 1 = I2S0 (used by Audioreactive), 2 = I2S1 #if defined(CONFIG_IDF_TARGET_ESP32S2) // ESP32-S2 only has 4 RMT channels - if (num > 4) return I_NONE; - if (num > 3) offset = 1; // only one I2S (use last to allow Audioreactive) + if (_useParallelI2S) { + if (num > 11) return I_NONE; + if (num > 3) offset = 1; // use x8 parallel I2S0 channels (use last to allow Audioreactive) + } else { + if (num > 4) return I_NONE; + if (num > 3) offset = 1; // only one I2S0 (use last to allow Audioreactive) + } #elif defined(CONFIG_IDF_TARGET_ESP32C3) // On ESP32-C3 only the first 2 RMT channels are usable for transmitting if (num > 1) return I_NONE; //if (num > 1) offset = 1; // I2S not supported yet (only 1 I2S) #elif defined(CONFIG_IDF_TARGET_ESP32S3) // On ESP32-S3 only the first 4 RMT channels are usable for transmitting - if (num > 3) return I_NONE; - //if (num > 3) offset = num -4; // I2S not supported yet + if (_useParallelI2S) { + if (num > 11) return I_NONE; + if (num > 3) offset = 1; // use x8 parallel I2S LCD channels + } else { + if (num > 3) return I_NONE; // do not use single I2S (as it is not supported) + } #else - // standard ESP32 has 8 RMT and 2 I2S channels - if (useParallelI2S) { - if (num > 16) return I_NONE; - if (num < 8) offset = 2; // prefer 8 parallel I2S1 channels - if (num == 16) offset = 1; + // standard ESP32 has 8 RMT and x1/x8 I2S1 channels + if (_useParallelI2S) { + if (num > 15) return I_NONE; + if (num > 7) offset = 1; // 8 RMT followed by 8 I2S } else { if (num > 9) return I_NONE; - if (num > 8) offset = 1; - if (num == 0) offset = 2; // prefer I2S1 for 1st bus (less flickering but more RAM needed) + if (num == 0) offset = 1; // prefer I2S1 for 1st bus (less flickering but more RAM needed) } #endif switch (busType) { diff --git a/wled00/button.cpp b/wled00/button.cpp index 5144f09f2..cf8fabe42 100644 --- a/wled00/button.cpp +++ b/wled00/button.cpp @@ -40,7 +40,7 @@ void longPressAction(uint8_t b) { if (!macroLongPress[b]) { switch (b) { - case 0: setRandomColor(col); colorUpdated(CALL_MODE_BUTTON); break; + case 0: setRandomColor(colPri); colorUpdated(CALL_MODE_BUTTON); break; case 1: if(buttonBriDirection) { if (bri == 255) break; // avoid unnecessary updates to brightness @@ -230,7 +230,7 @@ void handleAnalog(uint8_t b) effectPalette = constrain(effectPalette, 0, strip.getPaletteCount()-1); // map is allowed to "overshoot", so we need to contrain the result } else if (macroDoublePress[b] == 200) { // primary color, hue, full saturation - colorHStoRGB(aRead*256,255,col); + colorHStoRGB(aRead*256,255,colPri); } else { // otherwise use "double press" for segment selection Segment& seg = strip.getSegment(macroDoublePress[b]); diff --git a/wled00/cfg.cpp b/wled00/cfg.cpp index 00cfc60d7..fa0397fc6 100644 --- a/wled00/cfg.cpp +++ b/wled00/cfg.cpp @@ -20,11 +20,11 @@ bool deserializeConfig(JsonObject doc, bool fromFS) { //long vid = doc[F("vid")]; // 2010020 -#ifdef WLED_USE_ETHERNET +#if defined(ARDUINO_ARCH_ESP32) && defined(WLED_USE_ETHERNET) JsonObject ethernet = doc[F("eth")]; CJSON(ethernetType, ethernet["type"]); // NOTE: Ethernet configuration takes priority over other use of pins - WLED::instance().initEthernet(); + initEthernet(); #endif JsonObject id = doc["id"]; @@ -53,9 +53,11 @@ bool deserializeConfig(JsonObject doc, bool fromFS) { 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]); @@ -63,6 +65,7 @@ bool deserializeConfig(JsonObject doc, bool fromFS) { } 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; @@ -118,6 +121,9 @@ bool deserializeConfig(JsonObject doc, bool fromFS) { Bus::setCCTBlend(cctBlending); strip.setTargetFps(hw_led["fps"]); //NOP if 0, default 42 FPS CJSON(useGlobalLedBuffer, hw_led[F("ld")]); + #if defined(ARDUINO_ARCH_ESP32) && !defined(CONFIG_IDF_TARGET_ESP32C3) + CJSON(useParallelI2S, hw_led[F("prl")]); + #endif #ifndef WLED_DISABLE_2D // 2D Matrix Settings @@ -162,37 +168,9 @@ bool deserializeConfig(JsonObject doc, bool fromFS) { DEBUG_PRINTF_P(PSTR("Heap before buses: %d\n"), ESP.getFreeHeap()); int s = 0; // bus iterator if (fromFS) BusManager::removeAll(); // can't safely manipulate busses directly in network callback - unsigned mem = 0; - - // determine if it is sensible to use parallel I2S outputs on ESP32 (i.e. more than 5 outputs = 1 I2S + 4 RMT) - bool useParallel = false; - #if defined(ARDUINO_ARCH_ESP32) && !defined(ARDUINO_ARCH_ESP32S2) && !defined(ARDUINO_ARCH_ESP32S3) && !defined(ARDUINO_ARCH_ESP32C3) - unsigned digitalCount = 0; - unsigned maxLedsOnBus = 0; - unsigned maxChannels = 0; - for (JsonObject elm : ins) { - unsigned type = elm["type"] | TYPE_WS2812_RGB; - unsigned len = elm["len"] | DEFAULT_LED_COUNT; - if (!Bus::isDigital(type)) continue; - if (!Bus::is2Pin(type)) { - digitalCount++; - unsigned channels = Bus::getNumberOfChannels(type); - if (len > maxLedsOnBus) maxLedsOnBus = len; - if (channels > maxChannels) maxChannels = channels; - } - } - DEBUG_PRINTF_P(PSTR("Maximum LEDs on a bus: %u\nDigital buses: %u\n"), maxLedsOnBus, digitalCount); - // we may remove 300 LEDs per bus limit when NeoPixelBus is updated beyond 2.9.0 - if (maxLedsOnBus <= 300 && digitalCount > 5) { - DEBUG_PRINTLN(F("Switching to parallel I2S.")); - useParallel = true; - BusManager::useParallelOutput(); - mem = BusManager::memUsage(maxChannels, maxLedsOnBus, 8); // use alternate memory calculation - } - #endif for (JsonObject elm : ins) { - if (s >= WLED_MAX_BUSSES+WLED_MIN_VIRTUAL_BUSSES) break; + if (s >= WLED_MAX_BUSSES) break; uint8_t pins[5] = {255, 255, 255, 255, 255}; JsonArray pinArr = elm["pin"]; if (pinArr.size() == 0) continue; @@ -220,26 +198,14 @@ bool deserializeConfig(JsonObject doc, bool fromFS) { maMax = 0; } ledType |= refresh << 7; // hack bit 7 to indicate strip requires off refresh - if (fromFS) { - BusConfig bc = BusConfig(ledType, pins, start, length, colorOrder, reversed, skipFirst, AWmode, freqkHz, useGlobalLedBuffer, maPerLed, maMax); - if (useParallel && s < 8) { - // if for some unexplained reason the above pre-calculation was wrong, update - unsigned memT = BusManager::memUsage(bc); // includes x8 memory allocation for parallel I2S - if (memT > mem) mem = memT; // if we have unequal LED count use the largest - } else - mem += BusManager::memUsage(bc); // includes global buffer - if (mem <= MAX_LED_MEMORY) if (BusManager::add(bc) == -1) break; // finalization will be done in WLED::beginStrip() - } else { - if (busConfigs[s] != nullptr) delete busConfigs[s]; - busConfigs[s] = new BusConfig(ledType, pins, start, length, colorOrder, reversed, skipFirst, AWmode, freqkHz, useGlobalLedBuffer, maPerLed, maMax); - doInitBusses = true; // finalization done in beginStrip() - } - s++; + + //busConfigs.push_back(std::move(BusConfig(ledType, pins, start, length, colorOrder, reversed, skipFirst, AWmode, freqkHz, useGlobalLedBuffer, maPerLed, maMax))); + busConfigs.emplace_back(ledType, pins, start, length, colorOrder, reversed, skipFirst, AWmode, freqkHz, useGlobalLedBuffer, maPerLed, maMax); + doInitBusses = true; // finalization done in beginStrip() + if (!Bus::isVirtual(ledType)) s++; // have as many virtual buses as you want } - DEBUG_PRINTF_P(PSTR("LED buffer size: %uB\n"), mem); - DEBUG_PRINTF_P(PSTR("Heap after buses: %d\n"), ESP.getFreeHeap()); } - if (hw_led["rev"]) BusManager::getBus(0)->setReversed(true); //set 0.11 global reversed setting for first bus + 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")]; @@ -705,10 +671,10 @@ void deserializeConfigFromFS() { // call readFromConfig() with an empty object so that usermods can initialize to defaults prior to saving JsonObject empty = JsonObject(); UsermodManager::readFromConfig(empty); - serializeConfig(); + serializeConfigToFS(); // init Ethernet (in case default type is set at compile time) - #ifdef WLED_USE_ETHERNET - WLED::instance().initEthernet(); + #if defined(ARDUINO_ARCH_ESP32) && defined(WLED_USE_ETHERNET) + initEthernet(); #endif return; } @@ -719,10 +685,10 @@ void deserializeConfigFromFS() { bool needsSave = deserializeConfig(root, true); releaseJSONBufferLock(); - if (needsSave) serializeConfig(); // usermods required new parameters + if (needsSave) serializeConfigToFS(); // usermods required new parameters } -void serializeConfig() { +void serializeConfigToFS() { serializeConfigSec(); DEBUG_PRINTLN(F("Writing settings to /cfg.json...")); @@ -731,6 +697,17 @@ void serializeConfig() { 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 @@ -756,6 +733,9 @@ void serializeConfig() { 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"); @@ -791,7 +771,7 @@ void serializeConfig() { wifi[F("txpwr")] = txPower; #endif -#ifdef WLED_USE_ETHERNET +#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) { @@ -828,6 +808,9 @@ void serializeConfig() { hw_led["fps"] = strip.getTargetFps(); hw_led[F("rgbwm")] = Bus::getGlobalAWMode(); // global auto white mode override hw_led[F("ld")] = useGlobalLedBuffer; + #if defined(ARDUINO_ARCH_ESP32) && !defined(CONFIG_IDF_TARGET_ESP32C3) + hw_led[F("prl")] = BusManager::hasParallelOutput(); + #endif #ifndef WLED_DISABLE_2D // 2D Matrix Settings @@ -852,32 +835,42 @@ void serializeConfig() { JsonArray hw_led_ins = hw_led.createNestedArray("ins"); for (size_t s = 0; s < BusManager::getNumBusses(); s++) { - Bus *bus = BusManager::getBus(s); - if (!bus || bus->getLength()==0) break; + DEBUG_PRINTF_P(PSTR("Cfg: Saving bus #%u\n"), s); + const Bus *bus = BusManager::getBus(s); + if (!bus || !bus->isOk()) break; + 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(); + 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("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("ledma")] = bus->getLEDCurrent(); } 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) break; - + if (!entry || !entry->len) break; JsonObject co = hw_com.createNestedObject(); co["start"] = entry->start; co["len"] = entry->len; @@ -1129,13 +1122,6 @@ void serializeConfig() { JsonObject usermods_settings = root.createNestedObject("um"); UsermodManager::addToConfig(usermods_settings); - - File f = WLED_FS.open(FPSTR(s_cfg_json), "w"); - if (f) serializeJson(root, f); - f.close(); - releaseJSONBufferLock(); - - doSerializeConfig = false; } @@ -1234,4 +1220,4 @@ void serializeConfigSec() { if (f) serializeJson(root, f); f.close(); releaseJSONBufferLock(); -} +} \ No newline at end of file diff --git a/wled00/colors.cpp b/wled00/colors.cpp index f154a1aea..ba38d5a15 100644 --- a/wled00/colors.cpp +++ b/wled00/colors.cpp @@ -10,26 +10,28 @@ */ uint32_t 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 - uint32_t rb1 = color1 & 0x00FF00FF; - uint32_t wg1 = (color1>>8) & 0x00FF00FF; - uint32_t rb2 = color2 & 0x00FF00FF; - uint32_t wg2 = (color2>>8) & 0x00FF00FF; - uint32_t rb3 = ((((rb1 << 8) | rb2) + (rb2 * blend) - (rb1 * blend)) >> 8) & 0x00FF00FF; - uint32_t wg3 = ((((wg1 << 8) | wg2) + (wg2 * blend) - (wg1 * blend))) & 0xFF00FF00; + 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/Aircoookie/WLED/pull/2465 by https://github.com/Proto-molecule + * original idea: https://github.com/wled-dev/WLED/pull/2465 by https://github.com/Proto-molecule * speed optimisations by @dedehai */ uint32_t color_add(uint32_t c1, uint32_t c2, bool preserveCR) { if (c1 == BLACK) return c2; if (c2 == BLACK) return c1; - uint32_t rb = (c1 & 0x00FF00FF) + (c2 & 0x00FF00FF); // mask and add two colors at once - uint32_t wg = ((c1>>8) & 0x00FF00FF) + ((c2>>8) & 0x00FF00FF); + 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); uint32_t r = rb >> 16; // extract single color values uint32_t b = rb & 0xFFFF; uint32_t w = wg >> 16; @@ -44,10 +46,10 @@ uint32_t color_add(uint32_t c1, uint32_t c2, bool preserveCR) //max = b > max ? b : max; //max = w > max ? w : max; if (max > 255) { - 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) & 0x00FF00FF; // - wg = (wg * scale) & 0xFF00FF00; - } else wg = wg << 8; //shift white and green back to correct position + 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 return rb | wg; } else { r = r > 255 ? 255 : r; @@ -77,8 +79,9 @@ uint32_t color_fade(uint32_t c1, uint8_t amount, bool video) addRemains |= B(c1) ? 0x00000001 : 0; addRemains |= W(c1) ? 0x01000000 : 0; } - uint32_t rb = (((c1 & 0x00FF00FF) * scale) >> 8) & 0x00FF00FF; // scale red and blue - uint32_t wg = (((c1 & 0xFF00FF00) >> 8) * scale) & 0xFF00FF00; // scale white and green + const uint32_t TWO_CHANNEL_MASK = 0x00FF00FF; + uint32_t rb = (((c1 & TWO_CHANNEL_MASK) * scale) >> 8) & TWO_CHANNEL_MASK; // scale red and blue + uint32_t wg = (((c1 >> 8) & TWO_CHANNEL_MASK) * scale) & ~TWO_CHANNEL_MASK; // scale white and green scaledcolor = (rb | wg) + addRemains; return scaledcolor; } @@ -87,25 +90,27 @@ uint32_t color_fade(uint32_t c1, uint8_t amount, bool video) uint32_t ColorFromPaletteWLED(const CRGBPalette16& pal, unsigned index, uint8_t brightness, TBlendType blendType) { if (blendType == LINEARBLEND_NOWRAP) { - index = (index*240) >> 8; // Blend range is affected by lo4 blend of values, remap to avoid wrapping + index = (index * 0xF0) >> 8; // Blend range is affected by lo4 blend of values, remap to avoid wrapping } unsigned hi4 = byte(index) >> 4; - const CRGB* entry = (CRGB*)((uint8_t*)(&(pal[0])) + (hi4 * sizeof(CRGB))); + unsigned lo4 = (index & 0x0F); + const CRGB* entry = (CRGB*)&(pal[0]) + hi4; unsigned red1 = entry->r; unsigned green1 = entry->g; unsigned blue1 = entry->b; - if (blendType != NOBLEND) { + if (lo4 && blendType != NOBLEND) { if (hi4 == 15) entry = &(pal[0]); else ++entry; - unsigned f2 = ((index & 0x0F) << 4) + 1; // +1 so we scale by 256 as a max value, then result can just be shifted by 8 - unsigned f1 = (257 - f2); // f2 is 1 minimum, so this is 256 max - red1 = (red1 * f1 + (unsigned)entry->r * f2) >> 8; + unsigned f2 = (lo4 << 4); + unsigned f1 = 256 - f2; + red1 = (red1 * f1 + (unsigned)entry->r * f2) >> 8; // note: using color_blend() is 20% 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 color_fade(c1, brightness) uint32_t scale = brightness + 1; // adjust for rounding (bitshift) - red1 = (red1 * scale) >> 8; + red1 = (red1 * scale) >> 8; // note: using color_fade() is 30% slower green1 = (green1 * scale) >> 8; blue1 = (blue1 * scale) >> 8; } diff --git a/wled00/const.h b/wled00/const.h index 1ebcb9397..2b460f3f1 100644 --- a/wled00/const.h +++ b/wled00/const.h @@ -37,7 +37,7 @@ #endif #ifndef WLED_MAX_USERMODS - #ifdef ESP8266 + #if defined(ESP8266) || defined(CONFIG_IDF_TARGET_ESP32S2) #define WLED_MAX_USERMODS 4 #else #define WLED_MAX_USERMODS 6 @@ -49,31 +49,31 @@ #define WLED_MAX_DIGITAL_CHANNELS 3 #define WLED_MAX_ANALOG_CHANNELS 5 #define WLED_MAX_BUSSES 4 // will allow 3 digital & 1 analog RGB - #define WLED_MIN_VIRTUAL_BUSSES 2 + #define WLED_MIN_VIRTUAL_BUSSES 3 // no longer used for bus creation but used to distinguish S2/S3 in UI #else #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_BUSSES 6 // will allow 2 digital & 2 analog RGB or 6 PWM white #define WLED_MAX_DIGITAL_CHANNELS 2 //#define WLED_MAX_ANALOG_CHANNELS 6 - #define WLED_MIN_VIRTUAL_BUSSES 3 + #define WLED_MIN_VIRTUAL_BUSSES 4 // no longer used for bus creation but used to distinguish S2/S3 in UI #elif defined(CONFIG_IDF_TARGET_ESP32S2) // 4 RMT, 8 LEDC, only has 1 I2S bus, supported in NPB // the 5th bus (I2S) will prevent Audioreactive usermod from functioning (it is last used though) #define WLED_MAX_BUSSES 7 // will allow 5 digital & 2 analog RGB #define WLED_MAX_DIGITAL_CHANNELS 5 //#define WLED_MAX_ANALOG_CHANNELS 8 - #define WLED_MIN_VIRTUAL_BUSSES 3 - #elif defined(CONFIG_IDF_TARGET_ESP32S3) // 4 RMT, 8 LEDC, has 2 I2S but NPB does not support them ATM - #define WLED_MAX_BUSSES 6 // will allow 4 digital & 2 analog RGB - #define WLED_MAX_DIGITAL_CHANNELS 4 + #define WLED_MIN_VIRTUAL_BUSSES 4 // no longer used for bus creation but used to distinguish S2/S3 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_BUSSES 14 // will allow 12 digital & 2 analog RGB + #define WLED_MAX_DIGITAL_CHANNELS 12 // x4 RMT + x8 I2S-LCD //#define WLED_MAX_ANALOG_CHANNELS 8 - #define WLED_MIN_VIRTUAL_BUSSES 4 + #define WLED_MIN_VIRTUAL_BUSSES 6 // no longer used for bus creation but used to distinguish S2/S3 in UI #else // the last digital bus (I2S0) will prevent Audioreactive usermod from functioning - #define WLED_MAX_BUSSES 20 // will allow 17 digital & 3 analog RGB - #define WLED_MAX_DIGITAL_CHANNELS 17 + #define WLED_MAX_BUSSES 19 // will allow 16 digital & 3 analog RGB + #define WLED_MAX_DIGITAL_CHANNELS 16 // x1/x8 I2S1 + x8 RMT //#define WLED_MAX_ANALOG_CHANNELS 16 - #define WLED_MIN_VIRTUAL_BUSSES 4 + #define WLED_MIN_VIRTUAL_BUSSES 6 // no longer used for bus creation but used to distinguish S2/S3 in UI #endif #endif #else @@ -87,7 +87,7 @@ #ifndef WLED_MAX_DIGITAL_CHANNELS #error You must also define WLED_MAX_DIGITAL_CHANNELS. #endif - #define WLED_MIN_VIRTUAL_BUSSES (5-WLED_MAX_BUSSES) + #define WLED_MIN_VIRTUAL_BUSSES 3 #else #if WLED_MAX_BUSSES > 20 #error Maximum number of buses is 20. @@ -98,7 +98,11 @@ #ifndef WLED_MAX_DIGITAL_CHANNELS #error You must also define WLED_MAX_DIGITAL_CHANNELS. #endif - #define WLED_MIN_VIRTUAL_BUSSES (20-WLED_MAX_BUSSES) + #if defined(CONFIG_IDF_TARGET_ESP32S2) || defined(CONFIG_IDF_TARGET_ESP32C3) + #define WLED_MIN_VIRTUAL_BUSSES 4 + #else + #define WLED_MIN_VIRTUAL_BUSSES 6 + #endif #endif #endif @@ -115,7 +119,7 @@ #endif #endif -#ifdef ESP8266 +#if defined(ESP8266) || defined(CONFIG_IDF_TARGET_ESP32S2) #define WLED_MAX_COLOR_ORDER_MAPPINGS 5 #else #define WLED_MAX_COLOR_ORDER_MAPPINGS 10 @@ -125,7 +129,7 @@ #undef WLED_MAX_LEDMAPS #endif #ifndef WLED_MAX_LEDMAPS - #ifdef ESP8266 + #if defined(ESP8266) || defined(CONFIG_IDF_TARGET_ESP32S2) #define WLED_MAX_LEDMAPS 10 #else #define WLED_MAX_LEDMAPS 16 @@ -205,6 +209,7 @@ #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" //Access point behavior #define AP_BEHAVIOR_BOOT_NO_CONN 0 //Open AP when no connection after boot @@ -476,6 +481,8 @@ #ifndef MAX_LEDS #ifdef ESP8266 #define MAX_LEDS 1664 //can't rely on memory limit to limit this to 1600 LEDs +#elif defined(CONFIG_IDF_TARGET_ESP32S2) +#define MAX_LEDS 2048 //due to memory constraints #else #define MAX_LEDS 8192 #endif @@ -485,7 +492,9 @@ #ifdef ESP8266 #define MAX_LED_MEMORY 4000 #else - #if defined(ARDUINO_ARCH_ESP32S2) || defined(ARDUINO_ARCH_ESP32C3) + #if defined(ARDUINO_ARCH_ESP32S2) + #define MAX_LED_MEMORY 16000 + #elif defined(ARDUINO_ARCH_ESP32C3) #define MAX_LED_MEMORY 32000 #else #define MAX_LED_MEMORY 64000 diff --git a/wled00/data/common.js b/wled00/data/common.js index 9378ef07a..658346e75 100644 --- a/wled00/data/common.js +++ b/wled00/data/common.js @@ -2,7 +2,7 @@ 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/Aircoookie/WLED"); } +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 @@ -16,7 +16,7 @@ function isI(n) { return n === +n && n === (n|0); } // isInteger function toggle(el) { gId(el).classList.toggle("hide"); gId('No'+el).classList.toggle("hide"); } function tooltip(cont=null) { d.querySelectorAll((cont?cont+" ":"")+"[title]").forEach((element)=>{ - element.addEventListener("mouseover", ()=>{ + element.addEventListener("pointerover", ()=>{ // save title element.setAttribute("data-title", element.getAttribute("title")); const tooltip = d.createElement("span"); @@ -41,7 +41,7 @@ function tooltip(cont=null) { tooltip.classList.add("visible"); }); - element.addEventListener("mouseout", ()=>{ + element.addEventListener("pointerout", ()=>{ d.querySelectorAll('.tooltip').forEach((tooltip)=>{ tooltip.classList.remove("visible"); d.body.removeChild(tooltip); diff --git a/wled00/data/index.htm b/wled00/data/index.htm index aa06b5122..c55c98373 100644 --- a/wled00/data/index.htm +++ b/wled00/data/index.htm @@ -6,6 +6,7 @@ + WLED @@ -362,7 +363,7 @@ diff --git a/wled00/data/index.js b/wled00/data/index.js index dc94cf426..147abf389 100644 --- a/wled00/data/index.js +++ b/wled00/data/index.js @@ -409,7 +409,9 @@ function pName(i) function isPlaylist(i) { - return pJson[i].playlist && pJson[i].playlist.ps; + 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) @@ -775,8 +777,8 @@ function populateSegments(s) } let segp = `
`+ - ``+ - `
`+ + ``+ + `
`+ ``+ `
`+ `
`+ @@ -807,12 +809,14 @@ function populateSegments(s) `
`+ `
`; cn += `
`+ ``+ `
`+ `&#x${inst.frz ? (li.live && li.liveseg==i?'e410':'e0e8') : 'e325'};`+ @@ -1416,7 +1420,7 @@ function makeWS() { ws = null; } ws.onopen = (e)=>{ - //ws.send("{'v':true}"); // unnecessary (https://github.com/Aircoookie/WLED/blob/master/wled00/ws.cpp#L18) + //ws.send("{'v':true}"); // unnecessary (https://github.com/wled-dev/WLED/blob/main/wled00/ws.cpp#L18) wsRpt = 0; reqsLegal = true; } @@ -1664,13 +1668,17 @@ function setEffectParameters(idx) paOnOff[0] = paOnOff[0].substring(0,dPos); } if (paOnOff.length>0 && paOnOff[0] != "!") text = paOnOff[0]; + gId("adPal").classList.remove("hide"); + if (lastinfo.cpalcount>0) gId("rmPal").classList.remove("hide"); } else { // disable palette list text += ' not used'; palw.style.display = "none"; + gId("adPal").classList.add("hide"); + gId("rmPal").classList.add("hide"); // Close palette dialog if not available - if (gId("palw").lastElementChild.tagName == "DIALOG") { - gId("palw").lastElementChild.close(); + if (palw.lastElementChild.tagName == "DIALOG") { + palw.lastElementChild.close(); } } pall.innerHTML = icon + text; @@ -1885,7 +1893,7 @@ function makeSeg() function resetUtil(off=false) { gId('segutil').innerHTML = `
` - + '' + + '' + `
Add segment
` + '
' + `` @@ -1899,15 +1907,16 @@ function resetUtil(off=false) if (lSeg>2) d.querySelectorAll("#Segments .pop").forEach((e)=>{e.classList.remove("hide");}); } -function makePlSel(el, incPl=false) +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; - if (!(!incPl && a[1].playlist && a[1].playlist.ps)) // skip playlists, sub-playlists not yet supported - plSelContent += ``; + // skip endless playlists and itself + if (!isPlaylist(a[1]) || (a[1].playlist.repeat > 0 && a[0]!=p)) plSelContent += ``; }); return plSelContent; } @@ -1932,21 +1941,23 @@ function refreshPlE(p) }); } -// p: preset ID, i: ps index +// p: preset ID, i: playlist item index function addPl(p,i) { - plJson[p].ps.splice(i+1,0,0); - plJson[p].dur.splice(i+1,0,plJson[p].dur[i]); - plJson[p].transition.splice(i+1,0,plJson[p].transition[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) { - if (plJson[p].ps.length < 2) return; - plJson[p].ps.splice(i,1); - plJson[p].dur.splice(i,1); - plJson[p].transition.splice(i,1); + 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); } @@ -1963,6 +1974,13 @@ function pleDur(p,i,field) 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); } @@ -1982,6 +2000,17 @@ function plR(p) } } +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 = ""; @@ -1995,12 +2024,17 @@ function makeP(i,pl) r: false, end: 0 }; - var rep = plJson[i].repeat ? plJson[i].repeat : 0; + const rep = plJson[i].repeat ? plJson[i].repeat : 0; + const man = plJson[i].dur == 0; content = `
+
`; @@ -2084,25 +2118,26 @@ function makePUtil() function makePlEntry(p,i) { + const man = gId(`pl${p}manual`).checked; return `
- + - - + +
DurationDuration (0=inf.) Transition #${i+1}
ssss
@@ -2655,28 +2690,28 @@ function fromRgb() var g = gId('sliderG').value; var b = gId('sliderB').value; setPicker(`rgb(${r},${g},${b})`); - let cd = gId('csl').children; // color slots - cd[csel].dataset.r = r; - cd[csel].dataset.g = g; - cd[csel].dataset.b = b; - setCSL(cd[csel]); + 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; // color slots - cd[csel].dataset.w = w.value; - setCSL(cd[csel]); + 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; // color slots - let cdd = cd[csel].dataset; - let w = 0, r,g,b; + 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; @@ -2684,7 +2719,7 @@ function setColor(sr) cdd.g = g = hasRGB ? col.g : w; cdd.b = b = hasRGB ? col.b : w; cdd.w = w; - setCSL(cd[csel]); + setCSL(cd); var obj = {"seg": {"col": [[],[],[]]}}; obj.seg.col[csel] = [r, g, b, w]; requestJson(obj); @@ -2727,7 +2762,7 @@ setInterval(()=>{ gId('heart').style.color = `hsl(${hc}, 100%, 50%)`; }, 910); -function openGH() { window.open("https://github.com/Aircoookie/WLED/wiki"); } +function openGH() { window.open("https://github.com/wled-dev/WLED/wiki"); } var cnfr = false; function cnfReset() @@ -3120,10 +3155,9 @@ function mergeDeep(target, ...sources) return mergeDeep(target, ...sources); } -function tooltip(cont=null) -{ +function tooltip(cont=null) { d.querySelectorAll((cont?cont+" ":"")+"[title]").forEach((element)=>{ - element.addEventListener("mouseover", ()=>{ + element.addEventListener("pointerover", ()=>{ // save title element.setAttribute("data-title", element.getAttribute("title")); const tooltip = d.createElement("span"); @@ -3148,7 +3182,7 @@ function tooltip(cont=null) tooltip.classList.add("visible"); }); - element.addEventListener("mouseout", ()=>{ + element.addEventListener("pointerout", ()=>{ d.querySelectorAll('.tooltip').forEach((tooltip)=>{ tooltip.classList.remove("visible"); d.body.removeChild(tooltip); diff --git a/wled00/data/settings_dmx.htm b/wled00/data/settings_dmx.htm index e800be6ea..7458ec9fe 100644 --- a/wled00/data/settings_dmx.htm +++ b/wled00/data/settings_dmx.htm @@ -6,7 +6,7 @@ DMX Settings