├── .coveragerc ├── .env ├── .github ├── release-drafter.yml └── workflows │ └── nodejs.yml ├── .gitignore ├── .readthedocs.yml ├── .travis.yml ├── AUTHORS.rst ├── CHANGELOG.rst ├── LICENSE.txt ├── MANIFEST.in ├── README.rst ├── docs ├── Makefile ├── README.rst ├── _static │ ├── css │ │ └── custom.css │ ├── logo.png │ ├── small_black_alpha.png │ └── small_white_alpha.png ├── _templates │ └── breadcrumbs.html ├── api.rst ├── conf.py ├── configuring.rst ├── developer.rst ├── index.rst ├── installing.rst ├── make.bat ├── requirements_docs.txt └── trouble.rst ├── frontend ├── .gitignore ├── .prettierrc ├── README.md ├── eslintrc.js ├── package.json ├── public │ ├── __init__.py │ ├── favicon.ico │ ├── index.html │ ├── logo192.png │ ├── logo512.png │ └── robots.txt ├── src │ ├── App │ │ ├── createStore.js │ │ ├── index.jsx │ │ ├── style.css │ │ ├── theme.js │ │ └── useDarkMode.js │ ├── assets │ │ └── img │ │ │ └── icon │ │ │ ├── favicon.ico │ │ │ └── large_white_alpha.png │ ├── components │ │ ├── AddSceneCard │ │ │ └── index.jsx │ │ ├── DeviceConfigDialog │ │ │ ├── AdditionalProperties.jsx │ │ │ └── index.jsx │ │ ├── DeviceMiniControl │ │ │ └── index.jsx │ │ ├── DevicesTable │ │ │ ├── DevicesTableItem.jsx │ │ │ └── index.jsx │ │ ├── EffectControl │ │ │ └── index.jsx │ │ ├── MelbankGraph │ │ │ └── index.jsx │ │ ├── MiniScenesCard │ │ │ └── index.jsx │ │ ├── PixelColorGraph │ │ │ └── index.jsx │ │ ├── PresetsCard │ │ │ └── index.jsx │ │ ├── SceneCard │ │ │ ├── SceneConfigTable.jsx │ │ │ └── index.jsx │ │ ├── SceneConfigDialog │ │ │ └── index.jsx │ │ ├── SchemaForm │ │ │ ├── AdditionalProperties.jsx │ │ │ ├── customFields │ │ │ │ └── Slider.jsx │ │ │ ├── index.jsx │ │ │ ├── mapper.js │ │ │ └── utils.js │ │ └── forms │ │ │ └── DropDown │ │ │ └── index.jsx │ ├── index.js │ ├── layouts │ │ └── Default │ │ │ ├── Header │ │ │ └── index.jsx │ │ │ ├── Sidebar │ │ │ ├── index.jsx │ │ │ └── style.jsx │ │ │ └── index.jsx │ ├── modules │ │ ├── devices.js │ │ ├── index.js │ │ ├── presets.js │ │ ├── scenes.js │ │ ├── schemas.js │ │ ├── selectedDevice.js │ │ └── settings.js │ ├── proxies │ │ ├── device.js │ │ ├── effects.js │ │ ├── scenes.js │ │ ├── schema.js │ │ └── settings.js │ ├── react-app-env.d.ts │ ├── routes │ │ ├── index.jsx │ │ └── views.jsx │ ├── serviceWorker.js │ ├── setupTests.js │ ├── utils │ │ ├── api │ │ │ ├── index.js │ │ │ └── websocket.js │ │ ├── helpers.js │ │ └── style.js │ └── views │ │ ├── Dashboard │ │ └── index.jsx │ │ ├── Developer │ │ └── index.jsx │ │ ├── Device │ │ └── index.jsx │ │ ├── Devices │ │ └── index.jsx │ │ ├── Scenes │ │ └── index.jsx │ │ └── Settings │ │ ├── AudioInput.jsx │ │ ├── ConfigCard.jsx │ │ └── index.jsx ├── tsconfig.json └── yarn.lock ├── icons ├── demo_setup.png ├── discord.ico ├── discord.png ├── favicon.ico ├── insta.png ├── large_black_alpha.png └── large_white_alpha.png ├── ledfx ├── __init__.py ├── __main__.py ├── api │ ├── __init__.py │ ├── audio_devices.py │ ├── config.py │ ├── device.py │ ├── device_effects.py │ ├── device_presets.py │ ├── devices.py │ ├── effect.py │ ├── effects.py │ ├── find_devices.py │ ├── graphics_quality.py │ ├── info.py │ ├── presets.py │ ├── scenes.py │ ├── schema.py │ ├── schema_types.py │ ├── utils.py │ └── websocket.py ├── color.py ├── config.py ├── consts.py ├── core.py ├── default_presets.yaml ├── devices │ ├── FXMatrix.py │ ├── __init__.py │ ├── e131.py │ └── udp.py ├── effects │ ├── __init__.py │ ├── audio.py │ ├── bands(Reactive).py │ ├── bar(Reactive).py │ ├── blocks(Reactive).py │ ├── effectlets │ │ ├── __init__.py │ │ ├── droplet_0.npy │ │ ├── droplet_1.npy │ │ └── droplet_2.npy │ ├── energy(Reactive).py │ ├── equalizer(reactive).py │ ├── fade.py │ ├── gradient.py │ ├── magnitude(Reactive).py │ ├── math.py │ ├── mel.py │ ├── modulate.py │ ├── multiBar(Reactive).py │ ├── pitchSpectrum(Reactive).py │ ├── power(Reactive).py │ ├── rain(Reactive).py │ ├── rainbow.py │ ├── scroll(Reactive).py │ ├── singleColor.py │ ├── spectrum(Reactive).py │ ├── strobe(Reactive).py │ ├── temporal.py │ └── wavelength(Reactive).py ├── events.py ├── frontend │ └── components │ │ └── SceneCard │ │ └── SceneConfigTable.jsx ├── http.py └── utils.py ├── ledfx_frontend ├── __init__.py ├── asset-manifest.json ├── favicon.ico ├── index.html ├── logo192.png ├── logo512.png ├── precache-manifest.b20253c4725abce969d00ebfad32158c.js ├── robots.txt ├── service-worker.js └── static │ ├── css │ ├── main.c95fd1ee.chunk.css │ └── main.c95fd1ee.chunk.css.map │ └── js │ ├── 2.78a442b4.chunk.js │ ├── 2.78a442b4.chunk.js.LICENSE.txt │ ├── 2.78a442b4.chunk.js.map │ ├── main.f987f2e0.chunk.js │ ├── main.f987f2e0.chunk.js.map │ ├── runtime-main.5d286450.js │ └── runtime-main.5d286450.js.map ├── pyproject.toml ├── release.py ├── requirements.txt ├── setup.cfg ├── setup.py ├── win.spec └── yarn.lock /.coveragerc: -------------------------------------------------------------------------------- 1 | # .coveragerc to control coverage.py 2 | [run] 3 | branch = True 4 | source = */ledfx/* 5 | # omit = bad_file.py 6 | 7 | [report] 8 | # Regexes for lines to exclude from consideration 9 | exclude_lines = 10 | # Have to re-enable the standard pragma 11 | pragma: no cover 12 | 13 | # Don't complain about missing debug-only code: 14 | def __repr__ 15 | if self\.debug 16 | 17 | # Don't complain if tests don't hit defensive assertion code: 18 | raise AssertionError 19 | raise NotImplementedError 20 | 21 | # Don't complain if non-runnable code isn't run: 22 | if 0: 23 | if __name__ == .__main__.: 24 | -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | NODE_PATH=./frontend/ 2 | -------------------------------------------------------------------------------- /.github/release-drafter.yml: -------------------------------------------------------------------------------- 1 | template: | 2 | ## What's Changed 3 | 4 | $CHANGES -------------------------------------------------------------------------------- /.github/workflows/nodejs.yml: -------------------------------------------------------------------------------- 1 | name: Node CI 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | 8 | runs-on: ubuntu-latest 9 | 10 | strategy: 11 | matrix: 12 | node-version: [8.x, 10.x, 12.x] 13 | 14 | steps: 15 | - uses: actions/checkout@v1 16 | - name: Use Node.js ${{ matrix.node-version }} 17 | uses: actions/setup-node@v1 18 | with: 19 | node-version: ${{ matrix.node-version }} 20 | - name: npm install, build, and test 21 | run: | 22 | npm ci 23 | npm run build --if-present 24 | npm test 25 | env: 26 | CI: true 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Temporary and binary files 2 | *~ 3 | *.py[cod] 4 | *.so 5 | *.cfg 6 | !setup.cfg 7 | *.orig 8 | *.log 9 | *.pot 10 | __pycache__/* 11 | .cache/* 12 | .*.swp 13 | */.ipynb_checkpoints/* 14 | *.DS_Store 15 | 16 | # Python env 17 | env 18 | 19 | # NPM 20 | node_modules/ 21 | npm-debug.log 22 | package-lock.json 23 | 24 | # Project files 25 | .ropeproject 26 | .project 27 | .pydevproject 28 | .settings 29 | .idea 30 | .vscode 31 | *.code-workspace 32 | 33 | # Package files 34 | *.egg 35 | *.eggs/ 36 | .installed.cfg 37 | *.egg-info 38 | 39 | # Unittest and coverage 40 | htmlcov/* 41 | .coverage 42 | .tox 43 | junit.xml 44 | coverage.xml 45 | 46 | # Build and docs folder/files 47 | build/* 48 | dist/* 49 | sdist/* 50 | docs/api/* 51 | docs/_build/* 52 | cover/* 53 | MANIFEST 54 | build.bat 55 | ledfx.nsi 56 | __main__.spec 57 | LedFx Installer.exe 58 | 59 | 60 | # Keyfile and pyupdater files for signing pyupdater 61 | keypack.pyu 62 | .pyupdater/ 63 | pyu-data/* -------------------------------------------------------------------------------- /.readthedocs.yml: -------------------------------------------------------------------------------- 1 | # .readthedocs.yml 2 | # Read the Docs configuration file 3 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details 4 | 5 | # Required 6 | version: 2 7 | 8 | # Build documentation in the docs/ directory with Sphinx 9 | sphinx: 10 | configuration: docs/conf.py 11 | 12 | # Build documentation with MkDocs 13 | #mkdocs: 14 | # configuration: mkdocs.yml 15 | 16 | # Don't build any documents (htmlzip, pdf, epub) 17 | formats: [] 18 | 19 | # Set the version of Python and requirements required to build your docs 20 | python: 21 | version: 3.7 22 | install: 23 | - requirements: docs/requirements_docs.txt -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | # EXAMPLES CAN BE FOUND HERE 2 | # https://github.com/numpy/numpy/blob/master/.travis.yml 3 | 4 | _docs_job: &docs_job 5 | addons: 6 | apt: 7 | update: false 8 | packages: 9 | cache: false 10 | before_install: 11 | install: pip install -r docs/requirements_docs.txt 12 | script: 13 | - travis-sphinx build --nowarn --source=docs 14 | 15 | os: linux 16 | dist: xenial 17 | language: python 18 | 19 | # Travis allows these packages, additions can be requested 20 | # https://github.com/travis-ci/apt-package-safelist 21 | addons: 22 | apt: 23 | update: true # runs `apt-get update` before installs 24 | packages: 25 | - portaudio19-dev # Install portaudio (requirement for pyaudio) 26 | - libudev-dev # 27 | 28 | jobs: 29 | fast_finish: true 30 | include: 31 | - python: "3.7" 32 | name: "Docs" 33 | dist: xenial 34 | <<: *docs_job 35 | - python: "3.7" 36 | env: TOXENV=py37 37 | dist: xenial 38 | - python: "3.8" 39 | env: TOXENV=py38 40 | dist: xenial 41 | - python: "3.9-dev" 42 | env: TOXENV=py39 43 | dist: xenial 44 | if: branch = dev AND type = push 45 | allow_failures: 46 | - python: "3.9-dev" 47 | env: TOXENV=py39 48 | dist: xenial 49 | 50 | cache: 51 | directories: 52 | - $HOME/.cache/pip 53 | - "node_modules" 54 | 55 | before_install: 56 | - nvm install 10 # Use nvm to install Node.js v10.x 57 | - nvm use 10 # Use nvm to switch to Node.js v10.x 58 | 59 | install: 60 | - npm install -g yarn 61 | - pip install tox-travis 62 | - pip install tox-venv 63 | 64 | script: 65 | # Build the frontend first 66 | - cd frontend 67 | - yarn 68 | - yarn build 69 | - cd .. 70 | # Then, build the backend 71 | - pip install -e . 72 | # - travis_wait 10 tox -v --develop 73 | 74 | # after_success: 75 | # - travis-sphinx deploy 76 | -------------------------------------------------------------------------------- /AUTHORS.rst: -------------------------------------------------------------------------------- 1 | ============ 2 | Contributors 3 | ============ 4 | 5 | * Austin Hodges 6 | # Matthew Bowley 7 | # Matt Muller -------------------------------------------------------------------------------- /CHANGELOG.rst: -------------------------------------------------------------------------------- 1 | ========= 2 | Changelog 3 | ========= 4 | 5 | Version 0. 6 | =========== 7 | 8 | - Updating React and front end dependencies 9 | - separated JS code from PY code moved front end to top level folder 10 | - Removed webpack in favor of CRA for less complexity and faster dev work 11 | 12 | Version 0.2 13 | =========== 14 | 15 | - More effects and support for UDP devices 16 | - Frontend converted to react and more features added 17 | 18 | Version 0.1 19 | =========== 20 | 21 | - **Initial release with basic feature set!** 22 | - Added a framework for highly customizable effects and outputs 23 | - Added support for E1.31 devices 24 | - Added some basic effects and audio reaction ones 25 | 26 | Version 0.8 27 | =========== 28 | 29 | - **New Effects** 30 | - _BPM Based Effects:_ Bar, MultiBar, Strobe 31 | - _EQ-Like Effects:_ Blocks, Equalizer 32 | - _Other Effects:_ Power, Magnitude 33 | - **Scenes & Presets** 34 | - _Scenes:_ Save current effects of all devices as a _scene_, and reactivate any saved scene from the dashboard. 35 | - _Presets:_ Effects have presets, with different settings to demo how the effect can be configured. You can save your own effect configurations as a _custom preset_. 36 | - **Facelift.** 37 | - More polished interface. 38 | - New colour scheme. 39 | - More icons. 40 | - **Device Auto Discovery.** WLED devices on the network are automatically found at launch if no devices are present. Scan for WLED devices in Device Management. 41 | - **Improved Documentation.** better guides for installation across Win, Mac, and Linux 42 | 43 | Version 0.9 44 | =========== 45 | 46 | - **Sliders.** Effect configuration needs sliders, not numbers! What year is it? 47 | - **Polish.** Smooth transitions between effects. 48 | - **Automatic Updates.** Installable EXE will automatically update itself at launch if there's a new version available -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2018 Austin Hodges 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.rst 2 | include AUTHORS.rst 3 | include CHANGELOG.rst 4 | include LICENSE.txt 5 | include requirements.txt 6 | recursive-include ledfx *.npy 7 | recursive-include ledfx_frontend * 8 | include ledfx/default_presets.yaml 9 | include ledfx/frontend/components/SceneCard/* 10 | # Add documentation to sdist 11 | # Docs: 12 | recursive-include docs * 13 | prune docs/_build 14 | prune */__pycache__ -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = -a -b dirhtml 6 | SPHINXBUILD = sphinx-build 7 | SPHINXPROJ = LedFx 8 | SOURCEDIR = . 9 | BUILDDIR = _build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | 22 | livehtml: 23 | sphinx-autobuild "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) -------------------------------------------------------------------------------- /docs/README.rst: -------------------------------------------------------------------------------- 1 | -------------------------- 2 | Document Development 3 | -------------------------- 4 | 5 | The documentation is written in reStructuredText. Once you are finished 6 | making changes, you must build the documentation. To build the LedFx 7 | documentation follow the steps outlined below: 8 | 9 | Linux 10 | ------- 11 | 12 | .. code:: bash 13 | 14 | $ cd ~/ledfx/docs 15 | $ pip install -r requirements_docs.txt 16 | $ make html 17 | 18 | macOS 19 | ------- 20 | 21 | .. code:: bash 22 | 23 | $ conda activate ledfx 24 | $ cd ~/ledfx/docs 25 | $ pip install -r requirements_docs.txt 26 | $ make html 27 | 28 | 29 | .. Extensions used by sphinx 30 | 31 | .. _sphinx.ext.autodoc: https://www.sphinx-doc.org/en/master/usage/extensions/autodoc.html 32 | .. _sphinx.ext.githubpages: https://www.sphinx-doc.org/en/master/usage/extensions/githubpages.html 33 | .. _sphinxcontrib.httpdomain: https://sphinxcontrib-httpdomain.readthedocs.io/en/stable/ 34 | .. _sphinx_rtd_theme: https://sphinx-rtd-theme.readthedocs.io/en/latest/index.html -------------------------------------------------------------------------------- /docs/_static/css/custom.css: -------------------------------------------------------------------------------- 1 | @import '_build/html/_static/css/theme.css'; 2 | 3 | .rst-content pre.literal-block, .rst-content div[class^='highlight'] pre, .rst-content .linenodiv pre { 4 | /*color of code blocks*/ 5 | background-color: #EEEEEE 6 | } 7 | 8 | /* Hide "On GitHub" section from versions menu */ 9 | div.rst-versions > div.rst-other-versions > div.injected > dl:nth-child(3) { 10 | display: none; 11 | } -------------------------------------------------------------------------------- /docs/_static/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Mattallmighty/LedFx-OLD/9734e7ebf20e9a4b67aca32f932572003badca4d/docs/_static/logo.png -------------------------------------------------------------------------------- /docs/_static/small_black_alpha.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Mattallmighty/LedFx-OLD/9734e7ebf20e9a4b67aca32f932572003badca4d/docs/_static/small_black_alpha.png -------------------------------------------------------------------------------- /docs/_static/small_white_alpha.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Mattallmighty/LedFx-OLD/9734e7ebf20e9a4b67aca32f932572003badca4d/docs/_static/small_white_alpha.png -------------------------------------------------------------------------------- /docs/_templates/breadcrumbs.html: -------------------------------------------------------------------------------- 1 | {%- extends "sphinx_rtd_theme/breadcrumbs.html" %} 2 | 3 | {% block breadcrumbs_aside %} 4 | {% endblock %} -------------------------------------------------------------------------------- /docs/configuring.rst: -------------------------------------------------------------------------------- 1 | =================== 2 | Configuration 3 | =================== 4 | 5 | .. _conf-firmware: 6 | 7 | Firmware Specific 8 | ------------------- 9 | 10 | Once you have LedFx installed, it's time to add some devices! Make sure you have a device with appropriate 11 | firmware for integration with LedFx. Open the LedFx UI and navigate to the 'Device Management' page. 12 | Click the "Add Device" button at the lower right of the web page. Add the device using the following 13 | configuration based on your firmware: 14 | 15 | * ESPixelStick_ 16 | 17 | - Add the device as an E1.31 device. 18 | - The default E1.31 settings should work fine. 19 | 20 | * `Scott's Audio Reactive Firmware`_ 21 | 22 | - Add the device as a UDP device. 23 | - Click 'Additional Properties' and check the 'Include Indexes' box. 24 | 25 | * WLED_ 26 | 27 | - Enable E1.31 support from the 'Sync Settings' page on the WLED web-interface. 28 | - Add the device as an E1.31 device. 29 | - If you have more than 170 LEDs click 'Additional Properties' and set the 'Universe Size' to 510. 30 | 31 | .. Links Down Here 32 | 33 | .. _`Scott's Audio Reactive Firmware`: https://github.com/scottlawsonbc/audio-reactive-led-strip 34 | .. _ESPixelStick: https://github.com/forkineye/ESPixelStick 35 | .. _WLED: https://github.com/Aircoookie/WLED -------------------------------------------------------------------------------- /docs/developer.rst: -------------------------------------------------------------------------------- 1 | ======================= 2 | Development Setup 3 | ======================= 4 | 5 | The development workflow is still being worked on, but this page covers the current state of the world. 6 | 7 | ------------------------------ 8 | 9 | ------------------------- 10 | Backend Development 11 | ------------------------- 12 | 13 | Linux 14 | ------- 15 | 16 | **1.** Clone the dev branch from the LedFx Github repository: 17 | 18 | .. code:: bash 19 | 20 | $ git clone https://github.com/ahodges9/LedFx.git -b dev 21 | $ cd LedFx 22 | 23 | **2.** Enable development mode to prevent having to reinstall and instead just load from the git repository: 24 | 25 | .. code:: bash 26 | 27 | $ python setup.py develop 28 | 29 | **3.** This will let you run LedFx directly from your Git repository via: 30 | 31 | .. code:: bash 32 | 33 | $ ledfx --open-ui 34 | 35 | macOS 36 | ------- 37 | 38 | **1.** Clone the dev branch from the LedFx Github repository: 39 | 40 | .. code:: bash 41 | 42 | $ git clone https://github.com/ahodges9/LedFx.git -b dev 43 | $ cd ./ledfx 44 | 45 | **2.** Create a conda environment for LedFx with Python 3.7 and install dependencies: 46 | 47 | .. code:: bash 48 | 49 | $ conda create -n ledfx python=3.7 50 | $ conda activate ledfx 51 | $ conda config --add channels conda-forge 52 | $ conda install aubio portaudio 53 | 54 | **3.** Install LedFx and its requirements using pip: 55 | 56 | .. code:: bash 57 | 58 | $ pip install -r requirements.txt 59 | $ pip install -e . 60 | $ ledfx --open-ui 61 | 62 | ------------------------------ 63 | 64 | -------------------------- 65 | Frontend Development 66 | -------------------------- 67 | 68 | Linux 69 | ------- 70 | 71 | Building the LedFx frontend is different from how the core backend is built. The frontend is based on React.js and thus uses NPM as the core package management. To get started, first install npm and all the requirements: 72 | 73 | **1.** Start in the LedFx repo directory: 74 | 75 | .. code:: bash 76 | 77 | $ pip install yarn 78 | $ yarn install 79 | 80 | The easiest way to test and validate your changes is to run a watcher that will automatically rebuild as you save and then just leave LedFx running in a separate command window. (Note: LedFx will need to be running in development mode for everything to work). 81 | 82 | **2.** Start LedFx in development mode and start the watcher: 83 | 84 | .. code:: bash 85 | 86 | $ ledfx 87 | $ yarn start 88 | 89 | At that point any change you make to the frontend will be recompiled and after a browser refresh LedFx will pick up the new files. After development and testing you will need to run a full build to generate the appropriate distribution files prior to submitting any changes. 90 | 91 | **3.** Build the frontend: 92 | 93 | .. code:: bash 94 | 95 | $ yarn build 96 | 97 | macOS 98 | ------- 99 | 100 | **1.** Install nodejs and NPM requirements using homebrew: 101 | 102 | .. code:: bash 103 | 104 | $ brew install nodejs 105 | $ brew install yarn 106 | $ cd ~/frontend 107 | $ yarn install 108 | 109 | **2.** Start LedFx in developer mode and start the NPM watcher. (Open the config.yaml file in the .ledfx folder and set ``dev_mode: true``): 110 | 111 | .. code:: bash 112 | 113 | $ ledfx 114 | $ yarn start 115 | 116 | **3.** Build the frontend: 117 | 118 | .. code:: bash 119 | 120 | $ yarn build 121 | 122 | ------------------------------ 123 | 124 | .. include:: README.rst -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../README.rst 2 | 3 | .. .. include:: quickstart.rst 4 | 5 | .. .. include:: contents.rst 6 | 7 | .. toctree:: 8 | :maxdepth: 2 9 | :caption: Installation & Development 10 | 11 | installing 12 | developer 13 | 14 | .. toctree:: 15 | :maxdepth: 2 16 | :caption: Configuration 17 | 18 | configuring 19 | 20 | .. toctree:: 21 | :maxdepth: 2 22 | :caption: LedFx API 23 | 24 | api 25 | 26 | .. toctree:: 27 | :maxdepth: 2 28 | :caption: Help 29 | 30 | trouble 31 | 32 | License 33 | ------- 34 | 35 | LedFx is licensed under the MIT license. -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=python -msphinx 9 | ) 10 | set SPHINXOPTS= 11 | set SPHINXBUILD=sphinx-build 12 | set SOURCEDIR=. 13 | set BUILDDIR=build 14 | set SPHINXPROJ=LedFx 15 | 16 | if "%1" == "" goto help 17 | 18 | %SPHINXBUILD% >NUL 2>NUL 19 | if errorlevel 9009 ( 20 | echo. 21 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 22 | echo.installed, then set the SPHINXBUILD environment variable to point 23 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 24 | echo.may add the Sphinx directory to PATH. 25 | echo. 26 | echo.If you don't have Sphinx installed, grab it from 27 | echo.http://sphinx-doc.org/ 28 | exit /b 1 29 | ) 30 | 31 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% 32 | goto end 33 | 34 | :help 35 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% 36 | 37 | :end 38 | popd 39 | -------------------------------------------------------------------------------- /docs/requirements_docs.txt: -------------------------------------------------------------------------------- 1 | sphinx>=3.0 2 | sphinxcontrib-websupport==1.2.4 3 | sphinxcontrib-httpdomain 4 | sphinx-autodoc-typehints==1.11.1 5 | sphinx-autodoc-annotation==1.0.post1 6 | sphinx-autobuild 7 | sphinx_rtd_theme==0.5.0 8 | travis-sphinx==2.2.1 -------------------------------------------------------------------------------- /frontend/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /frontend/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "arrowParens": "avoid", 3 | "bracketSpacing": true, 4 | "eslintIntegration": true, 5 | "printWidth": 100, 6 | "semi": true, 7 | "tabWidth": 4, 8 | "singleQuote": true, 9 | "trailingComma": "es5" 10 | } -------------------------------------------------------------------------------- /frontend/README.md: -------------------------------------------------------------------------------- 1 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). 2 | 3 | ## Available Scripts 4 | 5 | In the project directory, you can run: 6 | 7 | ### `yarn start` 8 | 9 | Runs the app in the development mode.
10 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser. 11 | 12 | The page will reload if you make edits.
13 | You will also see any lint errors in the console. 14 | 15 | ### `yarn test` 16 | 17 | Launches the test runner in the interactive watch mode.
18 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. 19 | 20 | ### `yarn build` 21 | 22 | Builds the app for production to the `build` folder.
23 | It correctly bundles React in production mode and optimizes the build for the best performance. 24 | 25 | The build is minified and the filenames include the hashes.
26 | Your app is ready to be deployed! 27 | 28 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. 29 | 30 | ### `yarn eject` 31 | 32 | **Note: this is a one-way operation. Once you `eject`, you can’t go back!** 33 | 34 | If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. 35 | 36 | Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own. 37 | 38 | You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it. 39 | 40 | ## Learn More 41 | 42 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). 43 | 44 | To learn React, check out the [React documentation](https://reactjs.org/). 45 | 46 | ### Code Splitting 47 | 48 | This section has moved here: https://facebook.github.io/create-react-app/docs/code-splitting 49 | 50 | ### Analyzing the Bundle Size 51 | 52 | This section has moved here: https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size 53 | 54 | ### Making a Progressive Web App 55 | 56 | This section has moved here: https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app 57 | 58 | ### Advanced Configuration 59 | 60 | This section has moved here: https://facebook.github.io/create-react-app/docs/advanced-configuration 61 | 62 | ### Deployment 63 | 64 | This section has moved here: https://facebook.github.io/create-react-app/docs/deployment 65 | 66 | ### `yarn build` fails to minify 67 | 68 | This section has moved here: https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify 69 | -------------------------------------------------------------------------------- /frontend/eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | parser: '@typescript-eslint/parser', 4 | parserOptions: { 5 | ecmaVersion: 2018, 6 | ecmaFeatures: { 7 | legacyDecorators: true, 8 | }, 9 | sourceType: 'module', 10 | }, 11 | extends: ['plugin:@typescript-eslint/eslint-recommended', 'prettier', 'prettier/react'], 12 | plugins: [ 13 | 'react', 14 | '@typescript-eslint', 15 | 'prettier', 16 | ], 17 | globals: { 18 | __DEV__: false, 19 | jest: false, 20 | jasmine: false, 21 | it: false, 22 | describe: false, 23 | expect: false, 24 | element: false, 25 | by: false, 26 | beforeAll: false, 27 | beforeEach: false, 28 | afterAll: false, 29 | }, 30 | rules: { 31 | 'prettier/prettier': 'error', 32 | 'no-shadow': 'off', 33 | }, 34 | }; 35 | -------------------------------------------------------------------------------- /frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ledfx", 3 | "version": "0.3.0", 4 | "description": "LedFx", 5 | "author": "Austin Hodges", 6 | "license": "MIT", 7 | "repository": { 8 | "type": "git", 9 | "url": "https://github.com/ahodges9/ledfx.git" 10 | }, 11 | "dependencies": { 12 | "@material-ui/core": "^4.10.2", 13 | "@material-ui/icons": "^4.9.1", 14 | "axios": "^0.19.2", 15 | "chart.js": "^2.9.3", 16 | "connected-react-router": "^6.8.0", 17 | "core-js": "^3.6.5", 18 | "debounce": "^1.2.0", 19 | "glob": "^7.1.6", 20 | "history": "^4.10.1", 21 | "react": "^16.13.1", 22 | "react-chartjs-2": "^2.9.0", 23 | "react-dom": "^16.13.1", 24 | "react-event-listener": "^0.6.6", 25 | "react-redux": "^7.2.0", 26 | "react-router": "^4.3.1", 27 | "react-router-dom": "^5.2.0", 28 | "react-schema-form": "^0.8.7", 29 | "redux": "^4.0.5", 30 | "redux-actions": "^2.6.5", 31 | "redux-thunk": "^2.3.0", 32 | "sockette": "^2.0.6", 33 | "spotify-web-api-node": "^4.0.0", 34 | "tv4": "^1.3.0" 35 | }, 36 | "scripts": { 37 | "start": "react-scripts start", 38 | "build_win": "react-scripts build && yarn copyFilesUp_win", 39 | "build": "react-scripts build && yarn copyFilesUp", 40 | "test": "react-scripts test", 41 | "eject": "react-scripts eject", 42 | "clean": "rimraf ./node_modules", 43 | "copyFilesUp": "rimraf ../ledfx_frontend && cp -r ./build/ ../ledfx_frontend", 44 | "copyFilesUp_win": "rmdir /s /q ..\\ledfx_frontend 2>nul & ren build ledfx_frontend && move ledfx_frontend .." 45 | }, 46 | "eslintConfig": { 47 | "extends": "react-app" 48 | }, 49 | "browserslist": { 50 | "production": [ 51 | ">0.2%", 52 | "not dead", 53 | "not op_mini all" 54 | ], 55 | "development": [ 56 | "last 1 chrome version", 57 | "last 1 firefox version", 58 | "last 1 safari version" 59 | ] 60 | }, 61 | "devDependencies": { 62 | "@testing-library/dom": ">=5", 63 | "@testing-library/jest-dom": "^4.2.4", 64 | "@testing-library/react": "^9.3.2", 65 | "@testing-library/user-event": "^7.1.2", 66 | "@typescript-eslint/eslint-plugin": "^2.33.0", 67 | "@typescript-eslint/parser": "^2.33.0", 68 | "eslint": "^6.8.0", 69 | "eslint-config-prettier": "^6.10.0", 70 | "eslint-import-resolver-alias": "^1.1.2", 71 | "eslint-plugin-import": "^2.20.1", 72 | "eslint-plugin-jsx-a11y": "^6.2.3", 73 | "eslint-plugin-prettier": "^3.1.2", 74 | "eslint-plugin-react": "^7.18.2", 75 | "prettier": "^1.19.1", 76 | "react-scripts": "3.4.1", 77 | "redux-devtools-extension": "^2.13.8", 78 | "rimraf": "^3.0.2", 79 | "typescript": "^3.9.2" 80 | }, 81 | "proxy": "http://127.0.0.1:8888/" 82 | } 83 | -------------------------------------------------------------------------------- /frontend/public/__init__.py: -------------------------------------------------------------------------------- 1 | """ledfx_frontend""" 2 | import os 3 | 4 | def where(): 5 | return os.path.dirname(__file__) -------------------------------------------------------------------------------- /frontend/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Mattallmighty/LedFx-OLD/9734e7ebf20e9a4b67aca32f932572003badca4d/frontend/public/favicon.ico -------------------------------------------------------------------------------- /frontend/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | LedFx 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /frontend/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Mattallmighty/LedFx-OLD/9734e7ebf20e9a4b67aca32f932572003badca4d/frontend/public/logo192.png -------------------------------------------------------------------------------- /frontend/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Mattallmighty/LedFx-OLD/9734e7ebf20e9a4b67aca32f932572003badca4d/frontend/public/logo512.png -------------------------------------------------------------------------------- /frontend/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /frontend/src/App/createStore.js: -------------------------------------------------------------------------------- 1 | import thunk from 'redux-thunk'; 2 | import { applyMiddleware, combineReducers, createStore } from 'redux'; 3 | import { composeWithDevTools } from 'redux-devtools-extension'; 4 | import { connectRouter, routerMiddleware } from 'connected-react-router'; 5 | import { createBrowserHistory } from 'history'; 6 | 7 | import reducers from '../modules'; 8 | 9 | export const history = createBrowserHistory(); 10 | 11 | export default () => { 12 | const store = createStore( 13 | combineReducers({ 14 | ...reducers, 15 | router: connectRouter(history), 16 | }), 17 | composeWithDevTools(applyMiddleware(routerMiddleware(history), thunk)) 18 | ); 19 | 20 | return store; 21 | }; 22 | -------------------------------------------------------------------------------- /frontend/src/App/index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Provider } from 'react-redux'; 3 | import { Router, Route, Switch } from 'react-router-dom'; 4 | import { MuiThemeProvider } from '@material-ui/core/styles'; 5 | import CssBaseline from '@material-ui/core/CssBaseline'; 6 | 7 | import indexRoutes from 'routes'; 8 | import createStore, { history } from './createStore'; 9 | import defaultTheme from './theme'; 10 | import './style.css'; 11 | 12 | const store = createStore(); 13 | 14 | export default function App() { 15 | return ( 16 | 17 | 18 | 19 | 20 | 21 | {indexRoutes.map(({ component: Component, path }, key) => { 22 | return ( 23 | } 27 | /> 28 | ); 29 | })} 30 | 31 | 32 | 33 | 34 | ); 35 | } 36 | -------------------------------------------------------------------------------- /frontend/src/App/style.css: -------------------------------------------------------------------------------- 1 | html, body { 2 | font-family: 'Roboto', 'Helvetica', sans-serif; 3 | font-weight: 300; 4 | height: 100%; 5 | width: 100%; 6 | overflow: hidden; 7 | } 8 | 9 | #root { 10 | height: 100%; 11 | width: 100%; 12 | } -------------------------------------------------------------------------------- /frontend/src/App/theme.js: -------------------------------------------------------------------------------- 1 | import { createMuiTheme } from '@material-ui/core/styles'; 2 | import teal from '@material-ui/core/colors/teal'; 3 | 4 | export default createMuiTheme({ 5 | palette: { 6 | primary: teal, 7 | secondary: { 8 | main: '#ef6c00', 9 | }, 10 | }, 11 | overrides: { 12 | MuiFormControl: { 13 | root: { 14 | margin: 8, 15 | minWidth: 225, 16 | flex: '1 0 30%', 17 | }, 18 | }, 19 | }, 20 | props: { 21 | MuiCard: { 22 | variant: "outlined" 23 | } 24 | } 25 | }); 26 | -------------------------------------------------------------------------------- /frontend/src/App/useDarkMode.js: -------------------------------------------------------------------------------- 1 | // useDarkMode.js 2 | 3 | 4 | // Found this online, provides logic to use light or dark theme depending on: 5 | // 1 - Has the user previously set a light or dark theme in this app? 6 | // 2 - If not, what is their OS theme? (window.matchMedia) 7 | // Tried to implement this into the actual theme provider but that proved a bit ambitious for my skills :D 8 | 9 | 10 | import { useEffect, useState } from 'react'; 11 | 12 | export const useDarkMode = () => { 13 | const [theme, setTheme] = useState('light'); 14 | const [componentMounted, setComponentMounted] = useState(false); 15 | const setMode = mode => { 16 | window.localStorage.setItem('theme', mode) 17 | setTheme(mode) 18 | }; 19 | 20 | const toggleTheme = () => { 21 | if (theme === 'light') { 22 | setMode('dark') 23 | } else { 24 | setMode('light') 25 | } 26 | }; 27 | 28 | useEffect(() => { 29 | const localTheme = window.localStorage.getItem('theme'); 30 | window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches && !localTheme ? 31 | setMode('dark') : 32 | localTheme ? 33 | setTheme(localTheme) : 34 | setMode('light'); 35 | setComponentMounted(true); 36 | }, []); 37 | 38 | return [theme, toggleTheme, componentMounted] 39 | }; -------------------------------------------------------------------------------- /frontend/src/assets/img/icon/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Mattallmighty/LedFx-OLD/9734e7ebf20e9a4b67aca32f932572003badca4d/frontend/src/assets/img/icon/favicon.ico -------------------------------------------------------------------------------- /frontend/src/assets/img/icon/large_white_alpha.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Mattallmighty/LedFx-OLD/9734e7ebf20e9a4b67aca32f932572003badca4d/frontend/src/assets/img/icon/large_white_alpha.png -------------------------------------------------------------------------------- /frontend/src/components/AddSceneCard/index.jsx: -------------------------------------------------------------------------------- 1 | import React, { useReducer } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | import { makeStyles } from '@material-ui/core/styles'; 5 | import Card from '@material-ui/core/Card'; 6 | import CardHeader from '@material-ui/core/CardHeader'; 7 | import CardContent from '@material-ui/core/CardContent'; 8 | import Box from '@material-ui/core/Box'; 9 | import TextField from '@material-ui/core/TextField'; 10 | import Button from '@material-ui/core/Button'; 11 | import SaveIcon from '@material-ui/icons/Save'; 12 | 13 | const useStyles = makeStyles(theme => ({ 14 | button: { 15 | display: 'flex', 16 | justifyContent: 'flex-end', 17 | alignItems: 'center', 18 | margin: theme.spacing(1), 19 | }, 20 | })); 21 | 22 | const INITIAL_STATE = { 23 | name: '', 24 | error: '', 25 | }; 26 | 27 | const AddSceneCard = ({ scenes = {}, addScene }) => { 28 | const classes = useStyles(); 29 | const [state, dispatch] = useReducer( 30 | (state, payload) => ({ 31 | ...state, 32 | ...payload, 33 | }), 34 | INITIAL_STATE 35 | ); 36 | const { name, error } = state; 37 | 38 | const handleSave = () => { 39 | addScene(name); 40 | }; 41 | 42 | const handleNameChanged = ({ target: { value } }) => { 43 | const error = validateInput(value, scenes.list) ? '' : 'Scene name already in use!'; 44 | dispatch({ name: value, error }); 45 | }; 46 | 47 | return ( 48 | 49 | 50 | 51 | 52 | 53 | 61 | 62 | 63 | 75 | 76 | 77 | 78 | 79 | ); 80 | }; 81 | 82 | const validateInput = (input, scenes) => 83 | !scenes.some(p => p.name.toLowerCase() === input.toLowerCase()); 84 | 85 | AddSceneCard.propTypes = { 86 | addScene: PropTypes.func.isRequired, 87 | scenes: PropTypes.shape({ 88 | isLoading: PropTypes.bool.isRequired, 89 | list: PropTypes.array.isRequired, 90 | dictionary: PropTypes.object.isRequired, 91 | }).isRequired, 92 | }; 93 | 94 | export default AddSceneCard; 95 | -------------------------------------------------------------------------------- /frontend/src/components/DeviceConfigDialog/AdditionalProperties.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { SchemaForm } from 'react-schema-form'; 4 | 5 | import { makeStyles } from '@material-ui/core/styles'; 6 | import Collapse from '@material-ui/core/Collapse'; 7 | 8 | const useStyles = makeStyles(theme => ({ 9 | flexWrap: { 10 | display: 'flex', 11 | flexWrap: 'wrap', 12 | }, 13 | schemaForm: { 14 | display: 'flex', 15 | flexWrap: 'wrap', 16 | }, 17 | })); 18 | 19 | function AdditionalProperties({ form, model, schema, onChange, open }) { 20 | const classes = useStyles(); 21 | 22 | const handleChange = (...args) => { 23 | if (onChange) { 24 | onChange(...args); 25 | } 26 | }; 27 | 28 | return ( 29 | 30 |
31 | 38 |
39 |
40 | ); 41 | } 42 | 43 | AdditionalProperties.propTypes = { 44 | onChange: PropTypes.func.isRequired, 45 | form: PropTypes.array, 46 | model: PropTypes.object, 47 | scheme: PropTypes.object, 48 | }; 49 | AdditionalProperties.defaultProps = { 50 | classes: '', 51 | form: [], 52 | model: {}, 53 | scheme: {}, 54 | }; 55 | 56 | export default AdditionalProperties; 57 | -------------------------------------------------------------------------------- /frontend/src/components/DeviceMiniControl/index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import withStyles from '@material-ui/core/styles/withStyles'; 4 | import Button from '@material-ui/core/Button'; 5 | import Switch from '@material-ui/core/Switch'; 6 | import Grid from '@material-ui/core/Grid'; 7 | import Typography from '@material-ui/core/Typography'; 8 | import CircularProgress from '@material-ui/core/CircularProgress'; 9 | import { NavLink } from 'react-router-dom'; 10 | 11 | const styles = theme => ({ 12 | button: { 13 | margin: theme.spacing(1), 14 | float: 'right', 15 | }, 16 | submitControls: { 17 | margin: theme.spacing(1), 18 | display: 'block', 19 | width: '100%', 20 | }, 21 | tableCell: { 22 | lineHeight: '1.2', 23 | padding: '12px 8px', 24 | verticalAlign: 'middle', 25 | }, 26 | deviceLink: { 27 | size: "large", 28 | margin: theme.spacing(1), 29 | textDecoration: "none", 30 | "&,&:hover": { 31 | color: "#000000" 32 | }, 33 | }, 34 | actionsContainer: { 35 | display: 'flex', 36 | justifyContent: 'flex-end' 37 | }, 38 | toggleContainer: { 39 | width: '70px', 40 | }, 41 | }); 42 | 43 | class DeviceMiniControl extends React.Component { 44 | toggleOn = ({ target: { checked } }) => { 45 | const { device, setDeviceEffect } = this.props; 46 | 47 | setDeviceEffect(device.id, { 48 | ...device.effect, 49 | active: checked, 50 | }); 51 | }; 52 | 53 | render() { 54 | const { 55 | classes, 56 | device: { id, config, effect }, 57 | } = this.props; 58 | 59 | return ( 60 | 61 | 62 | 63 | {config.name} 64 | 65 | 66 | Effect: {effect.name ? effect.name : "None"} 67 | 68 | 69 | 70 | 74 | 82 | 88 | {effect.isProcessing ? ( 89 | 90 | ) : ( 91 | 96 | )} 97 | 98 | 99 | 100 | ); 101 | } 102 | } 103 | 104 | DeviceMiniControl.propTypes = { 105 | classes: PropTypes.object.isRequired, 106 | device: PropTypes.object.isRequired, 107 | setDeviceEffect: PropTypes.func.isRequired, 108 | }; 109 | 110 | export default withStyles(styles)(DeviceMiniControl); 111 | -------------------------------------------------------------------------------- /frontend/src/components/DevicesTable/DevicesTableItem.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { NavLink } from 'react-router-dom'; 4 | import { withStyles } from '@material-ui/core/styles'; 5 | import TableRow from '@material-ui/core/TableRow'; 6 | import TableCell from '@material-ui/core/TableCell'; 7 | import Button from '@material-ui/core/Button'; 8 | import DeleteIcon from '@material-ui/icons/Delete'; 9 | import EditIcon from '@material-ui/icons/Edit'; 10 | 11 | const styles = theme => ({ 12 | button: { 13 | margin: 0, 14 | padding: 0, 15 | minWidth: 32, 16 | }, 17 | deleteButton: { 18 | minWidth: 32, 19 | }, 20 | editButton: { 21 | minWidth: 32, 22 | }, 23 | actions: { 24 | display: 'flex', 25 | '& > *': { 26 | marginLeft: theme.spacing(1), 27 | }, 28 | }, 29 | deviceLink: { 30 | textDecoration: 'none', 31 | color: 'black', 32 | '&:hover': { 33 | color: theme.palette.primary.main, 34 | }, 35 | }, 36 | }); 37 | 38 | function DevicesTableItem({ device, onDelete, classes, onEdit }) { 39 | const handleDeleteDevice = () => { 40 | onDelete(device.id); 41 | }; 42 | 43 | const handleEditItem = () => { 44 | onEdit(device); 45 | }; 46 | 47 | return ( 48 | 49 | 50 | 55 | {device.config.name} 56 | 57 | 58 | {device.config.ip_address} 59 | {device.config.pixel_count} 60 | {device.type} 61 | 62 | 71 | 79 | 80 | 81 | ); 82 | } 83 | 84 | DevicesTableItem.propTypes = { 85 | classes: PropTypes.object.isRequired, 86 | device: PropTypes.object.isRequired, 87 | onDelete: PropTypes.func.isRequired, 88 | }; 89 | 90 | export default withStyles(styles)(DevicesTableItem); 91 | -------------------------------------------------------------------------------- /frontend/src/components/DevicesTable/index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { withStyles } from '@material-ui/core/styles'; 4 | import Table from '@material-ui/core/Table'; 5 | import TableHead from '@material-ui/core/TableHead'; 6 | import TableRow from '@material-ui/core/TableRow'; 7 | import TableCell from '@material-ui/core/TableCell'; 8 | import TableBody from '@material-ui/core/TableBody'; 9 | 10 | import DevicesTableItem from './DevicesTableItem.jsx'; 11 | 12 | const styles = theme => ({ 13 | table: { 14 | borderSpacing: '0', 15 | borderCollapse: 'collapse', 16 | }, 17 | tableResponsive: { 18 | overflowX: 'auto', 19 | }, 20 | }); 21 | 22 | function DevicesTable({ onDeleteDevice, classes, items, onEditDevice }) { 23 | return ( 24 |
25 | 26 | 27 | 28 | Name 29 | IP Address 30 | Pixel Count 31 | Type 32 | 33 | 34 | 35 | 36 | 37 | {items.map(device => { 38 | return ( 39 | 45 | ); 46 | })} 47 | 48 |
49 |
50 | ); 51 | } 52 | 53 | DevicesTable.propTypes = { 54 | classes: PropTypes.object.isRequired, 55 | items: PropTypes.array.isRequired, 56 | }; 57 | 58 | export default withStyles(styles)(DevicesTable); 59 | -------------------------------------------------------------------------------- /frontend/src/components/MiniScenesCard/index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import withStyles from '@material-ui/core/styles/withStyles'; 3 | import Card from '@material-ui/core/Card'; 4 | import CardHeader from '@material-ui/core/CardHeader'; 5 | import CardContent from '@material-ui/core/CardContent'; 6 | import Button from '@material-ui/core/Button'; 7 | import Grid from '@material-ui/core/Grid'; 8 | import CircularProgress from '@material-ui/core/CircularProgress'; 9 | 10 | const styles = theme => ({ 11 | sceneButton: { 12 | size: "large", 13 | margin: theme.spacing(1), 14 | textDecoration: "none", 15 | "&,&:hover": { 16 | color: "#000000" 17 | } 18 | }, 19 | submitControls: { 20 | display: "flex", 21 | flexWrap: "wrap", 22 | width: "100%", 23 | height: "100%" 24 | } 25 | }) 26 | 27 | 28 | class MiniScenesCard extends React.Component { 29 | 30 | render() { 31 | const { scenes, classes, activateScene } = this.props; 32 | 33 | return ( 34 | 35 | 36 | {/*link header to scenes management page*/} 37 | 38 | {scenes.isLoading ? ( 39 | 45 | 46 | 47 | ) : ( 48 | scenes.list.map(scene => ( 49 | 54 | )) 55 | )} 56 | 57 | 58 | ); 59 | } 60 | } 61 | 62 | export default (withStyles(styles)(MiniScenesCard)); -------------------------------------------------------------------------------- /frontend/src/components/SceneCard/SceneConfigTable.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { withStyles } from '@material-ui/core/styles'; 4 | 5 | import Table from '@material-ui/core/Table'; 6 | import TableHead from '@material-ui/core/TableHead'; 7 | import TableRow from '@material-ui/core/TableRow'; 8 | import TableCell from '@material-ui/core/TableCell'; 9 | import TableBody from '@material-ui/core/TableBody'; 10 | 11 | const styles = theme => ({ 12 | table: { 13 | } 14 | }); 15 | 16 | class SceneConfigTable extends React.Component { 17 | 18 | render() { 19 | const { classes, devices } = this.props; 20 | 21 | return ( 22 | 23 | 24 | 25 | Device 26 | Effect 27 | 28 | 29 | 30 | {renderRows(devices)} 31 | 32 |
33 | ); 34 | } 35 | } 36 | 37 | const renderRows = (devices) => { 38 | const devicesWithEffects = Object.keys(devices).filter(id => !!devices[id].type); 39 | return devicesWithEffects.map(id => { 40 | return ( 41 | 42 | 43 | {id} 44 | 45 | 46 | {devices[id].type} 47 | 48 | 49 | )}) 50 | } 51 | 52 | SceneConfigTable.propTypes = { 53 | classes: PropTypes.object.isRequired, 54 | devices: PropTypes.object.isRequired 55 | }; 56 | 57 | export default withStyles(styles)(SceneConfigTable); -------------------------------------------------------------------------------- /frontend/src/components/SceneCard/index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { makeStyles } from '@material-ui/core/styles'; 4 | import Card from '@material-ui/core/Card'; 5 | import CardHeader from '@material-ui/core/CardHeader'; 6 | import CardContent from '@material-ui/core/CardContent'; 7 | import CardActions from '@material-ui/core/CardActions'; 8 | import Button from '@material-ui/core/Button'; 9 | import DeleteIcon from '@material-ui/icons/Delete'; 10 | import CheckCircleIcon from '@material-ui/icons/CheckCircle'; 11 | 12 | import SceneConfigTable from 'components/SceneCard/SceneConfigTable'; 13 | 14 | const useStyles = makeStyles(theme => ({ 15 | deleteButton: { 16 | margin: theme.spacing(1), 17 | size: "medium", 18 | }, 19 | button: { 20 | color: "primary", 21 | margin: theme.spacing(1), 22 | size: "medium", 23 | variant: "contained", 24 | }, 25 | submitControls: { 26 | flex: 1, 27 | justifyContent: 'flex-end', 28 | }, 29 | })); 30 | 31 | export default function SceneCard({ scene, activateScene, deleteScene }) { 32 | const classes = useStyles(); 33 | 34 | const handleActivate = () => { 35 | activateScene(scene.id); 36 | }; 37 | 38 | const handleDelete = () => { 39 | deleteScene(scene.id); 40 | }; 41 | 42 | return ( 43 | 44 | 45 | 46 | {scene.devices && } 47 | 48 | 49 | 59 | 69 | 70 | 71 | ); 72 | } 73 | 74 | SceneCard.propTypes = { 75 | scene: PropTypes.object.isRequired, 76 | deleteScene: PropTypes.func.isRequired, 77 | activateScene: PropTypes.func.isRequired, 78 | }; 79 | -------------------------------------------------------------------------------- /frontend/src/components/SceneConfigDialog/index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import withStyles from '@material-ui/core/styles/withStyles'; 3 | import { connect } from 'react-redux'; 4 | 5 | import Dialog from '@material-ui/core/Dialog'; 6 | import DialogContent from '@material-ui/core/DialogContent'; 7 | import DialogTitle from '@material-ui/core/DialogTitle'; 8 | import DialogContentText from '@material-ui/core/DialogContentText'; 9 | 10 | import SchemaFormCollection from 'components/SchemaForm'; 11 | import { addDevice } from 'actions'; 12 | 13 | const styles = theme => ({ 14 | button: { 15 | float: 'right', 16 | }, 17 | }); 18 | 19 | class SceneConfigDialog extends React.Component { 20 | handleClose = () => { 21 | this.props.onClose(); 22 | }; 23 | 24 | handleSubmit = (type, config) => { 25 | this.props.dispatch(addDevice(type, config)); 26 | this.props.onClose(); 27 | }; 28 | 29 | render() { 30 | const { classes, dispatch, schemas, onClose, ...otherProps } = this.props; 31 | return ( 32 | 38 | Add Preset 39 | 40 | 41 | To add a scene to LedFx, please first configure the effects you wish to 42 | save, select the type of preset you wish, and then provide the necessary 43 | configuration. 44 | 45 | 52 | 53 | 54 | ); 55 | } 56 | } 57 | 58 | export default connect(state => ({ 59 | schemas: state.schemas, 60 | }))(withStyles(styles)(SceneConfigDialog)); 61 | -------------------------------------------------------------------------------- /frontend/src/components/SchemaForm/AdditionalProperties.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { SchemaForm } from 'react-schema-form'; 4 | 5 | import { makeStyles } from '@material-ui/core/styles'; 6 | import Collapse from '@material-ui/core/Collapse'; 7 | 8 | const useStyles = makeStyles(theme => ({ 9 | flexWrap: { 10 | display: 'flex', 11 | flexWrap: 'wrap', 12 | }, 13 | schemaForm: { 14 | display: 'flex', 15 | flexWrap: 'wrap', 16 | }, 17 | })); 18 | 19 | function AdditionalProperties({ form, model, schema, onChange, open }) { 20 | const classes = useStyles(); 21 | 22 | const handleChange = (...args) => { 23 | if (onChange) { 24 | onChange(...args); 25 | } 26 | }; 27 | 28 | return ( 29 | 30 |
31 | 38 |
39 |
40 | ); 41 | } 42 | 43 | AdditionalProperties.propTypes = { 44 | onChange: PropTypes.func.isRequired, 45 | form: PropTypes.array, 46 | model: PropTypes.object, 47 | scheme: PropTypes.object, 48 | }; 49 | AdditionalProperties.defaultProps = { 50 | classes: '', 51 | form: [], 52 | model: {}, 53 | scheme: {}, 54 | }; 55 | 56 | export default AdditionalProperties; 57 | -------------------------------------------------------------------------------- /frontend/src/components/SchemaForm/customFields/Slider.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ComposedComponent from 'react-schema-form/lib/ComposedComponent'; 3 | 4 | import { makeStyles } from '@material-ui/core/styles'; 5 | import Slider from '@material-ui/core/Slider'; 6 | import InputLabel from '@material-ui/core/InputLabel'; 7 | import Box from '@material-ui/core/Box'; 8 | import debounce from 'debounce'; 9 | 10 | const useStyles = makeStyles(theme => ({ 11 | root: { 12 | margin: theme.spacing(1), 13 | padding: theme.spacing(1), 14 | width: '100%', 15 | flex: '1 0 30%', 16 | }, 17 | valueLabel: { 18 | transformOrigin: 'top right', 19 | } 20 | })); 21 | 22 | function valuetext(value) { 23 | return value.toString(); 24 | } 25 | 26 | const SliderComponent = props => { 27 | const { 28 | form: { schema }, 29 | value, 30 | onChangeValidate, 31 | } = props; 32 | 33 | const debouncedOnChangeHandler = debounce(onChangeValidate, 200) 34 | 35 | const onChange = (e, newValue) => { 36 | debouncedOnChangeHandler(newValue); 37 | }; 38 | 39 | const classes = useStyles(); 40 | 41 | const marks = [ 42 | { 43 | value: schema.minimum, 44 | label: schema.minimum, 45 | }, 46 | { 47 | value: schema.maximum, 48 | label: schema.maximum, 49 | }, 50 | ]; 51 | 52 | return ( 53 |
54 | 55 | 56 | {schema.title} 57 | 58 | 59 | {value} 60 | 61 | 62 | 73 |
74 | ); 75 | }; 76 | 77 | // export default SliderComponent; 78 | export default ComposedComponent(SliderComponent); 79 | -------------------------------------------------------------------------------- /frontend/src/components/SchemaForm/mapper.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import DefaultNumberField from 'react-schema-form/lib/Number'; 3 | 4 | import Slider from './customFields/Slider'; 5 | 6 | const mapper = { 7 | number: props => { 8 | const { 9 | form: { 10 | schema 11 | }, 12 | } = props; 13 | 14 | if (schema.minimum !== undefined && schema.maximum !== undefined) { 15 | return ; 16 | } 17 | 18 | return 19 | }, 20 | }; 21 | 22 | export default mapper; 23 | -------------------------------------------------------------------------------- /frontend/src/components/SchemaForm/utils.js: -------------------------------------------------------------------------------- 1 | import tv4 from 'tv4'; 2 | 3 | export function selectOrSet(projection, obj, valueToSet, type) { 4 | var numRe = /^\d+$/; 5 | 6 | if (!obj) { 7 | obj = this; 8 | } 9 | // Support [] array syntax 10 | var parts = 11 | typeof projection === 'string' ? Object.parse(projection) : projection; 12 | 13 | if (typeof valueToSet !== 'undefined' && parts.length === 1) { 14 | // special case, just setting one variable 15 | obj[parts[0]] = valueToSet; 16 | return obj; 17 | } 18 | 19 | if (typeof valueToSet !== 'undefined' && typeof obj[parts[0]] === 'undefined') { 20 | // We need to look ahead to check if array is appropriate 21 | obj[parts[0]] = parts.length > 2 && numRe.test(parts[1]) ? [] : {}; 22 | } 23 | 24 | if ( 25 | typeof type !== 'undefined' && 26 | ['number', 'integer'].indexOf(type) > -1 && 27 | typeof valueToSet === 'undefined' 28 | ) { 29 | // number or integer can undefined 30 | obj[parts[0]] = valueToSet; 31 | return obj; 32 | } 33 | 34 | var value = obj[parts[0]]; 35 | for (var i = 1; i < parts.length; i += 1) { 36 | // Special case: We allow JSON Form syntax for arrays using empty brackets 37 | // These will of course not work here so we exit if they are found. 38 | if (parts[i] === '') { 39 | return undefined; 40 | } 41 | if (typeof valueToSet !== 'undefined') { 42 | if (i === parts.length - 1) { 43 | // last step. Let's set the value 44 | value[parts[i]] = valueToSet; 45 | return valueToSet; 46 | } 47 | // Make sure to create new objects on the way if they are not there. 48 | // We need to look ahead to check if array is appropriate 49 | var tmp = value[parts[i]]; 50 | if (typeof tmp === 'undefined' || tmp === null) { 51 | tmp = numRe.test(parts[i + 1]) ? [] : {}; 52 | value[parts[i]] = tmp; 53 | } 54 | value = tmp; 55 | } else if (value) { 56 | // Just get nex value. 57 | value = value[parts[i]]; 58 | } 59 | } 60 | return value; 61 | } 62 | 63 | export function extractValue(e, prop) { 64 | let value = undefined; 65 | switch (prop.type) { 66 | case 'integer': 67 | case 'number': 68 | if (e.target.value.indexOf('.') === -1) { 69 | value = parseInt(e.target.value); 70 | } else { 71 | value = parseFloat(e.target.value); 72 | } 73 | 74 | if (isNaN(value) || value === null) { 75 | value = undefined; 76 | } 77 | break; 78 | case 'boolean': 79 | value = e.target.checked; 80 | break; 81 | default: 82 | value = e.target.value; 83 | 84 | if (value === '') { 85 | value = undefined; 86 | } 87 | } 88 | 89 | return value; 90 | } 91 | 92 | export function validateValue(v, prop) { 93 | let schema = { type: 'object', properties: {} }; 94 | schema.properties[prop.title] = prop; 95 | schema.required = prop.required ? [prop.title] : []; 96 | 97 | let value = {}; 98 | if (v !== undefined) { 99 | value[prop.title] = v; 100 | } 101 | 102 | let result = tv4.validateResult(value, schema); 103 | return result; 104 | } 105 | 106 | export function validateData(formData, schema) { 107 | // let schema = { type: "object", properties: formData }; 108 | let result = tv4.validateResult(formData, schema); 109 | 110 | return result; 111 | } 112 | -------------------------------------------------------------------------------- /frontend/src/components/forms/DropDown/index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { makeStyles } from '@material-ui/core/styles'; 4 | import Select from '@material-ui/core/Select'; 5 | import InputLabel from '@material-ui/core/InputLabel'; 6 | import MenuItem from '@material-ui/core/MenuItem'; 7 | import FormControl from '@material-ui/core/FormControl'; 8 | import Typography from '@material-ui/core/Typography'; 9 | 10 | const useStyles = makeStyles(theme => ({ 11 | control: { 12 | margin: theme.spacing(1), 13 | width: '100%', 14 | }, 15 | })); 16 | 17 | function DropDown({ onChange, value, options, label }) { 18 | const classes = useStyles(); 19 | 20 | const handleChange = ({ target: { value } }) => { 21 | if (onChange) { 22 | onChange(value); 23 | } 24 | }; 25 | return ( 26 | 27 | {label && {label}} 28 | 41 | 42 | ); 43 | } 44 | 45 | DropDown.propTypes = { 46 | onChange: PropTypes.func.isRequired, 47 | options: PropTypes.array, 48 | value: PropTypes.string, 49 | }; 50 | DropDown.defaultProps = { 51 | classes: '', 52 | options: [], 53 | value: '', 54 | }; 55 | 56 | export default DropDown; 57 | -------------------------------------------------------------------------------- /frontend/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './App'; 4 | import * as serviceWorker from './serviceWorker'; 5 | 6 | ReactDOM.render( 7 | 8 | 9 | , 10 | document.getElementById('root') 11 | ); 12 | 13 | // If you want your app to work offline and load faster, you can change 14 | // unregister() to register() below. Note this comes with some pitfalls. 15 | // Learn more about service workers: https://bit.ly/CRA-PWA 16 | serviceWorker.unregister(); 17 | -------------------------------------------------------------------------------- /frontend/src/layouts/Default/Header/index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import withStyles from '@material-ui/core/styles/withStyles'; 4 | import AppBar from '@material-ui/core/AppBar'; 5 | import Toolbar from '@material-ui/core/Toolbar'; 6 | import IconButton from '@material-ui/core/IconButton'; 7 | import Hidden from '@material-ui/core/Hidden'; 8 | import Typography from '@material-ui/core/Typography'; 9 | import Menu from '@material-ui/icons/Menu'; 10 | 11 | import viewRoutes from 'routes/views.jsx'; 12 | import { drawerWidth } from 'utils/style'; 13 | 14 | const styles = theme => ({ 15 | appBar: { 16 | backgroundColor: 'transparent', 17 | boxShadow: 'none', 18 | position: 'absolute', 19 | marginLeft: drawerWidth, 20 | [theme.breakpoints.up('md')]: { 21 | width: `calc(100% - ${drawerWidth}px)`, 22 | }, 23 | }, 24 | flex: { 25 | flex: 1, 26 | fontSize: 18, 27 | fontWeight: 300, 28 | }, 29 | }); 30 | 31 | class Header extends React.Component { 32 | getPageName() { 33 | const { location, devicesDictionary } = this.props; 34 | const { pathname } = location; 35 | let name = viewRoutes.find((prop, key) => prop.path === pathname)?.navbarName; 36 | 37 | if (!name) { 38 | if (pathname.startsWith('/devices/')) { 39 | const deviceId = pathname.replace('/devices/', ''); 40 | const deviceName = 41 | devicesDictionary[deviceId] !== undefined 42 | ? devicesDictionary[deviceId].config.name 43 | : ''; 44 | name = 'Devices / ' + deviceName; 45 | } else if (pathname.startsWith('/developer/')) { 46 | name = 'Developer / Custom'; 47 | } 48 | } 49 | return name; 50 | } 51 | 52 | render() { 53 | const { classes } = this.props; 54 | 55 | return ( 56 | 57 | 58 | 59 | {this.getPageName()} 60 | 61 | 62 | 67 | 68 | 69 | 70 | 71 | 72 | ); 73 | } 74 | } 75 | 76 | Header.propTypes = { 77 | classes: PropTypes.object.isRequired, 78 | devicesDictionary: PropTypes.object.isRequired, 79 | }; 80 | export default withStyles(styles)(Header); 81 | -------------------------------------------------------------------------------- /frontend/src/layouts/Default/Sidebar/style.jsx: -------------------------------------------------------------------------------- 1 | import { drawerWidth } from 'utils/style'; 2 | 3 | const sidebarStyle = theme => ({ 4 | drawerPaper: { 5 | width: drawerWidth, 6 | [theme.breakpoints.up('md')]: { 7 | width: drawerWidth, 8 | position: 'fixed', 9 | height: '100%', 10 | }, 11 | }, 12 | logo: { 13 | position: 'relative', 14 | padding: '15px 15px', 15 | zIndex: '4', 16 | '&:after': { 17 | content: '""', 18 | position: 'absolute', 19 | bottom: '0', 20 | height: '1px', 21 | right: '15px', 22 | width: 'calc(100% - 30px)', 23 | backgroundColor: 'rgba(180, 180, 180, 0.3)', 24 | }, 25 | }, 26 | logoLink: { 27 | padding: '5px 0', 28 | display: 'block', 29 | fontSize: '18px', 30 | textAlign: 'left', 31 | fontWeight: '400', 32 | lineHeight: '30px', 33 | textDecoration: 'none', 34 | backgroundColor: 'transparent', 35 | '&,&:hover': { 36 | color: '#FFFFFF', 37 | }, 38 | }, 39 | logoImage: { 40 | width: '30px', 41 | display: 'inline-block', 42 | maxHeight: '30px', 43 | marginLeft: '10px', 44 | marginRight: '15px', 45 | }, 46 | img: { 47 | width: '35px', 48 | top: '17px', 49 | position: 'absolute', 50 | verticalAlign: 'middle', 51 | border: '0', 52 | }, 53 | background: { 54 | position: 'absolute', 55 | zIndex: '1', 56 | height: '100%', 57 | width: '100%', 58 | display: 'block', 59 | top: '0', 60 | left: '0', 61 | backgroundSize: 'cover', 62 | backgroundPosition: 'center center', 63 | '&:after': { 64 | position: 'absolute', 65 | zIndex: '3', 66 | width: '100%', 67 | height: '100%', 68 | content: '""', 69 | display: 'block', 70 | background: '#000', 71 | opacity: '.8', 72 | }, 73 | }, 74 | list: { 75 | marginTop: '20px', 76 | paddingLeft: '0', 77 | paddingTop: '0', 78 | paddingBottom: '0', 79 | marginBottom: '0', 80 | listStyle: 'none', 81 | position: 'unset', 82 | }, 83 | item: { 84 | position: 'relative', 85 | display: 'block', 86 | textDecoration: 'none', 87 | '&:hover,&:focus,&:visited,&': { 88 | color: '#FFFFFF', 89 | }, 90 | }, 91 | itemLink: { 92 | width: 'auto', 93 | transition: 'all 300ms linear', 94 | margin: '10px 15px 0', 95 | borderRadius: '3px', 96 | position: 'relative', 97 | display: 'block', 98 | padding: '10px 15px', 99 | backgroundColor: 'transparent', 100 | }, 101 | itemIcon: { 102 | width: '24px', 103 | minWidth: '24px', 104 | height: '30px', 105 | float: 'left', 106 | marginRight: '15px', 107 | textAlign: 'center', 108 | verticalAlign: 'middle', 109 | color: 'rgba(255, 255, 255, 0.8)', 110 | }, 111 | itemText: { 112 | margin: '0', 113 | lineHeight: '30px', 114 | fontSize: '14px', 115 | fontWeight: 300, 116 | color: '#FFFFFF', 117 | }, 118 | devicesItemText: { 119 | margin: '0', 120 | marginLeft: '10px', 121 | lineHeight: '30px', 122 | fontSize: '14px', 123 | fontWeight: 300, 124 | color: '#FFFFFF', 125 | textDecoration: 'none', 126 | }, 127 | activeView: { 128 | backgroundColor: [theme.palette.primary.main], 129 | boxShadow: [theme.shadows[12]], 130 | '&:hover,&:focus,&:visited,&': { 131 | backgroundColor: [theme.palette.primary.main], 132 | boxShadow: [theme.shadows[12]], 133 | }, 134 | color: '#FFFFFF', 135 | }, 136 | sidebarWrapper: { 137 | position: 'relative', 138 | height: 'calc(100vh - 70px)', 139 | overflow: 'auto', 140 | zIndex: '4', 141 | overflowScrolling: 'touch', 142 | }, 143 | }); 144 | 145 | export default sidebarStyle; 146 | -------------------------------------------------------------------------------- /frontend/src/layouts/Default/index.jsx: -------------------------------------------------------------------------------- 1 | import React, { createRef } from 'react'; 2 | import { connect } from 'react-redux'; 3 | import PropTypes from 'prop-types'; 4 | import { Switch, Route, Redirect } from 'react-router-dom'; 5 | import withStyles from '@material-ui/core/styles/withStyles'; 6 | 7 | import { fetchSchemas } from 'modules/schemas'; 8 | import { getConfig } from 'modules/settings'; 9 | import { drawerWidth } from 'utils/style'; 10 | import viewRoutes from 'routes/views'; 11 | 12 | import Header from './Header'; 13 | import Sidebar from './Sidebar'; 14 | 15 | const style = theme => ({ 16 | root: { 17 | overflow: 'hidden', 18 | display: 'flex', 19 | width: '100%', 20 | height: '100%', 21 | }, 22 | content: { 23 | flexGrow: 1, 24 | backgroundColor: theme.palette.background.default, 25 | padding: theme.spacing(3), 26 | minWidth: 200, 27 | [theme.breakpoints.up('md')]: { 28 | marginLeft: drawerWidth, 29 | }, 30 | overflowY: 'auto', 31 | }, 32 | toolbar: theme.mixins.toolbar, 33 | }); 34 | 35 | class DefaultLayout extends React.Component { 36 | constructor(props) { 37 | super(props); 38 | this.state = { 39 | mobileOpen: false, 40 | }; 41 | 42 | this.root = createRef(); 43 | } 44 | 45 | componentDidMount() { 46 | const { getConfig, fetchSchemas } = this.props; 47 | getConfig(); 48 | fetchSchemas(); 49 | } 50 | 51 | componentDidUpdate(prevProps) { 52 | if (prevProps.history.location.pathname !== prevProps.location.pathname) { 53 | this.root.scrollTo({ top: 0, behavior: 'smooth' }); 54 | 55 | if (this.state.mobileOpen) { 56 | this.setState({ mobileOpen: false }); 57 | } 58 | } 59 | } 60 | 61 | setRootRef = el => { 62 | this.root = el; 63 | }; 64 | 65 | handleDrawerToggle = () => { 66 | this.setState(prevState => ({ mobileOpen: !prevState.mobileOpen })); 67 | }; 68 | 69 | render() { 70 | const { classes, deviceDictionary, location, settings } = this.props; 71 | const { mobileOpen } = this.state; 72 | 73 | return ( 74 |
75 |
80 | 87 | 88 |
89 |
90 | 91 | {viewRoutes.map(({ redirect, path, to, component: Component }, key) => { 92 | if (redirect) { 93 | return ; 94 | } 95 | 96 | return ( 97 | } 102 | /> 103 | ); 104 | })} 105 | 106 |
107 |
108 | ); 109 | } 110 | } 111 | 112 | DefaultLayout.propTypes = { 113 | classes: PropTypes.object.isRequired, 114 | deviceDictionary: PropTypes.object.isRequired, 115 | settings: PropTypes.object.isRequired, 116 | schemas: PropTypes.object.isRequired, 117 | }; 118 | 119 | export default connect( 120 | state => ({ 121 | deviceDictionary: state.devices.dictionary, 122 | schemas: state.schemas, 123 | settings: state.settings, 124 | }), 125 | { 126 | fetchSchemas, 127 | getConfig, 128 | } 129 | )(withStyles(style)(DefaultLayout)); 130 | -------------------------------------------------------------------------------- /frontend/src/modules/index.js: -------------------------------------------------------------------------------- 1 | import devices from './devices'; 2 | import presets from './presets'; 3 | import selectedDevice from './selectedDevice'; 4 | import schemas from './schemas'; 5 | import scenes from './scenes'; 6 | import settings from './settings'; 7 | 8 | export default { 9 | devices, 10 | presets, 11 | schemas, 12 | scenes, 13 | settings, 14 | selectedDevice, 15 | }; 16 | -------------------------------------------------------------------------------- /frontend/src/modules/presets.js: -------------------------------------------------------------------------------- 1 | import { createAction, handleActions } from 'redux-actions'; 2 | import * as deviceProxies from 'proxies/device'; 3 | import * as effectsProxies from 'proxies/effects'; 4 | import { convertDictionaryToList } from 'utils/helpers'; 5 | import { effectReceived } from 'modules/selectedDevice'; 6 | 7 | // Actions 8 | const ACTION_ROOT = 'sceneManagement'; 9 | export const presetsFetching = createAction(`${ACTION_ROOT}/PRESETS_FETCHING`); 10 | export const presetsFetched = createAction(`${ACTION_ROOT}/PRESETS_FETCHED`); 11 | export const presetAdding = createAction(`${ACTION_ROOT}/PRESET_ADDING`); 12 | export const presetAdded = createAction(`${ACTION_ROOT}/PRESET_ADDED`); 13 | 14 | // Reducer 15 | const INITIAL_STATE = { 16 | isLoading: false, 17 | isProcessing: false, 18 | defaultPresets: [], 19 | customPresets: [], 20 | effectType: '', 21 | }; 22 | 23 | export default handleActions( 24 | { 25 | [presetsFetching]: state => ({ 26 | ...state, 27 | isLoading: true, 28 | }), 29 | [presetsFetched]: ( 30 | state, 31 | { payload, payload: { defaultPresets = [], customPresets = [], effectType }, error } 32 | ) => { 33 | return { 34 | ...state, 35 | defaultPresets: error ? [] : defaultPresets, 36 | customPresets: error ? [] : customPresets, 37 | effectType, 38 | isLoading: false, 39 | error: error ? payload.message : '', 40 | }; 41 | }, 42 | [presetAdding]: state => ({ 43 | ...state, 44 | isProcessing: true, 45 | }), 46 | 47 | [presetAdded]: (state, { payload, payload: { id, name, config }, error }) => { 48 | const customPresets = [ 49 | ...state.customPresets, 50 | { 51 | id, 52 | name, 53 | config, 54 | }, 55 | ]; 56 | return { 57 | ...state, 58 | customPresets: error ? state.customPresets : customPresets, 59 | isProcessing: false, 60 | error: error ? payload.message : '', 61 | }; 62 | }, 63 | }, 64 | INITIAL_STATE 65 | ); 66 | 67 | export function getEffectPresets(effectType) { 68 | return async dispatch => { 69 | dispatch(presetsFetching()); 70 | try { 71 | const response = await effectsProxies.getEffectPresets(effectType); 72 | 73 | if (response.statusText === 'OK') { 74 | const { default_presets, custom_presets, effect } = response.data; 75 | const defaultPresets = convertDictionaryToList(default_presets); 76 | const customPresets = convertDictionaryToList(custom_presets); 77 | dispatch(presetsFetched({ defaultPresets, customPresets, effectType: effect })); 78 | } 79 | } catch (error) { 80 | dispatch(presetsFetched(error)); 81 | } 82 | }; 83 | } 84 | 85 | export function addPreset(deviceId, name) { 86 | return async dispatch => { 87 | dispatch(presetAdding()); 88 | try { 89 | const { data, statusText } = await deviceProxies.addPreset(deviceId, { name }); 90 | if (statusText === 'OK') { 91 | dispatch(presetAdded(data.preset)); 92 | } 93 | } catch (error) { 94 | dispatch(presetAdded(error)); 95 | } 96 | }; 97 | } 98 | 99 | export function activatePreset(deviceId, category, effectId, presetId) { 100 | return async dispatch => { 101 | try { 102 | const request = { 103 | category: category, 104 | effect_id: effectId, 105 | preset_id: presetId, 106 | }; 107 | 108 | const { data, statusText } = await deviceProxies.updatePreset(deviceId, request); 109 | if (statusText === 'OK') { 110 | dispatch(effectReceived(data.effect)); 111 | } 112 | } catch (error) { 113 | dispatch(effectReceived(error)); 114 | } 115 | }; 116 | } 117 | -------------------------------------------------------------------------------- /frontend/src/modules/scenes.js: -------------------------------------------------------------------------------- 1 | import { createAction, handleActions } from 'redux-actions'; 2 | import * as scenesProxies from 'proxies/scenes'; 3 | 4 | // Actions 5 | const ACTION_ROOT = 'sceneManagement'; 6 | export const scenesFetching = createAction(`${ACTION_ROOT}/SCENES_FETCHING`); 7 | export const scenesFetched = createAction(`${ACTION_ROOT}/SCENES_FETCHED`); 8 | export const sceneAdding = createAction(`${ACTION_ROOT}/SCENE_ADDING`); 9 | export const sceneAdded = createAction(`${ACTION_ROOT}/SCENE_ADDED`); 10 | 11 | // Reducer 12 | const INITIAL_STATE = { 13 | isLoading: false, 14 | isProcessing: false, 15 | dictionary: {}, 16 | list: [], 17 | }; 18 | 19 | export default handleActions( 20 | { 21 | [scenesFetching]: state => ({ 22 | ...state, 23 | isLoading: true, 24 | }), 25 | [scenesFetched]: (state, { payload, payload: { scenes = {}, list = [] }, error }) => { 26 | return { 27 | ...state, 28 | dictionary: error ? {} : scenes, 29 | list: error ? [] : list, 30 | isLoading: false, 31 | error: error ? payload.message : '', 32 | }; 33 | }, 34 | [sceneAdding]: state => ({ 35 | ...state, 36 | isProcessing: true, 37 | }), 38 | 39 | [sceneAdded]: (state, { payload, payload: { id, config, error = '' } }) => { 40 | const scenes = { 41 | ...state.dictionary, 42 | [id]: config, 43 | }; 44 | return { 45 | ...state, 46 | dictionary: error ? {} : scenes, 47 | list: !error ? [] : convertScenesDictionaryToList(scenes), 48 | isProcessing: false, 49 | error: error ? payload.message : '', 50 | }; 51 | }, 52 | }, 53 | INITIAL_STATE 54 | ); 55 | 56 | export function getScenes() { 57 | return async dispatch => { 58 | dispatch(scenesFetching()); 59 | try { 60 | const response = await scenesProxies.getScenes(); 61 | 62 | if (response.statusText === 'OK') { 63 | const { scenes } = response.data; 64 | const list = convertScenesDictionaryToList(scenes); 65 | dispatch(scenesFetched({ scenes, list })); 66 | } 67 | } catch (error) { 68 | dispatch(scenesFetched(error)); 69 | } 70 | }; 71 | } 72 | 73 | export function addScene(name) { 74 | return async dispatch => { 75 | dispatch(sceneAdding()); 76 | try { 77 | const { data, statusText } = await scenesProxies.addScenes(name); 78 | if (statusText === 'OK') { 79 | dispatch(sceneAdded(data.scene)); 80 | } 81 | } catch (error) { 82 | dispatch(sceneAdded(error)); 83 | } 84 | }; 85 | } 86 | 87 | export function deleteScene(id) { 88 | return async dispatch => { 89 | await scenesProxies.deleteScenes(id); 90 | dispatch(getScenes()); 91 | }; 92 | } 93 | 94 | export function activateScene(id) { 95 | return async dispatch => { 96 | await scenesProxies.activateScenes(id); 97 | }; 98 | } 99 | 100 | export function renameScene(id, name) { 101 | return async dispatch => { 102 | await scenesProxies.renameScene({ id, name }); 103 | dispatch(getScenes()); 104 | }; 105 | } 106 | 107 | const convertScenesDictionaryToList = (scenes = {}) => 108 | Object.keys(scenes).map(key => { 109 | const currentScene = scenes[key]; 110 | return { 111 | ...currentScene, 112 | key, 113 | id: key, 114 | name: currentScene.name, 115 | }; 116 | }); 117 | -------------------------------------------------------------------------------- /frontend/src/modules/schemas.js: -------------------------------------------------------------------------------- 1 | import { createAction, handleActions } from 'redux-actions'; 2 | import * as schemaProxies from 'proxies/schema'; 3 | 4 | // Actions 5 | const ACTION_ROOT = 'schemas'; 6 | export const schemasFetching = createAction(`${ACTION_ROOT}/SCHEMAS_FETCHING`); 7 | export const schemasFetched = createAction(`${ACTION_ROOT}/SCHEMAS_FETCHED`); 8 | 9 | // Reducer 10 | const INITIAL_STATE = { 11 | isLoading: false, 12 | deviceTypes: undefined, 13 | effects: undefined, 14 | }; 15 | 16 | export default handleActions( 17 | { 18 | [schemasFetching]: state => ({ 19 | ...state, 20 | isLoading: true, 21 | }), 22 | [schemasFetched]: (state, { payload, payload: { deviceTypes, effects }, error }) => ({ 23 | ...state, 24 | isLoading: false, 25 | effects: error ? {} : effects, 26 | deviceTypes: error ? {} : deviceTypes, 27 | error: error ? payload.message : '', 28 | }), 29 | }, 30 | INITIAL_STATE 31 | ); 32 | 33 | export function fetchSchemas() { 34 | return async dispatch => { 35 | dispatch(schemasFetching()); 36 | try { 37 | const response = await schemaProxies.getSchemas(); 38 | if (response.statusText === 'OK') { 39 | const { devices: deviceTypes, effects } = response.data; 40 | dispatch(schemasFetched({ deviceTypes, effects })); 41 | } 42 | } catch (error) { 43 | dispatch(schemasFetched(error)); 44 | } 45 | }; 46 | } 47 | -------------------------------------------------------------------------------- /frontend/src/modules/selectedDevice.js: -------------------------------------------------------------------------------- 1 | import { createAction, handleActions } from 'redux-actions'; 2 | import * as deviceProxies from 'proxies/device'; 3 | 4 | // Actions 5 | const ACTION_ROOT = 'selectedDevice'; 6 | 7 | export const deviceRequested = createAction(`${ACTION_ROOT}/DEVICE_REQUESTED`); 8 | export const deviceReceived = createAction(`${ACTION_ROOT}/DEVICE_RECEIVED`); 9 | export const effectRequested = createAction(`${ACTION_ROOT}/DEVICE_EFFECT_REQUESTED`); 10 | export const effectReceived = createAction(`${ACTION_ROOT}/DEVICE_EFFECT_RECEIVED`); 11 | 12 | // Reducer 13 | const INITIAL_STATE = { 14 | isDeviceLoading: false, 15 | device: null, 16 | isEffectLoading: false, 17 | effect: {}, 18 | }; 19 | 20 | export default handleActions( 21 | { 22 | [deviceRequested]: state => ({ 23 | ...state, 24 | isDeviceLoading: true, 25 | }), 26 | [deviceReceived]: (state, { payload, error }) => ({ 27 | ...state, 28 | isDeviceLoading: false, 29 | device: error ? null : payload, 30 | error: error ? payload.message : '', 31 | }), 32 | [effectRequested]: state => ({ 33 | ...state, 34 | isEffectLoading: true, 35 | }), 36 | [effectReceived]: (state, { payload, error }) => ({ 37 | ...state, 38 | isEffectLoading: false, 39 | effect: error ? {} : payload, 40 | error: error ? payload.message : '', 41 | }), 42 | }, 43 | INITIAL_STATE 44 | ); 45 | 46 | export function clearDeviceEffect(deviceId) { 47 | return async dispatch => { 48 | try { 49 | const { 50 | statusText, 51 | data: { effect }, 52 | } = await deviceProxies.deleteDeviceEffect(deviceId); 53 | if (statusText !== 'OK') { 54 | throw new Error(`Error Clearing Device:${deviceId} Effect`); 55 | } 56 | dispatch(effectReceived(effect)); 57 | } catch (error) { 58 | dispatch(effectReceived(error)); 59 | } 60 | }; 61 | } 62 | 63 | export function setDeviceEffect(deviceId, { type, config }) { 64 | return async (dispatch, getState) => { 65 | const currentEffect = getState().selectedDevice.effect; 66 | const proxy = currentEffect.type 67 | ? deviceProxies.updateDeviceEffect 68 | : deviceProxies.setDeviceEffect; 69 | try { 70 | const { 71 | statusText, 72 | data: { effect }, 73 | } = await proxy(deviceId, { 74 | type, 75 | config, 76 | }); 77 | 78 | if (statusText !== 'OK') { 79 | throw new Error(`Error Clearing Device:${deviceId} Effect`); 80 | } 81 | dispatch(effectReceived(effect)); 82 | } catch (error) { 83 | dispatch(effectReceived(error)); 84 | } 85 | }; 86 | } 87 | 88 | export function loadDeviceInfo(deviceId) { 89 | return async (dispatch, getState) => { 90 | try { 91 | let device = getState().devices.dictionary; 92 | dispatch(deviceRequested()); 93 | device = await deviceProxies.getDevice(deviceId); 94 | dispatch(deviceReceived(device)); 95 | 96 | dispatch(effectRequested()); 97 | const { 98 | data: { effect }, 99 | } = await deviceProxies.getDeviceEffect(deviceId); 100 | dispatch(effectReceived(effect)); 101 | } catch (error) { 102 | dispatch(effectReceived(error)); 103 | } 104 | }; 105 | } 106 | -------------------------------------------------------------------------------- /frontend/src/proxies/device.js: -------------------------------------------------------------------------------- 1 | import { api } from 'utils/api'; 2 | 3 | export function scanForDevices() { 4 | return api.post('/find_devices'); 5 | } 6 | 7 | export function getDevices() { 8 | return api.get('/devices'); 9 | } 10 | 11 | export function deleteDevice(deviceId) { 12 | return api.delete(`/devices/${deviceId}`); 13 | } 14 | 15 | export function updateDevice(id, data) { 16 | return api.put(`/devices/${id}`, data); 17 | } 18 | 19 | export function createDevice(config) { 20 | return api.post('/devices', config); 21 | } 22 | 23 | export function getDevice(deviceId) { 24 | return api.get(`/devices/${deviceId}`).then(response => { 25 | const device = response.data; 26 | return { 27 | key: deviceId, 28 | id: deviceId, 29 | name: device.name, 30 | config: device, 31 | }; 32 | }); 33 | } 34 | 35 | export function getDeviceEffect(deviceId) { 36 | return api.get(`devices/${deviceId}/effects`); 37 | } 38 | 39 | export function setDeviceEffect(deviceId, data) { 40 | return api.post(`devices/${deviceId}/effects`, data); 41 | } 42 | 43 | export function updateDeviceEffect(deviceId, data) { 44 | return api.put(`devices/${deviceId}/effects`, data); 45 | } 46 | 47 | export function deleteDeviceEffect(deviceId) { 48 | return api.delete(`devices/${deviceId}/effects`); 49 | } 50 | 51 | export function getDevicePresets(deviceId) { 52 | return api.get(`devices/${deviceId}/presets`); 53 | } 54 | 55 | export function updatePreset(deviceId, data) { 56 | return api.put(`devices/${deviceId}/presets`, data); 57 | } 58 | 59 | export function addPreset(deviceId, data) { 60 | return api.post(`devices/${deviceId}/presets`, data); 61 | } 62 | -------------------------------------------------------------------------------- /frontend/src/proxies/effects.js: -------------------------------------------------------------------------------- 1 | import { api } from 'utils/api'; 2 | 3 | export function getEffectPresets(effectId) { 4 | return api.get(`effects/${effectId}/presets`); 5 | } 6 | 7 | export function updateEffectPreset(effectId, data) { 8 | return api.put(`effects/${effectId}/presets`, data); 9 | } 10 | 11 | export function addEffectPreset(effectId, data) { 12 | return api.post(`effects/${effectId}/presets`, data); 13 | } 14 | -------------------------------------------------------------------------------- /frontend/src/proxies/scenes.js: -------------------------------------------------------------------------------- 1 | import { api } from 'utils/api'; 2 | 3 | export function getScenes() { 4 | return api.get('/scenes'); 5 | } 6 | export function addScenes(name) { 7 | return api.post('/scenes', { name }); 8 | } 9 | 10 | export function deleteScenes(id) { 11 | return api.delete('/scenes', { data: { id } }); 12 | } 13 | 14 | export function activateScenes(id) { 15 | return api.put('/scenes', { 16 | id, 17 | action: 'activate', 18 | }); 19 | } 20 | export function renameScene({ id, name }) { 21 | return api.put('/scenes', { 22 | action: 'rename', 23 | id, 24 | name, 25 | }); 26 | } 27 | -------------------------------------------------------------------------------- /frontend/src/proxies/schema.js: -------------------------------------------------------------------------------- 1 | import { api } from 'utils/api'; 2 | 3 | export function getSchemas() { 4 | return api.get('/schema'); 5 | } -------------------------------------------------------------------------------- /frontend/src/proxies/settings.js: -------------------------------------------------------------------------------- 1 | import { api } from 'utils/api'; 2 | 3 | export function getSystemConfig() { 4 | return api.get('/config'); 5 | } 6 | 7 | export function getAudioInputs() { 8 | return api.get('/audio/devices'); 9 | } 10 | export function updateSelectedAudioInput(data) { 11 | return api.put('/audio/devices', data); 12 | } 13 | -------------------------------------------------------------------------------- /frontend/src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /frontend/src/routes/index.jsx: -------------------------------------------------------------------------------- 1 | import DefaultLayout from "../layouts/Default"; 2 | 3 | const indexRoutes = [{ path: "/", component: DefaultLayout }]; 4 | 5 | export default indexRoutes; 6 | -------------------------------------------------------------------------------- /frontend/src/routes/views.jsx: -------------------------------------------------------------------------------- 1 | // Icons 2 | import Dashboard from "@material-ui/icons/Dashboard"; 3 | import List from "@material-ui/icons/List"; 4 | import Settings from "@material-ui/icons/Settings"; 5 | import Tune from "@material-ui/icons/Tune"; 6 | import SaveAltIcon from '@material-ui/icons/SaveAlt'; 7 | import BuildIcon from '@material-ui/icons/Build'; 8 | 9 | // Components and Views 10 | import DashboardView from "../views/Dashboard"; 11 | import DevicesView from "../views/Devices"; 12 | import ScenesView from "../views/Scenes"; 13 | import DeviceView from "../views/Device"; 14 | import SettingsView from "../views/Settings"; 15 | import DeveloperView from "../views/Developer"; 16 | 17 | const viewRoutes = [ 18 | { 19 | path: "/dashboard", 20 | sidebarName: "Dashboard", 21 | navbarName: "Dashboard", 22 | icon: Dashboard, 23 | component: DashboardView 24 | }, 25 | { 26 | path: "/devices/:deviceId", 27 | navbarName: "Devices", 28 | sidebarName: "Devices", 29 | icon: List, 30 | component: DeviceView, 31 | }, 32 | { 33 | path: "/scenes", 34 | sidebarName: "Scenes Management", 35 | navbarName: "Scenes Management", 36 | icon: SaveAltIcon, 37 | component: ScenesView, 38 | }, 39 | { 40 | path: "/devices", 41 | sidebarName: "Device Management", 42 | navbarName: "Device Management", 43 | icon: Settings, 44 | component: DevicesView 45 | }, 46 | { 47 | path: "/settings", 48 | sidebarName: "Settings", 49 | navbarName: "Settings", 50 | icon: BuildIcon, 51 | component: SettingsView 52 | }, 53 | { 54 | path: "/developer/:graphString", 55 | navbarName: "Developer", 56 | component: DeveloperView 57 | }, 58 | { 59 | path: "/developer/melbank", 60 | sidebarName: "Developer", 61 | navbarName: "Developer", 62 | icon: Tune, 63 | component: DeveloperView 64 | }, 65 | { redirect: true, path: "/", to: "/dashboard", navbarName: "Redirect" } 66 | ]; 67 | 68 | export default viewRoutes; 69 | 70 | -------------------------------------------------------------------------------- /frontend/src/setupTests.js: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom/extend-expect'; 6 | -------------------------------------------------------------------------------- /frontend/src/utils/api/index.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | 3 | const baseURL = '/api'; 4 | 5 | export const api = axios.create({ 6 | baseURL, 7 | }); 8 | -------------------------------------------------------------------------------- /frontend/src/utils/api/websocket.js: -------------------------------------------------------------------------------- 1 | import Sockette from 'sockette'; 2 | 3 | const { NODE_ENV } = process.env; 4 | const { hostname, port } = window.location; 5 | 6 | const wsBaseUrl = NODE_ENV === 'development' ? 'localhost:8888' : `${hostname}:${port}`; 7 | 8 | export const websocketUrl = `ws://${wsBaseUrl}/api/websocket`; 9 | 10 | export const createWebSocket = options => 11 | new Sockette(websocketUrl, { 12 | timeout: 5e3, 13 | maxAttempts: 10, 14 | ...options, 15 | }); 16 | -------------------------------------------------------------------------------- /frontend/src/utils/helpers.js: -------------------------------------------------------------------------------- 1 | export const includeKeyInObject = (key, object) => ({ id: key, ...object}) 2 | 3 | export const mapIncludeKey = (scenes) => { 4 | const keys = Object.keys(scenes) 5 | return keys.map((k) => (includeKeyInObject(k, scenes[k]))) 6 | } 7 | 8 | export const convertDictionaryToList = (presets = {}) => 9 | Object.keys(presets).map(key => { 10 | const currentScene = presets[key]; 11 | return { 12 | ...currentScene, 13 | id: key, 14 | }; 15 | }); -------------------------------------------------------------------------------- /frontend/src/utils/style.js: -------------------------------------------------------------------------------- 1 | export const drawerWidth = 260; 2 | -------------------------------------------------------------------------------- /frontend/src/views/Dashboard/index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { connect } from 'react-redux'; 4 | import withStyles from '@material-ui/core/styles/withStyles'; 5 | import Card from '@material-ui/core/Card'; 6 | import CardContent from '@material-ui/core/CardContent'; 7 | import Grid from '@material-ui/core/Grid'; 8 | 9 | import PixelColorGraph from 'components/PixelColorGraph'; 10 | import DeviceMiniControl from 'components/DeviceMiniControl'; 11 | import AddSceneCard from 'components/AddSceneCard'; 12 | import MiniScenesCard from 'components/MiniScenesCard'; 13 | import { addScene, getScenes, activateScene } from 'modules/scenes'; 14 | import { setDeviceEffect, clearDeviceEffect, fetchDeviceList } from 'modules/devices'; 15 | 16 | const styles = theme => ({ 17 | root: { 18 | flexGrow: 1, 19 | }, 20 | card: { 21 | width: '100%', 22 | overflowX: 'auto', 23 | }, 24 | table: { 25 | width: '100%', 26 | maxWidth: '100%', 27 | backgroundColor: 'transparent', 28 | borderSpacing: '0', 29 | }, 30 | }); 31 | 32 | class DashboardView extends React.Component { 33 | componentDidMount() { 34 | this.props.getScenes(); 35 | this.props.fetchDeviceList(); 36 | } 37 | 38 | handleUpdateDeviceEffect = (deviceId, data) => { 39 | const { setDeviceEffect, clearDeviceEffect } = this.props; 40 | 41 | if (data.active) { 42 | setDeviceEffect(deviceId, data); 43 | } else { 44 | clearDeviceEffect(deviceId); 45 | } 46 | }; 47 | 48 | render() { 49 | const { classes, devices, scenes, addScene, activateScene } = this.props; 50 | 51 | if (!devices.list.length) { 52 | return ( 53 | 54 | 55 |

Looks like you have no devices! Go to 'Device Management' to add them

56 |
57 |
58 | ); 59 | } 60 | 61 | return ( 62 |
63 | 64 | {devices.list.map(device => { 65 | return ( 66 | 67 | 68 | 69 | 73 | 74 | 75 | 76 | 77 | ); 78 | })} 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 |
89 | ); 90 | } 91 | } 92 | 93 | DashboardView.propTypes = { 94 | devices: PropTypes.object.isRequired, 95 | scenes: PropTypes.object.isRequired, 96 | }; 97 | 98 | export default connect( 99 | state => ({ 100 | devices: state.devices, 101 | scenes: state.scenes, 102 | }), 103 | { addScene, activateScene, getScenes, setDeviceEffect, clearDeviceEffect, fetchDeviceList } 104 | )(withStyles(styles)(DashboardView)); 105 | -------------------------------------------------------------------------------- /frontend/src/views/Developer/index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import Grid from '@material-ui/core/Grid'; 4 | 5 | import MelbankGraph from 'components/MelbankGraph'; 6 | 7 | class DeveloperView extends React.Component { 8 | render() { 9 | const { graphString } = this.props.match.params; 10 | 11 | let graphList = graphString.split('+'); 12 | let graphDom = Object.keys(graphList).map(graphIndex => { 13 | return ( 14 | 15 |

{graphList[graphIndex].replace(/^\w/, c => c.toUpperCase())} Graph

16 | 17 |
18 | ); 19 | }); 20 | 21 | return ( 22 | 23 | {graphDom} 24 | 25 | ); 26 | } 27 | } 28 | 29 | export default DeveloperView; 30 | -------------------------------------------------------------------------------- /frontend/src/views/Scenes/index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { connect } from 'react-redux'; 3 | 4 | import withStyles from '@material-ui/core/styles/withStyles'; 5 | import Grid from '@material-ui/core/Grid'; 6 | import CircularProgress from '@material-ui/core/CircularProgress'; 7 | 8 | import ScenesCard from 'components/SceneCard'; 9 | import AddSceneCard from 'components/AddSceneCard'; 10 | import { getScenes, addScene, activateScene, deleteScene } from 'modules/scenes'; 11 | 12 | const styles = theme => ({ 13 | cardResponsive: { 14 | width: '100%', 15 | overflowX: 'auto', 16 | }, 17 | button: { 18 | position: 'absolute', 19 | bottom: theme.spacing(2), 20 | right: theme.spacing(2), 21 | }, 22 | dialogButton: { 23 | float: 'right', 24 | }, 25 | spinnerContainer: { 26 | height: '10rem', 27 | }, 28 | }); 29 | 30 | class ScenesView extends React.Component { 31 | componentDidMount() { 32 | this.props.getScenes(); 33 | } 34 | 35 | handleAddScene = name => { 36 | const { addScene } = this.props; 37 | addScene(name); 38 | }; 39 | 40 | render() { 41 | const { scenes, classes, deleteScene, activateScene } = this.props; 42 | 43 | return ( 44 | 45 | 46 | 47 | 48 | {scenes.isLoading ? ( 49 | 55 | 56 | 57 | ) : ( 58 | scenes.list.map(scene => ( 59 | 60 | 66 | 67 | )) 68 | )} 69 | 70 | ); 71 | } 72 | } 73 | 74 | export default connect( 75 | state => ({ 76 | scenes: state.scenes, 77 | }), 78 | { getScenes, addScene, activateScene, deleteScene } 79 | )(withStyles(styles)(ScenesView)); 80 | -------------------------------------------------------------------------------- /frontend/src/views/Settings/AudioInput.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { makeStyles } from '@material-ui/core/styles'; 3 | import Card from '@material-ui/core/Card'; 4 | import CardHeader from '@material-ui/core/CardHeader'; 5 | import CardContent from '@material-ui/core/CardContent'; 6 | import MenuItem from '@material-ui/core/MenuItem'; 7 | import FormControl from '@material-ui/core/FormControl'; 8 | import Select from '@material-ui/core/Select'; 9 | import FormHelperText from '@material-ui/core/FormHelperText'; 10 | import InputLabel from '@material-ui/core/InputLabel'; 11 | 12 | const useStyles = makeStyles({ 13 | form: { 14 | display: 'flex', 15 | }, 16 | }); 17 | 18 | const AudioCard = ({ options, value, onChange, error, isSaving, isLoading }) => { 19 | const classes = useStyles(); 20 | const handleAudioSelected = e => { 21 | const { value } = e.target; 22 | const selectedOption = options.find(o => o.value === value) || {}; 23 | onChange(selectedOption); 24 | }; 25 | 26 | return ( 27 | 28 | 29 | 30 | 31 | Current Device 32 | 44 | {error} 45 | 46 | 47 | 48 | ); 49 | }; 50 | 51 | export default AudioCard; 52 | -------------------------------------------------------------------------------- /frontend/src/views/Settings/ConfigCard.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { makeStyles } from '@material-ui/core/styles'; 3 | import Card from '@material-ui/core/Card'; 4 | import CardHeader from '@material-ui/core/CardHeader'; 5 | import CardContent from '@material-ui/core/CardContent'; 6 | import FormHelperText from '@material-ui/core/FormHelperText'; 7 | import InputLabel from '@material-ui/core/InputLabel'; 8 | import TextField from '@material-ui/core/TextField'; 9 | 10 | const useStyles = makeStyles({ 11 | content: { 12 | display: 'flex', 13 | flexDirection: 'column', 14 | }, 15 | }); 16 | 17 | const ConfigCard = ({ settings, error }) => { 18 | const classes = useStyles(); 19 | return ( 20 | 21 | 22 | 23 | Host 24 | 25 | Port 26 | 27 | DevMode 28 | 29 | {error && {error}} 30 | 31 | 32 | ); 33 | }; 34 | 35 | export default ConfigCard; 36 | -------------------------------------------------------------------------------- /frontend/src/views/Settings/index.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { withStyles } from '@material-ui/core/styles'; 4 | import Grid from '@material-ui/core/Grid'; 5 | 6 | import { getAudioInputs, setAudioInput } from 'modules/settings'; 7 | 8 | import AudioInputCard from './AudioInput'; 9 | import ConfigCard from './ConfigCard'; 10 | 11 | const styles = theme => ({}); 12 | 13 | class SettingsView extends Component { 14 | componentDidMount() { 15 | this.props.getAudioInputs(); 16 | } 17 | 18 | render() { 19 | const { setAudioInput, settings } = this.props; 20 | const { audioInputs } = settings; 21 | 22 | return ( 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | ); 32 | } 33 | } 34 | 35 | export default connect( 36 | state => ({ 37 | settings: state.settings, 38 | }), 39 | { 40 | getAudioInputs, 41 | setAudioInput, 42 | } 43 | )(withStyles(styles)(SettingsView)); 44 | -------------------------------------------------------------------------------- /frontend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["dom", "dom.iterable", "esnext"], 4 | "baseUrl": "src", 5 | "esModuleInterop": true, 6 | "strict": true, 7 | "forceConsistentCasingInFileNames": true, 8 | "module": "esnext", 9 | "resolveJsonModule": true, 10 | "isolatedModules": true, 11 | "skipLibCheck": true, 12 | "allowJs": true, 13 | "allowSyntheticDefaultImports": true, 14 | "experimentalDecorators": true, 15 | "jsx": "react", 16 | "moduleResolution": "node", 17 | "noEmit": true, 18 | // "noUnusedLocals": true, 19 | // "noUnusedParameters": true, 20 | "target": "esnext", 21 | "noImplicitAny": true, 22 | "noImplicitThis": true, 23 | "strictNullChecks": true 24 | }, 25 | "include": ["src"], 26 | "exclude": ["node_modules"] 27 | } 28 | -------------------------------------------------------------------------------- /icons/demo_setup.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Mattallmighty/LedFx-OLD/9734e7ebf20e9a4b67aca32f932572003badca4d/icons/demo_setup.png -------------------------------------------------------------------------------- /icons/discord.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Mattallmighty/LedFx-OLD/9734e7ebf20e9a4b67aca32f932572003badca4d/icons/discord.ico -------------------------------------------------------------------------------- /icons/discord.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Mattallmighty/LedFx-OLD/9734e7ebf20e9a4b67aca32f932572003badca4d/icons/discord.png -------------------------------------------------------------------------------- /icons/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Mattallmighty/LedFx-OLD/9734e7ebf20e9a4b67aca32f932572003badca4d/icons/favicon.ico -------------------------------------------------------------------------------- /icons/insta.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Mattallmighty/LedFx-OLD/9734e7ebf20e9a4b67aca32f932572003badca4d/icons/insta.png -------------------------------------------------------------------------------- /icons/large_black_alpha.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Mattallmighty/LedFx-OLD/9734e7ebf20e9a4b67aca32f932572003badca4d/icons/large_black_alpha.png -------------------------------------------------------------------------------- /icons/large_white_alpha.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Mattallmighty/LedFx-OLD/9734e7ebf20e9a4b67aca32f932572003badca4d/icons/large_white_alpha.png -------------------------------------------------------------------------------- /ledfx/__init__.py: -------------------------------------------------------------------------------- 1 | """ledfx""" -------------------------------------------------------------------------------- /ledfx/api/__init__.py: -------------------------------------------------------------------------------- 1 | from ledfx.utils import BaseRegistry, RegistryLoader 2 | from aiohttp import web 3 | import logging 4 | import inspect 5 | import json 6 | 7 | @BaseRegistry.no_registration 8 | class RestEndpoint(BaseRegistry): 9 | 10 | def __init__(self, ledfx): 11 | self._ledfx = ledfx 12 | 13 | async def handler(self, request: web.Request): 14 | method = getattr(self, request.method.lower(), None) 15 | if not method: 16 | raise web.HTTPMethodNotAllowed('') 17 | 18 | wanted_args = list(inspect.signature(method).parameters.keys()) 19 | available_args = request.match_info.copy() 20 | available_args.update({'request': request}) 21 | 22 | unsatisfied_args = set(wanted_args) - set(available_args.keys()) 23 | if unsatisfied_args: 24 | raise web.HttpBadRequest('') 25 | 26 | return await method(**{arg_name: available_args[arg_name] for arg_name in wanted_args}) 27 | 28 | class RestApi(RegistryLoader): 29 | 30 | PACKAGE_NAME = 'ledfx.api' 31 | 32 | def __init__(self, ledfx): 33 | super().__init__(ledfx, RestEndpoint, self.PACKAGE_NAME) 34 | self._ledfx = ledfx 35 | 36 | def register_routes(self, app): 37 | 38 | # Create the endpoints and register their routes 39 | for endpoint_type in self.types(): 40 | endpoint = self.create(type = endpoint_type, ledfx = self._ledfx) 41 | app.router.add_route('*', endpoint.ENDPOINT_PATH, endpoint.handler, 42 | name = "api_{}".format(endpoint_type)) -------------------------------------------------------------------------------- /ledfx/api/audio_devices.py: -------------------------------------------------------------------------------- 1 | from ledfx.api import RestEndpoint 2 | from ledfx.config import save_config 3 | from aiohttp import web 4 | import logging 5 | import json 6 | import pyaudio 7 | 8 | _LOGGER = logging.getLogger(__name__) 9 | 10 | class AudioDevicesEndpoint(RestEndpoint): 11 | 12 | ENDPOINT_PATH = "/api/audio/devices" 13 | 14 | _audio = None 15 | 16 | async def get(self) -> web.Response: 17 | """Get list of audio devices and active audio device WIP""" 18 | 19 | if self._audio is None: 20 | self._audio = pyaudio.PyAudio() 21 | 22 | info = self._audio.get_host_api_info_by_index(0) 23 | audio_config = self._ledfx.config.get('audio', {'device_index': 0}) 24 | 25 | audio_devices = {} 26 | audio_devices['devices'] = {} 27 | audio_devices['active_device_index'] = audio_config['device_index'] 28 | 29 | for i in range(0, info.get('deviceCount')): 30 | device_info = self._audio.get_device_info_by_host_api_device_index(0, i) 31 | if (device_info.get('maxInputChannels')) > 0: 32 | audio_devices['devices'][i] = device_info.get('name') 33 | 34 | return web.json_response(data=audio_devices, status=200) 35 | 36 | async def put(self, request) -> web.Response: 37 | """Set audio device to use as input. Requires restart for changes to take effect""" 38 | data = await request.json() 39 | index = data.get('index') 40 | 41 | info = self._audio.get_host_api_info_by_index(0) 42 | if index is None: 43 | response = { 'status' : 'failed', 'reason': 'Required attribute "index" was not provided' } 44 | return web.json_response(data=response, status=500) 45 | 46 | if index not in range(0, info.get('deviceCount')): 47 | response = { 'status' : 'failed', 'reason': 'Invalid device index [{}]'.format(index) } 48 | return web.json_response(data=response, status=500) 49 | 50 | # Update and save config 51 | new_config = self._ledfx.config.get('audio', {}) 52 | new_config['device_index'] = int(index) 53 | self._ledfx.config['audio'] = new_config 54 | 55 | save_config( 56 | config = self._ledfx.config, 57 | config_dir = self._ledfx.config_dir) 58 | 59 | if self._ledfx.audio: 60 | self._ledfx.audio.update_config(new_config) 61 | 62 | response = { 'status': 'success' } 63 | return web.json_response(data=response, status=200) 64 | -------------------------------------------------------------------------------- /ledfx/api/config.py: -------------------------------------------------------------------------------- 1 | from ledfx.api import RestEndpoint 2 | from aiohttp import web 3 | import logging 4 | import json 5 | 6 | _LOGGER = logging.getLogger(__name__) 7 | 8 | class ConfigEndpoint(RestEndpoint): 9 | 10 | ENDPOINT_PATH = "/api/config" 11 | 12 | async def get(self) -> web.Response: 13 | response = { 14 | 'config': self._ledfx.config 15 | } 16 | 17 | return web.json_response(data=response, status=200) 18 | -------------------------------------------------------------------------------- /ledfx/api/device.py: -------------------------------------------------------------------------------- 1 | from ledfx.config import save_config 2 | from ledfx.api import RestEndpoint 3 | from aiohttp import web 4 | import logging 5 | import json 6 | 7 | _LOGGER = logging.getLogger(__name__) 8 | 9 | class DeviceEndpoint(RestEndpoint): 10 | """REST end-point for querying and managing devices""" 11 | 12 | ENDPOINT_PATH = "/api/devices/{device_id}" 13 | 14 | async def get(self, device_id) -> web.Response: 15 | device = self._ledfx.devices.get(device_id) 16 | if device is None: 17 | response = { 'not found': 404 } 18 | return web.json_response(data=response, status=404) 19 | 20 | response = device.config 21 | return web.json_response(data=response, status=200) 22 | 23 | async def put(self, device_id, request) -> web.Response: 24 | device = self._ledfx.devices.get(device_id) 25 | if device is None: 26 | response = { 'not found': 404 } 27 | return web.json_response(data=response, status=404) 28 | 29 | data = await request.json() 30 | device_config = data.get('config') 31 | if device_config is None: 32 | response = { 'status' : 'failed', 'reason': 'Required attribute "config" was not provided' } 33 | return web.json_response(data=response, status=500) 34 | 35 | # TODO: Support dynamic device configuration updates. For now 36 | # remove the device and re-create it 37 | _LOGGER.info(("Updating device {} with config {}").format( 38 | device_id, device_config)) 39 | self._ledfx.devices.destroy(device_id) 40 | 41 | device = self._ledfx.devices.create( 42 | id = device_id, 43 | type = device_config.get('type'), 44 | config = device_config, 45 | ledfx = self._ledfx) 46 | 47 | # Update and save the configuration 48 | for device in self._ledfx.config['devices']: 49 | if (device['id'] == device_id): 50 | device['config'] = device_config 51 | break 52 | save_config( 53 | config = self._ledfx.config, 54 | config_dir = self._ledfx.config_dir) 55 | 56 | response = { 'status' : 'success' } 57 | return web.json_response(data=response, status=200) 58 | 59 | async def delete(self, device_id) -> web.Response: 60 | device = self._ledfx.devices.get(device_id) 61 | if device is None: 62 | response = { 'not found': 404 } 63 | return web.json_response(data=response, status=404) 64 | 65 | device.clear_effect() 66 | self._ledfx.devices.destroy(device_id) 67 | 68 | # Update and save the configuration 69 | self._ledfx.config['devices'] = [device for device in self._ledfx.config['devices'] if device['id'] != device_id] 70 | save_config( 71 | config = self._ledfx.config, 72 | config_dir = self._ledfx.config_dir) 73 | 74 | response = { 'status' : 'success' } 75 | return web.json_response(data=response, status=200) -------------------------------------------------------------------------------- /ledfx/api/devices.py: -------------------------------------------------------------------------------- 1 | from ledfx.config import save_config 2 | from ledfx.api import RestEndpoint 3 | from ledfx.utils import generate_id 4 | from aiohttp import web 5 | import logging 6 | import json 7 | 8 | _LOGGER = logging.getLogger(__name__) 9 | 10 | class DevicesEndpoint(RestEndpoint): 11 | """REST end-point for querying and managing devices""" 12 | 13 | ENDPOINT_PATH = "/api/devices" 14 | 15 | async def get(self) -> web.Response: 16 | response = { 'status' : 'success' , 'devices' : {}} 17 | for device in self._ledfx.devices.values(): 18 | response['devices'][device.id] = { 'config': device.config, 'id': device.id, 'type': device.type, 'effect' : {} } 19 | if device.active_effect: 20 | effect_response = {} 21 | effect_response['config'] = device.active_effect.config 22 | effect_response['name'] = device.active_effect.name 23 | effect_response['type'] = device.active_effect.type 24 | response['devices'][device.id]['effect'] = effect_response 25 | 26 | return web.json_response(data=response, status=200) 27 | 28 | async def post(self, request) -> web.Response: 29 | data = await request.json() 30 | 31 | device_config = data.get('config') 32 | if device_config is None: 33 | response = { 'status' : 'failed', 'reason': 'Required attribute "config" was not provided' } 34 | return web.json_response(data=response, status=500) 35 | 36 | device_type = data.get('type') 37 | if device_type is None: 38 | response = { 'status' : 'failed', 'reason': 'Required attribute "type" was not provided' } 39 | return web.json_response(data=response, status=500) 40 | 41 | device_id = generate_id(device_config.get('name')) 42 | # Remove the device it if already exist? 43 | 44 | # Create the device 45 | _LOGGER.info("Adding device of type {} with config {}".format(device_type, device_config)) 46 | device = self._ledfx.devices.create( 47 | id = device_id, 48 | type = device_type, 49 | config = device_config, 50 | ledfx = self._ledfx) 51 | 52 | # Update and save the configuration 53 | self._ledfx.config['devices'].append({'id': device.id, 'type': device.type, 'config': device.config }) 54 | save_config( 55 | config = self._ledfx.config, 56 | config_dir = self._ledfx.config_dir) 57 | 58 | response = { 'status' : 'success', 'device': { 'type': device.type, 'config': device.config, 'id': device.id }} 59 | return web.json_response(data=response, status=200) -------------------------------------------------------------------------------- /ledfx/api/effect.py: -------------------------------------------------------------------------------- 1 | from ledfx.api import RestEndpoint 2 | from aiohttp import web 3 | import logging 4 | import json 5 | 6 | _LOGGER = logging.getLogger(__name__) 7 | 8 | class EffectEndpoint(RestEndpoint): 9 | 10 | ENDPOINT_PATH = "/api/effects/{effect_id}" 11 | 12 | async def get(self, effect_id) -> web.Response: 13 | effect = self._ledfx.effects.get_class(effect_id) 14 | if effect is None: 15 | response = { 'not found': 404 } 16 | return web.json_response(data=response, status=404) 17 | 18 | response = { 'schema' : str(effect.schema()) } 19 | return web.json_response(data=response, status=200) 20 | 21 | -------------------------------------------------------------------------------- /ledfx/api/effects.py: -------------------------------------------------------------------------------- 1 | from ledfx.api import RestEndpoint 2 | from aiohttp import web 3 | import logging 4 | import json 5 | 6 | _LOGGER = logging.getLogger(__name__) 7 | 8 | class EffectsEndpoint(RestEndpoint): 9 | 10 | ENDPOINT_PATH = "/api/effects" 11 | 12 | async def get(self) -> web.Response: 13 | response = {} 14 | for device in self._ledfx.devices.values(): 15 | if device.active_effect: 16 | response[device.active_effect.type] = device.active_effect.config 17 | 18 | return web.json_response(data=response, status=200) 19 | -------------------------------------------------------------------------------- /ledfx/api/find_devices.py: -------------------------------------------------------------------------------- 1 | from ledfx.config import save_config 2 | from ledfx.api import RestEndpoint 3 | from ledfx.utils import generate_id, async_fire_and_forget 4 | from aiohttp import web 5 | import logging 6 | import json 7 | 8 | _LOGGER = logging.getLogger(__name__) 9 | 10 | class FindDevicesEndpoint(RestEndpoint): 11 | """REST end-point for detecting and adding wled devices""" 12 | 13 | ENDPOINT_PATH = "/api/find_devices" 14 | 15 | async def post(self) -> web.Response: 16 | """ Find and add all WLED devices on the LAN """ 17 | async_fire_and_forget(self._ledfx.devices.find_wled_devices(), self._ledfx.loop) 18 | 19 | response = { 'status' : 'success' } 20 | return web.json_response(data=response, status=200) -------------------------------------------------------------------------------- /ledfx/api/graphics_quality.py: -------------------------------------------------------------------------------- 1 | from ledfx.api import RestEndpoint 2 | from ledfx.config import save_config 3 | from aiohttp import web 4 | import logging 5 | import json 6 | 7 | _LOGGER = logging.getLogger(__name__) 8 | 9 | class GraphicsQualityEndpoint(RestEndpoint): 10 | 11 | ENDPOINT_PATH = "/api/graphics_quality" 12 | 13 | async def get(self) -> web.Response: 14 | """Get graphics quality setting""" 15 | 16 | response = { "graphics_quality" : self._ledfx.config.get('graphics_quality')} 17 | 18 | return web.json_response(data=response, status=200) 19 | 20 | async def put(self, request) -> web.Response: 21 | """Set graphics quality setting""" 22 | data = await request.json() 23 | graphics_quality = data.get('graphics_quality') 24 | 25 | if graphics_quality is None: 26 | response = { 'status' : 'failed', 'reason': 'Required attribute "graphics_quality" was not provided' } 27 | return web.json_response(data=response, status=500) 28 | 29 | if graphics_quality not in ["low", "medium", "high", "ultra"]: 30 | response = { 'status' : 'failed', 'reason': 'Invalid graphics_quality [{}]'.format(graphics_quality) } 31 | return web.json_response(data=response, status=500) 32 | 33 | # Update and save config 34 | self._ledfx.config['graphics_quality'] = graphics_quality 35 | 36 | save_config( 37 | config = self._ledfx.config, 38 | config_dir = self._ledfx.config_dir) 39 | 40 | # reopen all websockets with new graphics settings 41 | 42 | response = { 'status': 'success' } 43 | return web.json_response(data=response, status=200) 44 | -------------------------------------------------------------------------------- /ledfx/api/info.py: -------------------------------------------------------------------------------- 1 | from ledfx.api import RestEndpoint 2 | from ledfx.http import HttpServer 3 | from ledfx.consts import PROJECT_VERSION 4 | from aiohttp import web 5 | import logging 6 | import json 7 | 8 | _LOGGER = logging.getLogger(__name__) 9 | 10 | class InfoEndpoint(RestEndpoint): 11 | 12 | ENDPOINT_PATH = "/api/info" 13 | 14 | async def get(self) -> web.Response: 15 | response = { 16 | 'url': self._ledfx.http.base_url, 17 | 'name': 'LedFx Controller', 18 | 'version': PROJECT_VERSION, 19 | 'debug_mode': True 20 | } 21 | 22 | return web.json_response(data=response, status=200) 23 | -------------------------------------------------------------------------------- /ledfx/api/schema.py: -------------------------------------------------------------------------------- 1 | from ledfx.api import RestEndpoint 2 | from aiohttp import web 3 | from ledfx.api.utils import convertToJsonSchema 4 | import logging 5 | import json 6 | 7 | _LOGGER = logging.getLogger(__name__) 8 | 9 | class SchemaEndpoint(RestEndpoint): 10 | 11 | ENDPOINT_PATH = "/api/schema" 12 | 13 | async def get(self) -> web.Response: 14 | response = { 15 | 'devices': {}, 16 | 'effects': {} 17 | } 18 | 19 | # Generate all the device schema 20 | for device_type, device in self._ledfx.devices.classes().items(): 21 | response['devices'][device_type] = { 22 | 'schema': convertToJsonSchema(device.schema()), 23 | 'id': device_type 24 | } 25 | 26 | # Generate all the effect schema 27 | for effect_type, effect in self._ledfx.effects.classes().items(): 28 | response['effects'][effect_type] = { 29 | 'schema': convertToJsonSchema(effect.schema()), 30 | 'id': effect_type, 31 | 'name': effect.NAME 32 | } 33 | 34 | return web.json_response(data=response, status=200) -------------------------------------------------------------------------------- /ledfx/api/schema_types.py: -------------------------------------------------------------------------------- 1 | from ledfx.api import RestEndpoint 2 | from aiohttp import web 3 | from ledfx.api.utils import convertToJsonSchema 4 | import logging 5 | import json 6 | 7 | _LOGGER = logging.getLogger(__name__) 8 | 9 | class SchemaEndpoint(RestEndpoint): 10 | 11 | ENDPOINT_PATH = "/api/schema/{schema_type}" 12 | async def get(self, schema_type) -> web.Response: 13 | response = {} 14 | if schema_type == 'devices': 15 | # Generate all the device schema 16 | for device_type, device in self._ledfx.devices.classes().items(): 17 | response[device_type] = { 18 | 'schema': convertToJsonSchema(device.schema()), 19 | 'id': device_type 20 | } 21 | 22 | return web.json_response(data=response, status=200) 23 | elif schema_type == 'effects': 24 | # Generate all the effect schema 25 | for effect_type, effect in self._ledfx.effects.classes().items(): 26 | response[effect_type] = { 27 | 'schema': convertToJsonSchema(effect.schema()), 28 | 'id': effect_type, 29 | 'name': effect.NAME 30 | } 31 | 32 | return web.json_response(data=response, status=200) 33 | else: 34 | return web.json_response(data=response, status=200) -------------------------------------------------------------------------------- /ledfx/api/utils.py: -------------------------------------------------------------------------------- 1 | """Module to convert voluptuous schemas to dictionaries.""" 2 | import collections 3 | import json 4 | import re 5 | import voluptuous as vol 6 | from ledfx.utils import generate_title 7 | 8 | TYPES_MAP = { 9 | int: 'integer', 10 | str: 'string', 11 | float: 'number', 12 | bool: 'boolean', 13 | list: 'array' 14 | } 15 | 16 | def createRegistrySchema(registry): 17 | """Create a JSON Schema for an entire registry.""" 18 | 19 | class_schema_list = [] 20 | for class_type, class_obj in registry.classes().items(): 21 | obj_schema = convertToJsonSchema(class_obj.schema()) 22 | obj_schema['properties']['registry_type'] = {"enum": [class_type]} 23 | class_schema_list.append(obj_schema) 24 | 25 | return { 26 | "type": "object", 27 | "properties": { 28 | "registry_type": { 29 | "title": "Registry Type", 30 | "type": "string", 31 | "enum": list(registry.classes().keys()) 32 | } 33 | }, 34 | "required": ["registry_type"], 35 | "dependencies": { 36 | "registry_type": { 37 | "oneOf": class_schema_list 38 | } 39 | } 40 | } 41 | 42 | def convertToJsonSchema(schema): 43 | """ 44 | Converts a voluptuous schema to a JSON schema compatible 45 | with the schema REST API. This should be kept in line with 46 | the frontends "SchemaForm" component. 47 | """ 48 | if isinstance(schema, vol.Schema): 49 | schema = schema.schema 50 | 51 | if isinstance(schema, collections.abc.Mapping): 52 | val = {'properties': {}} 53 | required_vals = [] 54 | 55 | for key, value in schema.items(): 56 | description = None 57 | if isinstance(key, vol.Marker): 58 | pkey = key.schema 59 | description = key.description 60 | else: 61 | pkey = key 62 | 63 | pval = convertToJsonSchema(value) 64 | pval['title'] = generate_title(pkey) 65 | if description is not None: 66 | pval['description'] = description 67 | 68 | if key.default is not vol.UNDEFINED: 69 | pval['default'] = key.default() 70 | 71 | if isinstance(key, vol.Required): 72 | required_vals.append(pkey) 73 | 74 | val['properties'][pkey] = pval 75 | 76 | if required_vals: 77 | val['required'] = required_vals 78 | 79 | return val 80 | 81 | if isinstance(schema, vol.All): 82 | val = {} 83 | for validator in schema.validators: 84 | val.update(convertToJsonSchema(validator)) 85 | return val 86 | 87 | 88 | elif isinstance(schema, vol.Length): 89 | val = {} 90 | if schema.min is not None: 91 | val['minLength'] = schema.min 92 | if schema.max is not None: 93 | val['maxLength'] = schema.max 94 | return val 95 | 96 | elif isinstance(schema, (vol.Clamp, vol.Range)): 97 | val = {} 98 | if schema.min is not None: 99 | val['minimum'] = schema.min 100 | if schema.max is not None: 101 | val['maximum'] = schema.max 102 | return val 103 | 104 | elif isinstance(schema, vol.Datetime): 105 | return { 106 | 'type': 'datetime', 107 | 'format': schema.format, 108 | } 109 | 110 | elif isinstance(schema, vol.In): 111 | return {'type': 'string', 'enum': list(schema.container)} 112 | # val = {'type': 'string', 'enum': dict()} 113 | # for item in schema.container: 114 | # val['enum'][item] = item 115 | # return val 116 | 117 | elif isinstance(schema, vol.Coerce): 118 | schema = schema.type 119 | 120 | if schema in TYPES_MAP: 121 | return {'type': TYPES_MAP[schema]} 122 | 123 | raise ValueError('Unable to convert schema: {}'.format(schema)) -------------------------------------------------------------------------------- /ledfx/color.py: -------------------------------------------------------------------------------- 1 | from collections import namedtuple 2 | 3 | RGB = namedtuple('RGB','red, green, blue') 4 | 5 | COLORS = { 6 | 'red': RGB(255, 0, 0), 7 | 'orange-deep': RGB(255, 40, 0), 8 | 'orange': RGB(255, 120, 0), 9 | 'yellow': RGB(255, 200, 0), 10 | 'yellow-acid': RGB(160, 255, 0), 11 | 'green': RGB(0, 255, 0), 12 | 'green-forest': RGB(34, 139, 34), 13 | 'green-spring': RGB(0, 255, 127), 14 | 'green-teal': RGB(0, 128, 128), 15 | 'green-turquoise': RGB(0, 199, 140), 16 | 'green-coral': RGB(0, 255, 50), 17 | 'cyan': RGB(0, 255, 255), 18 | 'blue': RGB(0, 0, 255), 19 | 'blue-light': RGB(65, 105, 225), 20 | 'blue-navy': RGB(0, 0, 128), 21 | 'blue-aqua': RGB(0, 255, 255), 22 | 'purple': RGB(128, 0, 128), 23 | 'pink': RGB(255, 0, 178), 24 | 'magenta': RGB(255, 0, 255), 25 | 'black': RGB(0, 0, 0), 26 | 'white': RGB(255, 255, 255), 27 | 'brown': RGB(139, 69, 19), 28 | 'gold': RGB(255, 215, 0), 29 | 'hotpink': RGB(255, 105, 180), 30 | 'lightblue': RGB(173, 216, 230), 31 | 'lightgreen': RGB(152, 251, 152), 32 | 'lightpink': RGB(255, 182, 193), 33 | 'lightyellow': RGB(255, 255, 224), 34 | 'maroon': RGB(128, 0, 0), 35 | 'mint': RGB(189, 252, 201), 36 | 'olive': RGB(85, 107, 47), 37 | 'peach': RGB(255, 100,100), 38 | 'plum': RGB(221, 160, 221), 39 | 'purple': RGB(128, 0, 128), 40 | 'sepia': RGB(94, 38, 18), 41 | 'skyblue': RGB(135, 206, 235), 42 | 'steelblue': RGB(70, 130, 180), 43 | 'tan': RGB(210, 180, 140), 44 | 'violetred': RGB(208, 32, 144), 45 | } 46 | 47 | GRADIENTS = { 48 | "Spectral" : { 49 | "colors": ["red", "orange", "yellow", "green", "green-turquoise", "blue", "purple", "pink"] 50 | }, 51 | "Dancefloor": { 52 | "colors": ["red", "pink", "blue"] 53 | }, 54 | "Plasma": { 55 | "colors": ["blue", "purple", "red", "orange-deep", "yellow"] 56 | }, 57 | "Ocean" : { 58 | "colors": ["blue-aqua", "blue"] 59 | }, 60 | "Viridis" : { 61 | "colors": ["purple", "blue", "green-teal", "green", "yellow"] 62 | }, 63 | "Jungle" : { 64 | "colors": ["green", "green-forest", "orange"] 65 | }, 66 | "Spring" : { 67 | "colors": ["pink", "orange-deep", "yellow"] 68 | }, 69 | "Winter" : { 70 | "colors": ["green-turquoise", "green-coral"] 71 | }, 72 | "Frost" : { 73 | "colors": ["blue", "blue-aqua", "purple", "pink"] 74 | }, 75 | "Sunset" : { 76 | "colors": ["blue-navy", "orange", "red"] 77 | }, 78 | "Borealis" : { 79 | "colors": ["orange-deep", "purple", "green-turquoise", "green"] 80 | }, 81 | "Rust" : { 82 | "colors": ["orange-deep", "red"] 83 | }, 84 | "Christmas" : { 85 | "colors": ["red", "red", "red", "red", "red", "green", "green", "green", "green", "green"], 86 | "method": "repeat" 87 | } 88 | } -------------------------------------------------------------------------------- /ledfx/consts.py: -------------------------------------------------------------------------------- 1 | import pathlib 2 | 3 | __author__ = "Austin Hodges" 4 | __copyright__ = "Austin Hodges" 5 | __license__ = "mit" 6 | 7 | PROJECT_NAME = "LedFx" 8 | 9 | REQUIRED_PYTHON_VERSION = (3, 7, 0) 10 | REQUIRED_PYTHON_STRING = '>={}.{}.{}'.format(REQUIRED_PYTHON_VERSION[0], 11 | REQUIRED_PYTHON_VERSION[1], 12 | REQUIRED_PYTHON_VERSION[2]) 13 | 14 | MAJOR_VERSION = 0 15 | MINOR_VERSION = 8 16 | MICRO_VERSION = 4 17 | PROJECT_VERSION = '{}.{}.{}'.format(MAJOR_VERSION, MINOR_VERSION, MICRO_VERSION) 18 | 19 | __version__ = PROJECT_VERSION 20 | -------------------------------------------------------------------------------- /ledfx/devices/FXMatrix.py: -------------------------------------------------------------------------------- 1 | from ledfx.devices import Device 2 | import logging 3 | import voluptuous as vol 4 | import numpy as np 5 | import socket 6 | 7 | _LOGGER = logging.getLogger(__name__) 8 | 9 | class FXMatrix(Device): 10 | """FXMatrix device support""" 11 | 12 | CONFIG_SCHEMA = vol.Schema({ 13 | vol.Required('ip_address', description='Hostname or IP address of the device'): str, 14 | vol.Required('port', description='Port for the UDP device'): vol.All(vol.Coerce(int), vol.Range(min=1, max=65535)), 15 | vol.Required('width', description='Number of pixels width'): vol.All(vol.Coerce(int), vol.Range(min=1)), 16 | vol.Required('height', description='Number of pixels height'): vol.All(vol.Coerce(int), vol.Range(min=1)), 17 | # vol.Required('pixel_count', description='Number of individual pixels'): vol.All(vol.Coerce(int), vol.Range(min=1)), 18 | }) 19 | 20 | 21 | def activate(self): 22 | self._sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 23 | self._config["pixel_count"] = int(self._config["width"] * self._config["height"]) 24 | super().activate() 25 | 26 | def deactivate(self): 27 | super().deactivate() 28 | self._sock = None 29 | 30 | @property 31 | def pixel_count(self): 32 | return int(self._config["width"] * self._config["height"]) 33 | 34 | def flush(self, data): 35 | udpData = bytearray() 36 | byteData = data.astype(np.dtype('B')) 37 | # Append all of the pixel data 38 | udpData.extend(byteData.flatten().tobytes()) 39 | 40 | self._sock.sendto(bytes(udpData), (self._config['ip_address'], self._config['port'])) -------------------------------------------------------------------------------- /ledfx/devices/udp.py: -------------------------------------------------------------------------------- 1 | from ledfx.devices import Device 2 | import logging 3 | import voluptuous as vol 4 | import numpy as np 5 | import socket 6 | 7 | _LOGGER = logging.getLogger(__name__) 8 | 9 | class UDPDevice(Device): 10 | """Generic UDP device support""" 11 | 12 | CONFIG_SCHEMA = vol.Schema({ 13 | vol.Required('ip_address', description='Hostname or IP address of the device'): str, 14 | vol.Required('port', description='Port for the UDP device'): vol.All(vol.Coerce(int), vol.Range(min=1, max=65535)), 15 | vol.Required('pixel_count', description='Number of individual pixels'): vol.All(vol.Coerce(int), vol.Range(min=1)), 16 | vol.Optional('include_indexes', description='Include the index for every LED', default=False): bool, 17 | vol.Optional('data_prefix', description='Data to be appended in hex format'): str, 18 | vol.Optional('data_postfix', description='Data to be prepended in hex format'): str, 19 | }) 20 | 21 | def activate(self): 22 | self._sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 23 | super().activate() 24 | 25 | def deactivate(self): 26 | super().deactivate() 27 | self._sock = None 28 | 29 | @property 30 | def pixel_count(self): 31 | return int(self._config['pixel_count']) 32 | 33 | def flush(self, data): 34 | udpData = bytearray() 35 | byteData = data.astype(np.dtype('B')) 36 | 37 | # Append the prefix if provided 38 | prefix = self._config.get('data_prefix') 39 | if prefix: 40 | udpData.extend(bytes.fromhex(prefix)) 41 | 42 | # Append all of the pixel data 43 | if self._config['include_indexes']: 44 | for i in range(len(byteData)): 45 | udpData.extend(bytes([i])) 46 | udpData.extend(byteData[i].flatten().tobytes()) 47 | else: 48 | udpData.extend(byteData.flatten().tobytes()) 49 | 50 | # Append the postfix if provided 51 | postfix = self._config.get('data_postfix') 52 | if postfix: 53 | udpData.extend(bytes.fromhex(postfix)) 54 | 55 | self._sock.sendto(bytes(udpData), (self._config['ip_address'], self._config['port'])) -------------------------------------------------------------------------------- /ledfx/effects/bands(Reactive).py: -------------------------------------------------------------------------------- 1 | from ledfx.effects.audio import AudioReactiveEffect 2 | from ledfx.effects.gradient import GradientEffect 3 | from ledfx.color import GRADIENTS, COLORS 4 | import voluptuous as vol 5 | import numpy as np 6 | 7 | 8 | class BandsAudioEffect(AudioReactiveEffect, GradientEffect): 9 | 10 | NAME = "Bands" 11 | 12 | CONFIG_SCHEMA = vol.Schema({ 13 | vol.Optional('band_count', description='Number of bands', default = 6): vol.All(vol.Coerce(int), vol.Range(min=1, max=16)), 14 | vol.Optional('align', description='Alignment of bands', default = 'left'): vol.In(list(["left", "right", "invert", "center"])), 15 | vol.Optional('gradient_name', description='Color gradient to display', default = 'Spectral'): vol.In(list(GRADIENTS.keys())), 16 | vol.Optional('mirror', description='Mirror the effect', default = False): bool 17 | }) 18 | 19 | def config_updated(self, config): 20 | # Create the filters used for the effect 21 | self._r_filter = self.create_filter( 22 | alpha_decay = 0.05, 23 | alpha_rise = 0.999) 24 | self.bkg_color = np.array(COLORS["black"], dtype=float) 25 | 26 | def audio_data_updated(self, data): 27 | # Grab the filtered and interpolated melbank data 28 | y = data.interpolated_melbank(self.pixel_count, filtered = False) 29 | filtered_y = data.interpolated_melbank(self.pixel_count, filtered = True) 30 | 31 | # Grab the filtered difference between the filtered melbank and the 32 | # raw melbank. 33 | r = self._r_filter.update(y - filtered_y) 34 | out = np.tile(r, (3,1)).T 35 | out_clipped = np.clip(out, 0, 1) 36 | out_split = np.array_split(out_clipped, self._config["band_count"], axis=0) 37 | for i in range(self._config["band_count"]): 38 | band_width = len(out_split[i]) 39 | color = self.get_gradient_color(i/self._config["band_count"]) 40 | vol = int(out_split[i].max()*band_width) # length (vol) of band 41 | out_split[i][:] = self.bkg_color 42 | if vol: 43 | out_split[i][:vol] = color 44 | if self._config["align"] == "center": 45 | out_split[i] = np.roll(out_split[i], (band_width-vol)//2, axis=0) 46 | elif self._config["align"] == "invert": 47 | out_split[i] = np.roll(out_split[i], -vol//2, axis=0) 48 | elif self._config["align"] == "right": 49 | out_split[i] = np.flip(out_split[i], axis=0) 50 | elif self._config["align"] == "left": 51 | pass 52 | 53 | self.pixels = np.vstack(out_split) 54 | -------------------------------------------------------------------------------- /ledfx/effects/bar(Reactive).py: -------------------------------------------------------------------------------- 1 | from ledfx.effects.audio import AudioReactiveEffect 2 | from ledfx.effects.gradient import GradientEffect 3 | from ledfx.color import GRADIENTS 4 | import voluptuous as vol 5 | import numpy as np 6 | 7 | class BarAudioEffect(AudioReactiveEffect, GradientEffect): 8 | 9 | NAME = "Bar" 10 | CONFIG_SCHEMA = vol.Schema({ 11 | vol.Optional('gradient_name', description='Color scheme to cycle through', default = 'Spectral'): vol.In(list(GRADIENTS.keys())), 12 | vol.Optional('mode', description='Choose from different animations', default = 'wipe'): vol.In(list(["bounce", "wipe", "in-out"])), 13 | vol.Optional('ease_method', description='Acceleration profile of bar', default='ease_out'): vol.In(list(["ease_in_out", "ease_in", "ease_out", "linear"])), 14 | vol.Optional('color_step', description='Amount of color change per beat', default = 0.125): vol.All(vol.Coerce(float), vol.Range(min=0.0625, max=0.5)) 15 | }) 16 | 17 | def config_updated(self, config): 18 | self.phase = 0 19 | self.color_idx = 0 20 | self.bar_len = 0.3 21 | 22 | def audio_data_updated(self, data): 23 | # Run linear beat oscillator through easing method 24 | beat_oscillator, beat_now = data.oscillator() 25 | if self._config["ease_method"] == "ease_in_out": 26 | x = 0.5*np.sin(np.pi*(beat_oscillator-0.5))+0.5 27 | elif self._config["ease_method"] == "ease_in": 28 | x = beat_oscillator**2 29 | elif self._config["ease_method"] == "ease_out": 30 | x = -(beat_oscillator-1)**2+1 31 | elif self._config["ease_method"] == "linear": 32 | x = beat_oscillator 33 | 34 | # Colour change and phase 35 | if beat_now: 36 | self.phase = 1-self.phase # flip flop 0->1, 1->0 37 | if self.phase == 0: 38 | self.color_idx += self._config["color_step"] # 8 colours, 4 beats to a bar 39 | self.color_idx = self.color_idx % 1 # loop back to zero 40 | 41 | # Compute position of bar start and stop 42 | if self._config["mode"] == "wipe": 43 | if self.phase == 0: 44 | bar_end = x 45 | bar_start = 0 46 | elif self.phase == 1: 47 | bar_end = 1 48 | bar_start = x 49 | 50 | elif self._config["mode"] == "bounce": 51 | x = x*(1-self.bar_len) 52 | if self.phase == 0: 53 | bar_end = x+self.bar_len 54 | bar_start = x 55 | elif self.phase == 1: 56 | bar_end = 1-x 57 | bar_start = 1-(x+self.bar_len) 58 | 59 | elif self._config["mode"] == "in-out": 60 | if self.phase == 0: 61 | bar_end = x 62 | bar_start = 0 63 | elif self.phase == 1: 64 | bar_end = 1-x 65 | bar_start = 0 66 | 67 | # Construct the bar 68 | color = self.get_gradient_color(self.color_idx) 69 | p = np.zeros(np.shape(self.pixels)) 70 | p[int(self.pixel_count*bar_start):int(self.pixel_count*bar_end), :] = color 71 | 72 | # Update the pixel values 73 | self.pixels = p -------------------------------------------------------------------------------- /ledfx/effects/blocks(Reactive).py: -------------------------------------------------------------------------------- 1 | from ledfx.effects.audio import AudioReactiveEffect 2 | from ledfx.effects.gradient import GradientEffect 3 | import voluptuous as vol 4 | import numpy as np 5 | 6 | 7 | class BlocksAudioEffect(AudioReactiveEffect, GradientEffect): 8 | 9 | NAME = "Blocks" 10 | 11 | CONFIG_SCHEMA = vol.Schema({ 12 | vol.Optional('block_count', description='Number of color blocks', default = 4): vol.All(vol.Coerce(int), vol.Range(min=1, max=10)) 13 | }) 14 | 15 | def config_updated(self, config): 16 | # Create the filters used for the effect 17 | self._r_filter = self.create_filter( 18 | alpha_decay = 0.2, 19 | alpha_rise = 0.99) 20 | 21 | def audio_data_updated(self, data): 22 | # Grab the filtered and interpolated melbank data 23 | y = data.interpolated_melbank(self.pixel_count, filtered = False) 24 | filtered_y = data.interpolated_melbank(self.pixel_count, filtered = True) 25 | 26 | # Grab the filtered difference between the filtered melbank and the 27 | # raw melbank. 28 | r = self._r_filter.update(y - filtered_y) 29 | out = np.tile(r, (3,1)) 30 | out_split = np.array_split(out, self._config["block_count"], axis=1) 31 | for i in range(self._config["block_count"]): 32 | color = self.get_gradient_color(i/self._config["block_count"])[:, np.newaxis] 33 | out_split[i] = np.multiply(out_split[i], (out_split[i].max() * color)) 34 | 35 | self.pixels = np.hstack(out_split).T 36 | -------------------------------------------------------------------------------- /ledfx/effects/effectlets/__init__.py: -------------------------------------------------------------------------------- 1 | import os, fnmatch 2 | 3 | EFFECTLET_LIST = [] 4 | 5 | files = os.listdir(os.path.dirname(__file__)) 6 | for entry in files: 7 | if fnmatch.fnmatch(entry, "*.npy"): 8 | EFFECTLET_LIST.append(entry) 9 | -------------------------------------------------------------------------------- /ledfx/effects/effectlets/droplet_0.npy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Mattallmighty/LedFx-OLD/9734e7ebf20e9a4b67aca32f932572003badca4d/ledfx/effects/effectlets/droplet_0.npy -------------------------------------------------------------------------------- /ledfx/effects/effectlets/droplet_1.npy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Mattallmighty/LedFx-OLD/9734e7ebf20e9a4b67aca32f932572003badca4d/ledfx/effects/effectlets/droplet_1.npy -------------------------------------------------------------------------------- /ledfx/effects/effectlets/droplet_2.npy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Mattallmighty/LedFx-OLD/9734e7ebf20e9a4b67aca32f932572003badca4d/ledfx/effects/effectlets/droplet_2.npy -------------------------------------------------------------------------------- /ledfx/effects/energy(Reactive).py: -------------------------------------------------------------------------------- 1 | from ledfx.effects.audio import AudioReactiveEffect 2 | from ledfx.color import COLORS 3 | import voluptuous as vol 4 | import numpy as np 5 | 6 | class EnergyAudioEffect(AudioReactiveEffect): 7 | 8 | NAME = "Energy" 9 | CONFIG_SCHEMA = vol.Schema({ 10 | vol.Optional('blur', description='Amount to blur the effect', default = 4.0): vol.All(vol.Coerce(float), vol.Range(min=0.0, max=10)), 11 | vol.Optional('mirror', description='Mirror the effect', default = True): bool, 12 | vol.Optional('color_cycler', description='Change colors in time with the beat', default = False): bool, 13 | vol.Optional('color_lows', description='Color of low, bassy sounds', default = "red"): vol.In(list(COLORS.keys())), 14 | vol.Optional('color_mids', description='Color of midrange sounds', default = "green"): vol.In(list(COLORS.keys())), 15 | vol.Optional('color_high', description='Color of high sounds', default = "blue"): vol.In(list(COLORS.keys())), 16 | vol.Optional('sensitivity', description='Responsiveness to changes in sound', default = 0.85): vol.All(vol.Coerce(float), vol.Range(min=0.3, max=0.99)), 17 | vol.Optional('mixing_mode', description='Mode of combining each frequencies\' colours', default = "overlap"): vol.In(["additive", "overlap"]), 18 | }) 19 | 20 | def config_updated(self, config): 21 | # scale decay value between 0.1 and 0.2 22 | decay_sensitivity = (self._config["sensitivity"]-0.2)*0.25 23 | self._p_filter = self.create_filter( 24 | alpha_decay = decay_sensitivity, 25 | alpha_rise = self._config["sensitivity"]) 26 | 27 | self.color_cycler = 0 28 | 29 | self.lows_colour = np.array(COLORS[self._config['color_lows']], dtype=float) 30 | self.mids_colour = np.array(COLORS[self._config['color_mids']], dtype=float) 31 | self.high_colour = np.array(COLORS[self._config['color_high']], dtype=float) 32 | 33 | def audio_data_updated(self, data): 34 | 35 | # Calculate the low, mids, and high indexes scaling based on the pixel count 36 | lows_idx = int(np.mean(self.pixel_count * data.melbank_lows())) 37 | mids_idx = int(np.mean(self.pixel_count * data.melbank_mids())) 38 | highs_idx = int(np.mean(self.pixel_count * data.melbank_highs())) 39 | 40 | if self._config["color_cycler"]: 41 | beat_oscillator, beat_now = data.oscillator() 42 | if beat_now: 43 | # Cycle between 0,1,2 for lows, mids and highs 44 | self.color_cycler = (self.color_cycler + 1) % 3 45 | color = np.random.choice(list(COLORS.keys())) 46 | 47 | if self.color_cycler == 0: 48 | self.lows_colour = COLORS[color] 49 | elif self.color_cycler == 1: 50 | self.mids_colour = COLORS[color] 51 | elif self.color_cycler == 2: 52 | self.high_colour = COLORS[color] 53 | 54 | # Build the new energy profile based on the mids, highs and lows setting 55 | # the colors as red, green, and blue channel respectively 56 | p = np.zeros(np.shape(self.pixels)) 57 | if self._config["mixing_mode"] == "additive": 58 | p[:lows_idx] = self.lows_colour 59 | p[:mids_idx] += self.mids_colour 60 | p[:highs_idx] += self.high_colour 61 | elif self._config["mixing_mode"] == "overlap": 62 | p[:lows_idx] = self.lows_colour 63 | p[:mids_idx] = self.mids_colour 64 | p[:highs_idx] = self.high_colour 65 | 66 | # Filter and update the pixel values 67 | self.pixels = self._p_filter.update(p) 68 | -------------------------------------------------------------------------------- /ledfx/effects/equalizer(reactive).py: -------------------------------------------------------------------------------- 1 | from ledfx.effects.audio import AudioReactiveEffect 2 | from ledfx.effects.gradient import GradientEffect 3 | from ledfx.color import GRADIENTS, COLORS 4 | import voluptuous as vol 5 | import numpy as np 6 | 7 | 8 | class EQAudioEffect(AudioReactiveEffect, GradientEffect): 9 | 10 | NAME = "Equalizer" 11 | 12 | CONFIG_SCHEMA = vol.Schema({ 13 | vol.Optional('align', description='Alignment of bands', default = 'left'): vol.In(list(["left", "right", "invert", "center"])), 14 | vol.Optional('gradient_name', description='Color gradient to display', default = 'Spectral'): vol.In(list(GRADIENTS.keys())), 15 | vol.Optional('gradient_repeat', description='Repeat the gradient into segments', default = 6): vol.All(vol.Coerce(int), vol.Range(min=1, max=16)), 16 | vol.Optional('mirror', description='Mirror the effect', default = False): bool, 17 | }) 18 | 19 | def config_updated(self, config): 20 | # Create the filters used for the effect 21 | self._r_filter = self.create_filter( 22 | alpha_decay = 0.5, 23 | alpha_rise = 0.1) 24 | 25 | def audio_data_updated(self, data): 26 | # Grab the filtered and interpolated melbank data 27 | y = data.interpolated_melbank(self.pixel_count, filtered = False) 28 | filtered_y = data.interpolated_melbank(self.pixel_count, filtered = True) 29 | 30 | # Grab the filtered difference between the filtered melbank and the 31 | # raw melbank. 32 | r = self._r_filter.update(y - filtered_y) 33 | r_clipped = np.clip(r, 0, 1) 34 | r_split = np.array_split(r_clipped, self._config["gradient_repeat"]) 35 | for i in range(self._config["gradient_repeat"]): 36 | band_width = len(r_split[i]) 37 | volume = int(r_split[i].sum()*band_width) # length (volume) of band 38 | r_split[i][:] = 0 39 | if volume: 40 | r_split[i][:volume] = 1 41 | if self._config["align"] == "center": 42 | r_split[i] = np.roll(r_split[i], (band_width-volume)//2, axis=0) 43 | elif self._config["align"] == "invert": 44 | r_split[i] = np.roll(r_split[i], -volume//2, axis=0) 45 | elif self._config["align"] == "right": 46 | r_split[i] = np.flip(r_split[i], axis=0) 47 | elif self._config["align"] == "left": 48 | pass 49 | 50 | self.pixels = self.apply_gradient(np.hstack(r_split)) 51 | -------------------------------------------------------------------------------- /ledfx/effects/fade.py: -------------------------------------------------------------------------------- 1 | from ledfx.effects.temporal import TemporalEffect 2 | from ledfx.effects.gradient import GradientEffect 3 | #from ledfx.color import COLORS, GRADIENTS 4 | #from ledfx.effects import Effect 5 | import voluptuous as vol 6 | import numpy as np 7 | import logging 8 | 9 | class FadeEffect(TemporalEffect, GradientEffect): 10 | """ 11 | Fades through the colours of a gradient 12 | """ 13 | 14 | NAME = "Fade" 15 | 16 | CONFIG_SCHEMA = vol.Schema({ 17 | vol.Optional('speed', default = 0.5, description="Rate of change of color"): vol.All(vol.Coerce(float), vol.Range(min=0.1, max=10)), 18 | }) 19 | 20 | def config_updated(self, config): 21 | self.idx = 0 22 | self.forward = True 23 | 24 | def effect_loop(self): 25 | self.idx += 0.0015 26 | if self.idx > 1: 27 | self.idx = 1 28 | self.forward = not self.forward 29 | self.idx = self.idx % 1 30 | 31 | if self.forward: 32 | i = self.idx 33 | else: 34 | i = 1-self.idx 35 | 36 | color = self.get_gradient_color(i) 37 | self.pixels = np.tile(color, (self.pixel_count, 1)) -------------------------------------------------------------------------------- /ledfx/effects/magnitude(Reactive).py: -------------------------------------------------------------------------------- 1 | from ledfx.effects.audio import AudioReactiveEffect, FREQUENCY_RANGES 2 | from ledfx.effects.gradient import GradientEffect 3 | import voluptuous as vol 4 | import numpy as np 5 | 6 | class MagnitudeAudioEffect(AudioReactiveEffect, GradientEffect): 7 | 8 | NAME = "Magnitude" 9 | CONFIG_SCHEMA = vol.Schema({ 10 | vol.Optional('frequency_range', description='Frequency range for the beat detection', default = 'bass'): vol.In(list(FREQUENCY_RANGES.keys())), 11 | }) 12 | 13 | def config_updated(self, config): 14 | self._frequency_range = np.linspace( 15 | FREQUENCY_RANGES[self.config['frequency_range']].min, 16 | FREQUENCY_RANGES[self.config['frequency_range']].max, 17 | 20) 18 | 19 | def audio_data_updated(self, data): 20 | 21 | # Grab the filtered and interpolated melbank data 22 | magnitude = np.max(data.sample_melbank(list(self._frequency_range))) 23 | if magnitude > 1.0: 24 | magnitude = 1.0 25 | self.pixels = self.apply_gradient(magnitude) 26 | -------------------------------------------------------------------------------- /ledfx/effects/math.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from functools import lru_cache 3 | 4 | @lru_cache(maxsize=32) 5 | def _normalized_linspace(size): 6 | return np.linspace(0, 1, size) 7 | 8 | def interpolate(y, new_length): 9 | """Resizes the array by linearly interpolating the values""" 10 | 11 | if len(y) == new_length: 12 | return y 13 | 14 | x_old = _normalized_linspace(len(y)) 15 | x_new = _normalized_linspace(new_length) 16 | z = np.interp(x_new, x_old, y) 17 | 18 | return z 19 | 20 | class ExpFilter: 21 | """Simple exponential smoothing filter""" 22 | 23 | def __init__(self, val=None, alpha_decay=0.5, alpha_rise=0.5): 24 | assert 0.0 < alpha_decay < 1.0, 'Invalid decay smoothing factor' 25 | assert 0.0 < alpha_rise < 1.0, 'Invalid rise smoothing factor' 26 | self.alpha_decay = alpha_decay 27 | self.alpha_rise = alpha_rise 28 | self.value = val 29 | 30 | def update(self, value): 31 | 32 | # Handle deferred initilization 33 | if self.value is None: 34 | self.value = value 35 | return self.value 36 | 37 | if isinstance(self.value, (list, np.ndarray, tuple)): 38 | alpha = value - self.value 39 | alpha[alpha > 0.0] = self.alpha_rise 40 | alpha[alpha <= 0.0] = self.alpha_decay 41 | else: 42 | alpha = self.alpha_rise if value > self.value else self.alpha_decay 43 | self.value = alpha * value + (1.0 - alpha) * self.value 44 | 45 | return self.value -------------------------------------------------------------------------------- /ledfx/effects/modulate.py: -------------------------------------------------------------------------------- 1 | from ledfx.effects import Effect 2 | import time 3 | import logging 4 | import voluptuous as vol 5 | import numpy as np 6 | 7 | _LOGGER = logging.getLogger(__name__) 8 | _rate = 60 9 | 10 | @Effect.no_registration 11 | class ModulateEffect(Effect): 12 | """ 13 | Extension of TemporalEffect that applies brightness modulation 14 | over the strip. This is intended to allow more static effects like 15 | Gradient or singleColor to have some visual movement. 16 | """ 17 | # _thread_active = False 18 | # _thread = None 19 | 20 | CONFIG_SCHEMA = vol.Schema({ 21 | vol.Optional('modulate', description='Brightness modulation', default = False): bool, 22 | vol.Optional('modulation_effect', default = "sine", description="Choose an animation"): vol.In(list(["sine", "breath"])), 23 | vol.Optional('modulation_speed', default = 0.5, description="Animation speed"): vol.All(vol.Coerce(float), vol.Range(min=0.01, max=1)) 24 | }) 25 | 26 | def config_updated(self, config): 27 | self._counter = 0 28 | 29 | # temporal array for breathing cycle 30 | self._breath_cycle = np.linspace(0,9,9*_rate) 31 | self._breath_cycle[:3*_rate] = 0.4 * np.sin(self._breath_cycle[:3*_rate]-(np.pi/2)) + 0.6 32 | self._breath_cycle[3*_rate:] = np.exp(3-self._breath_cycle[3*_rate:]) + 0.2 33 | 34 | def modulate(self, pixels): 35 | """ 36 | Call this function from the effect 37 | """ 38 | if not self._config["modulate"]: 39 | return pixels 40 | 41 | if self._config["modulation_effect"] == "sine": 42 | self._counter += 0.1 * self._config["modulation_speed"] / np.pi 43 | if self._counter >= 2*np.pi: 44 | self._counter = 0 45 | overlay = np.linspace(self._counter + np.pi, 46 | self._counter, 47 | self.pixel_count) 48 | overlay = np.tile(0.3*np.sin(overlay)+0.4, (3,1)).T 49 | return pixels * overlay 50 | 51 | elif self._config["modulation_effect"] == "breath": 52 | self._counter += self._config["modulation_speed"] 53 | if self._counter == 9*_rate: 54 | self._counter = 0 55 | 56 | pixels[int(self._breath_cycle[int(self._counter)] * self.pixel_count):, :] = 0 57 | return pixels 58 | 59 | else: 60 | # LOG that unknown mode selected somehow? 61 | return pixels -------------------------------------------------------------------------------- /ledfx/effects/multiBar(Reactive).py: -------------------------------------------------------------------------------- 1 | from ledfx.effects.audio import AudioReactiveEffect 2 | from ledfx.effects.gradient import GradientEffect 3 | from ledfx.color import GRADIENTS 4 | import voluptuous as vol 5 | import numpy as np 6 | 7 | class MultiBarAudioEffect(AudioReactiveEffect, GradientEffect): 8 | 9 | NAME = "Multicolor Bar" 10 | CONFIG_SCHEMA = vol.Schema({ 11 | vol.Optional('gradient_name', description='Color scheme to cycle through', default = 'Spectral'): vol.In(list(GRADIENTS.keys())), 12 | vol.Optional('mode', description='Choose from different animations', default = 'wipe'): vol.In(list(["cascade", "wipe"])), 13 | vol.Optional('ease_method', description='Acceleration profile of bar', default='linear'): vol.In(list(["ease_in_out", "ease_in", "ease_out", "linear"])), 14 | vol.Optional('color_step', description='Amount of color change per beat', default = 0.125): vol.All(vol.Coerce(float), vol.Range(min=0.0625, max=0.5)) 15 | }) 16 | 17 | def config_updated(self, config): 18 | self.phase = 0 19 | self.color_idx = 0 20 | 21 | def audio_data_updated(self, data): 22 | # Run linear beat oscillator through easing method 23 | beat_oscillator, beat_now = data.oscillator() 24 | if self._config["ease_method"] == "ease_in_out": 25 | x = 0.5*np.sin(np.pi*(beat_oscillator-0.5))+0.5 26 | elif self._config["ease_method"] == "ease_in": 27 | x = beat_oscillator**2 28 | elif self._config["ease_method"] == "ease_out": 29 | x = -(beat_oscillator-1)**2+1 30 | elif self._config["ease_method"] == "linear": 31 | x = beat_oscillator 32 | 33 | # Colour change and phase 34 | if beat_now: 35 | self.phase = 1-self.phase # flip flop 0->1, 1->0 36 | self.color_idx += self._config["color_step"] # 8 colours, 4 beats to a bar 37 | self.color_idx = self.color_idx % 1 # loop back to zero 38 | 39 | color_fg = self.get_gradient_color(self.color_idx) 40 | color_bkg = self.get_gradient_color((self.color_idx+self._config["color_step"])%1) 41 | 42 | # Compute position of bar start and stop 43 | if self._config["mode"] == "wipe": 44 | if self.phase == 0: 45 | idx = x 46 | elif self.phase == 1: 47 | idx = 1-x 48 | color_fg, color_bkg = color_bkg, color_fg 49 | 50 | elif self._config["mode"] == "cascade": 51 | idx = x 52 | 53 | # Construct the array 54 | p = np.zeros(np.shape(self.pixels)) 55 | p[:int(self.pixel_count*idx), :] = color_bkg 56 | p[int(self.pixel_count*idx):, :] = color_fg 57 | # Update the pixel values 58 | self.pixels = p -------------------------------------------------------------------------------- /ledfx/effects/pitchSpectrum(Reactive).py: -------------------------------------------------------------------------------- 1 | from ledfx.effects.audio import AudioReactiveEffect, MIN_MIDI, MAX_MIDI 2 | from ledfx.effects.gradient import GradientEffect 3 | from ledfx.effects import mix_colors 4 | from ledfx.color import COLORS 5 | import voluptuous as vol 6 | import numpy as np 7 | import aubio 8 | 9 | class PitchSpectrumAudioEffect(AudioReactiveEffect, GradientEffect): 10 | 11 | NAME = "PitchSpectrum" 12 | 13 | CONFIG_SCHEMA = vol.Schema({ 14 | vol.Optional('blur', description='Amount to blur the effect', default = 1.0): vol.All(vol.Coerce(float), vol.Range(min=0.0, max=10)), 15 | vol.Optional('mirror', description='Mirror the effect', default = True): bool, 16 | vol.Optional('fade_rate', description='Rate at which notes fade', default = 0.15): vol.All(vol.Coerce(float), vol.Range(min=0.0, max=1.0)), 17 | vol.Optional('responsiveness', description='Responsiveness to note changes', default = 0.15): vol.All(vol.Coerce(float), vol.Range(min=0.0, max=1.0)), 18 | }) 19 | 20 | def config_updated(self, config): 21 | self.avg_midi = None 22 | 23 | 24 | def audio_data_updated(self, data): 25 | y = data.interpolated_melbank(self.pixel_count, filtered = False) 26 | midi_value = data.midi_value() 27 | note_color = COLORS['black'] 28 | if not self.avg_midi: 29 | self.avg_midi = midi_value 30 | 31 | # Average out the midi values to be a little more stable 32 | if midi_value >= MIN_MIDI: 33 | self.avg_midi = self.avg_midi * (1.0 - self._config['responsiveness']) + midi_value * self._config['responsiveness'] 34 | 35 | # Grab the note color based on where it falls in the midi range 36 | if self.avg_midi >= MIN_MIDI: 37 | midi_scaled = (self.avg_midi - MIN_MIDI) / (MAX_MIDI - MIN_MIDI) 38 | 39 | note_color = self.get_gradient_color(midi_scaled) 40 | 41 | # Mix in the new color based on the filterbank information and fade out 42 | # the old colors 43 | new_pixels = self.pixels 44 | for index in range(self.pixel_count): 45 | new_color = mix_colors(self.pixels[index], note_color, y[index]) 46 | new_color = mix_colors(new_color, COLORS['black'], self._config['fade_rate']) 47 | new_pixels[index] = new_color 48 | 49 | # Set the pixels 50 | self.pixels = new_pixels 51 | -------------------------------------------------------------------------------- /ledfx/effects/power(Reactive).py: -------------------------------------------------------------------------------- 1 | from ledfx.effects.audio import AudioReactiveEffect 2 | from ledfx.effects.gradient import GradientEffect 3 | from ledfx.color import COLORS 4 | import voluptuous as vol 5 | import numpy as np 6 | 7 | 8 | class PowerAudioEffect(AudioReactiveEffect, GradientEffect): 9 | 10 | NAME = "Power" 11 | 12 | CONFIG_SCHEMA = vol.Schema({ 13 | vol.Optional('mirror', description='Mirror the effect', default = True): bool, 14 | vol.Optional('blur', description='Amount to blur the effect', default = 0.0): vol.All(vol.Coerce(float), vol.Range(min=0.0, max=10)), 15 | vol.Optional('sparks', description='Flash on percussive hits', default = True): bool 16 | }) 17 | 18 | def config_updated(self, config): 19 | 20 | # Create the filters used for the effect 21 | self._r_filter = self.create_filter( 22 | alpha_decay = 0.2, 23 | alpha_rise = 0.99) 24 | 25 | self._bass_filter = self.create_filter( 26 | alpha_decay = 0.1, 27 | alpha_rise = 0.99) 28 | 29 | # Would be nice to initialise here with np.shape(self.pixels) 30 | self.sparks_overlay = None 31 | 32 | self.sparks_decay = 0.75 33 | self.sparks_color = np.array(COLORS["white"], dtype=float) 34 | 35 | def audio_data_updated(self, data): 36 | 37 | # Grab the filtered and interpolated melbank data 38 | y = data.interpolated_melbank(self.pixel_count, filtered = False) 39 | filtered_y = data.interpolated_melbank(self.pixel_count, filtered = True) 40 | 41 | # Grab the filtered difference between the filtered melbank and the 42 | # raw melbank. 43 | r = self._r_filter.update(y - filtered_y) 44 | 45 | # Apply the melbank data to the gradient curve 46 | out = self.apply_gradient(r) 47 | 48 | if self._config["sparks"]: 49 | # Initialise sparks overlay if its not made yet 50 | if self.sparks_overlay is None: 51 | self.sparks_overlay = np.zeros(np.shape(self.pixels)) 52 | # Get onset data 53 | onsets = data.onset() 54 | # Fade existing sparks a little 55 | self.sparks_overlay *= self.sparks_decay 56 | # Apply new sparks 57 | if onsets["high"]: 58 | sparks = np.random.choice(self.pixel_count, self.pixel_count//50) 59 | self.sparks_overlay[sparks] = self.sparks_color 60 | if onsets["mids"]: 61 | sparks = np.random.choice(self.pixel_count, self.pixel_count//10) 62 | self.sparks_overlay[sparks] = self.sparks_color * 1 63 | # Apply sparks over pixels 64 | out += self.sparks_overlay 65 | 66 | # Get bass power through filter 67 | bass = np.max(data.melbank_lows()) * (1/5) 68 | bass = self._bass_filter.update(bass) 69 | # Grab corresponding color 70 | color = self.get_gradient_color(bass) 71 | # Map it to the length of the strip and apply it 72 | bass_idx = int(bass * self.pixel_count) 73 | out[:bass_idx] = color 74 | 75 | # Update the pixels 76 | self.pixels = out 77 | 78 | 79 | -------------------------------------------------------------------------------- /ledfx/effects/rainbow.py: -------------------------------------------------------------------------------- 1 | from ledfx.effects.temporal import TemporalEffect 2 | from ledfx.effects import fill_rainbow 3 | import voluptuous as vol 4 | 5 | class RainbowEffect(TemporalEffect): 6 | 7 | NAME = "Rainbow" 8 | CONFIG_SCHEMA = vol.Schema({ 9 | vol.Optional('frequency', description='Frequency of the effect curve', default = 1.0): vol.All(vol.Coerce(float), vol.Range(min=0.1, max=10)), 10 | }) 11 | 12 | _hue = 0.1 13 | 14 | def effect_loop(self): 15 | hue_delta = self._config['frequency'] / self.pixel_count 16 | self.pixels = fill_rainbow(self.pixels, self._hue, hue_delta) 17 | 18 | self._hue = self._hue + 0.01 -------------------------------------------------------------------------------- /ledfx/effects/scroll(Reactive).py: -------------------------------------------------------------------------------- 1 | from ledfx.effects.audio import AudioReactiveEffect, FREQUENCY_RANGES_SIMPLE 2 | from ledfx.color import COLORS 3 | import voluptuous as vol 4 | import numpy as np 5 | 6 | 7 | class ScrollAudioEffect(AudioReactiveEffect): 8 | 9 | NAME = "Scroll" 10 | 11 | CONFIG_SCHEMA = vol.Schema({ 12 | vol.Optional('blur', description='Amount to blur the effect', default = 3.0): vol.All(vol.Coerce(float), vol.Range(min=0.0, max=10)), 13 | vol.Optional('mirror', description='Mirror the effect', default = True): bool, 14 | vol.Optional('speed', description='Speed of the effect', default = 5): vol.All(vol.Coerce(int), vol.Range(min=1, max=10)), 15 | vol.Optional('decay', description='Decay rate of the scroll', default = 0.97): vol.All(vol.Coerce(float), vol.Range(min=0.8, max=1.0)), 16 | vol.Optional('threshold', description='Cutoff for quiet sounds. Higher -> only loud sounds are detected', default = 0.0): vol.All(vol.Coerce(float), vol.Range(min=0, max=1)), 17 | vol.Optional('color_lows', description='Color of low, bassy sounds', default = "red"): vol.In(list(COLORS.keys())), 18 | vol.Optional('color_mids', description='Color of midrange sounds', default = "green"): vol.In(list(COLORS.keys())), 19 | vol.Optional('color_high', description='Color of high sounds', default = "blue"): vol.In(list(COLORS.keys())), 20 | }) 21 | 22 | def config_updated(self, config): 23 | 24 | # TODO: Determine how buffers based on the pixels should be 25 | # allocated. Technically there is no guarantee that the effect 26 | # is bound to a device while the config gets updated. Might need 27 | # to move to a model where effects are created for a device and 28 | # must be destroyed and recreated to be moved to another device. 29 | self.output = None 30 | 31 | self.lows_colour = np.array(COLORS[self._config['color_lows']], dtype=float) 32 | self.mids_colour = np.array(COLORS[self._config['color_mids']], dtype=float) 33 | self.high_colour = np.array(COLORS[self._config['color_high']], dtype=float) 34 | 35 | self.lows_cutoff = self._config['threshold'] 36 | self.mids_cutoff = self._config['threshold'] / 4 37 | self.high_cutoff = self._config['threshold'] / 7 38 | 39 | def audio_data_updated(self, data): 40 | 41 | if self.output is None: 42 | self.output = self.pixels 43 | 44 | # Divide the melbank into lows, mids and highs 45 | lows_max = np.clip(np.max(data.melbank_lows() ** 2), 0, 1) 46 | mids_max = np.clip(np.max(data.melbank_mids() ** 2), 0, 1) 47 | highs_max = np.clip(np.max(data.melbank_highs() ** 2), 0, 1) 48 | 49 | if lows_max < self.lows_cutoff: 50 | lows_max = 0 51 | if mids_max < self.mids_cutoff: 52 | mids_max = 0 53 | if highs_max < self.high_cutoff: 54 | highs_max = 0 55 | 56 | # Compute the value for each range based on the max 57 | #lows_val = (np.array((255,0,0)) * lows_max) 58 | #mids_val = (np.array((0,255,0)) * mids_max) 59 | #high_val = (np.array((0,0,255)) * highs_max) 60 | 61 | # Roll the effect and apply the decay 62 | speed = self.config['speed'] 63 | self.output[speed:,:] = self.output[:-speed,:] 64 | self.output = (self.output * self.config['decay']) 65 | 66 | # Add in the new color from the signal maxes 67 | #self.output[:speed, 0] = lows_val[0] + mids_val[0] + high_val[0] 68 | #self.output[:speed, 1] = lows_val[1] + mids_val[1] + high_val[1] 69 | #self.output[:speed, 2] = lows_val[2] + mids_val[2] + high_val[2] 70 | 71 | self.output[:speed] = self.lows_colour * lows_max 72 | self.output[:speed] += self.mids_colour * mids_max 73 | self.output[:speed] += self.high_colour * highs_max 74 | 75 | # Set the pixels 76 | self.pixels = self.output -------------------------------------------------------------------------------- /ledfx/effects/singleColor.py: -------------------------------------------------------------------------------- 1 | from ledfx.color import COLORS 2 | from ledfx.effects.modulate import ModulateEffect 3 | from ledfx.effects.temporal import TemporalEffect 4 | from ledfx.effects.modulate import ModulateEffect 5 | import voluptuous as vol 6 | import numpy as np 7 | 8 | class SingleColorEffect(TemporalEffect, ModulateEffect): 9 | 10 | NAME = "Single Color" 11 | CONFIG_SCHEMA = vol.Schema({ 12 | vol.Optional('color', description='Color of strip', default = "red"): vol.In(list(COLORS.keys())), 13 | }) 14 | 15 | def config_updated(self, config): 16 | self.color = np.array(COLORS[self._config['color']], dtype=float) 17 | 18 | def effect_loop(self): 19 | color_array = np.tile(self.color, (self.pixel_count, 1)) 20 | self.pixels = self.modulate(color_array) 21 | 22 | -------------------------------------------------------------------------------- /ledfx/effects/spectrum(Reactive).py: -------------------------------------------------------------------------------- 1 | from ledfx.effects.audio import AudioReactiveEffect 2 | import voluptuous as vol 3 | import numpy as np 4 | 5 | class SpectrumAudioEffect(AudioReactiveEffect): 6 | 7 | NAME = "Spectrum" 8 | CONFIG_SCHEMA = vol.Schema({}) 9 | 10 | _prev_y = None 11 | 12 | def config_updated(self, config): 13 | 14 | # Create all the filters used for the effect 15 | self._r_filter = self.create_filter( 16 | alpha_decay = 0.2, 17 | alpha_rise = 0.99) 18 | self._b_filter = self.create_filter( 19 | alpha_decay = 0.1, 20 | alpha_rise = 0.5) 21 | 22 | def audio_data_updated(self, data): 23 | 24 | # Grab the filtered and interpolated melbank data 25 | y = data.interpolated_melbank(self.pixel_count, filtered = False) 26 | filtered_y = data.interpolated_melbank(self.pixel_count, filtered = True) 27 | if self._prev_y is None: 28 | self._prev_y = y 29 | 30 | # Update all the filters and build up the RGB values 31 | r = self._r_filter.update(y - filtered_y) 32 | g = np.abs(y - self._prev_y) 33 | b = self._b_filter.update(y) 34 | 35 | self._prev_y = y 36 | 37 | output = np.array([r,g,b]) * 255 38 | self.pixels = output.T -------------------------------------------------------------------------------- /ledfx/effects/strobe(Reactive).py: -------------------------------------------------------------------------------- 1 | from ledfx.effects.audio import AudioReactiveEffect 2 | from ledfx.color import COLORS 3 | import voluptuous as vol 4 | import numpy as np 5 | 6 | class Strobe(AudioReactiveEffect): 7 | 8 | NAME = "Strobe" 9 | CONFIG_SCHEMA = vol.Schema({ 10 | vol.Optional('color', description='Strobe colour', default = "white"): vol.In(list(COLORS.keys())), 11 | vol.Optional('frequency', description='Strobe frequency', default = "1/16 (◉﹏◉ )"): vol.In(list(["1/2 (.-. )", "1/4 (.o. )", "1/8 (◉◡◉ )", "1/16 (◉﹏◉ )", "1/32 (⊙▃⊙ )"])) 12 | }) 13 | 14 | def config_updated(self, config): 15 | MAPPINGS = {"1/2 (.-. )": 2, 16 | "1/4 (.o. )": 4, 17 | "1/8 (◉◡◉ )": 8, 18 | "1/16 (◉﹏◉ )": 16, 19 | "1/32 (⊙▃⊙ )": 32} 20 | self.color = np.array(COLORS[self._config['color']], dtype=float) 21 | self.f = MAPPINGS[self._config["frequency"]] 22 | 23 | def audio_data_updated(self, data): 24 | beat_oscillator, beat_now = data.oscillator() 25 | brightness = (-beat_oscillator % (2 / self.f)) * (self.f / 2) 26 | self.pixels = np.tile(self.color*brightness, (self.pixel_count, 1)) 27 | -------------------------------------------------------------------------------- /ledfx/effects/temporal.py: -------------------------------------------------------------------------------- 1 | import time 2 | import logging 3 | from ledfx.effects import Effect 4 | #from ledfx.effects.audio import AudioReactiveEffect 5 | from threading import Thread 6 | import voluptuous as vol 7 | 8 | _LOGGER = logging.getLogger(__name__) 9 | DEFAULT_RATE = 1.0 / 300.0 10 | 11 | @Effect.no_registration 12 | class TemporalEffect(Effect): 13 | _thread_active = False 14 | _thread = None 15 | 16 | CONFIG_SCHEMA = vol.Schema({ 17 | vol.Optional('speed', default = 1.0, description="Speed of the effect"): vol.All(vol.Coerce(float), vol.Range(min=0.1, max=10)), 18 | }) 19 | 20 | def thread_function(self): 21 | 22 | while self._thread_active: 23 | startTime = time.time() 24 | 25 | # Treat the return value of the effect loop as a speed modifier 26 | # such that effects that are naturally faster or slower can have 27 | # a consistent feel. 28 | sleepInterval = self.effect_loop() 29 | if sleepInterval is None: 30 | sleepInterval = 1.0 31 | sleepInterval = sleepInterval * DEFAULT_RATE 32 | 33 | # Calculate the time to sleep accounting for potential heavy 34 | # frame assembly operations 35 | timeToSleep = (sleepInterval / self._config['speed']) - (time.time() - startTime) 36 | if timeToSleep > 0: 37 | time.sleep(timeToSleep) 38 | 39 | def effect_loop(self): 40 | """ 41 | Triggered periodically based on the effect speed and 42 | any additional effect modifiers 43 | """ 44 | pass 45 | 46 | def activate(self, pixel_count): 47 | super().activate(pixel_count) 48 | 49 | self._thread_active = True 50 | self._thread = Thread(target = self.thread_function) 51 | self._thread.start() 52 | 53 | def deactivate(self): 54 | if self._thread_active: 55 | self._thread_active = False 56 | self._thread.join() 57 | self._thread = None 58 | 59 | super().deactivate() 60 | -------------------------------------------------------------------------------- /ledfx/effects/wavelength(Reactive).py: -------------------------------------------------------------------------------- 1 | from ledfx.effects.audio import AudioReactiveEffect 2 | from ledfx.effects.gradient import GradientEffect 3 | import voluptuous as vol 4 | import numpy as np 5 | 6 | 7 | class WavelengthAudioEffect(AudioReactiveEffect, GradientEffect): 8 | 9 | NAME = "Wavelength" 10 | 11 | # There is no additional configuration here, but override the blur 12 | # default to be 3.0 so blurring is enabled. 13 | CONFIG_SCHEMA = vol.Schema({ 14 | vol.Optional('blur', description='Amount to blur the effect', default = 3.0): vol.All(vol.Coerce(float), vol.Range(min=0.0, max=10)) 15 | }) 16 | 17 | def config_updated(self, config): 18 | 19 | # Create the filters used for the effect 20 | self._r_filter = self.create_filter( 21 | alpha_decay = 0.2, 22 | alpha_rise = 0.99) 23 | 24 | def audio_data_updated(self, data): 25 | 26 | # Grab the filtered and interpolated melbank data 27 | y = data.interpolated_melbank(self.pixel_count, filtered = False) 28 | filtered_y = data.interpolated_melbank(self.pixel_count, filtered = True) 29 | 30 | # Grab the filtered difference between the filtered melbank and the 31 | # raw melbank. 32 | r = self._r_filter.update(y - filtered_y) 33 | 34 | # Apply the melbank data to the gradient curve and update the pixels 35 | self.pixels = self.apply_gradient(r) 36 | -------------------------------------------------------------------------------- /ledfx/events.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import numpy as np 3 | from typing import Callable 4 | 5 | _LOGGER = logging.getLogger(__name__) 6 | 7 | class Event: 8 | """Base for events""" 9 | 10 | LEDFX_SHUTDOWN = 'shutdown' 11 | DEVICE_UPDATE = 'device_update' 12 | GRAPH_UPDATE = 'graph_update' 13 | 14 | def __init__(self, type : str): 15 | self.event_type = type 16 | 17 | def to_dict(self): 18 | return self.__dict__ 19 | 20 | class DeviceUpdateEvent(Event): 21 | """Event emmitted when a device's pixels are updated""" 22 | 23 | def __init__(self, device_id : str, pixels : np.ndarray): 24 | super().__init__(Event.DEVICE_UPDATE) 25 | self.device_id = device_id 26 | self.pixels = pixels.T.tolist() 27 | 28 | class GraphUpdateEvent(Event): 29 | """Event emmitted when a device's pixels are updated""" 30 | 31 | def __init__(self, graph_id : str, melbank : np.ndarray, frequencies : np.ndarray): 32 | super().__init__(Event.GRAPH_UPDATE) 33 | self.graph_id = graph_id 34 | self.melbank = melbank.tolist() 35 | self.frequencies = frequencies.tolist() 36 | 37 | class LedFxShutdownEvent(Event): 38 | """Event emmitted when LedFx is shutting down""" 39 | 40 | def __init__(self): 41 | super().__init__(Event.LEDFX_SHUTDOWN) 42 | 43 | class EventListener: 44 | def __init__(self, callback: Callable, event_filter: dict={}): 45 | self.callback = callback 46 | self.filter = event_filter 47 | 48 | def filter_event(self, event): 49 | event_dict = event.to_dict() 50 | for filter_key in self.filter: 51 | if event_dict.get(filter_key) != self.filter[filter_key]: 52 | return True 53 | 54 | return False 55 | 56 | class Events: 57 | 58 | def __init__(self, ledfx): 59 | self._ledfx = ledfx 60 | self._listeners = {} 61 | 62 | def fire_event(self, event : Event) -> None: 63 | 64 | listeners = self._listeners.get(event.event_type, []) 65 | if not listeners: 66 | return 67 | 68 | for listener in listeners: 69 | if not listener.filter_event(event): 70 | self._ledfx.loop.call_soon(listener.callback, event) 71 | 72 | def add_listener(self, callback: Callable, event_type: str, event_filter: dict={}) -> None: 73 | 74 | listener = EventListener(callback, event_filter) 75 | if event_type in self._listeners: 76 | self._listeners[event_type].append(listener) 77 | else: 78 | self._listeners[event_type] = [listener] 79 | 80 | def remove_listener() -> None: 81 | self._remove_listener(event_type, listener) 82 | 83 | return remove_listener 84 | 85 | def _remove_listener(self, event_type: str, listener: Callable) -> None: 86 | 87 | try: 88 | self._listeners[event_type].remove(listener) 89 | if not self._listeners[event_type]: 90 | self._listeners.pop(event_type) 91 | except (KeyError, ValueError): 92 | _LOGGER.warning("Failed to remove event listener %s", listener) 93 | -------------------------------------------------------------------------------- /ledfx/frontend/components/SceneCard/SceneConfigTable.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { withStyles } from '@material-ui/core/styles'; 4 | 5 | import Table from '@material-ui/core/Table'; 6 | import TableHead from '@material-ui/core/TableHead'; 7 | import TableRow from '@material-ui/core/TableRow'; 8 | import TableCell from '@material-ui/core/TableCell'; 9 | import TableBody from '@material-ui/core/TableBody'; 10 | 11 | const styles = theme => ({ 12 | table: { 13 | } 14 | }); 15 | 16 | class SceneConfigTable extends React.Component { 17 | 18 | render() { 19 | const { classes, devices } = this.props; 20 | 21 | return ( 22 | 23 | 24 | 25 | Device 26 | Effect 27 | 28 | 29 | 30 | {renderRows(devices)} 31 | 32 |
33 | ); 34 | } 35 | } 36 | 37 | const renderRows = (devices) => { 38 | const devicesWithEffects = Object.keys(devices).filter(id => !!devices[id].type); 39 | return devicesWithEffects.map(id => { 40 | return ( 41 | 42 | 43 | {id} 44 | 45 | 46 | {devices[id].type} 47 | 48 | 49 | )}) 50 | } 51 | 52 | SceneConfigTable.propTypes = { 53 | classes: PropTypes.object.isRequired, 54 | devices: PropTypes.object.isRequired 55 | }; 56 | 57 | export default withStyles(styles)(SceneConfigTable); -------------------------------------------------------------------------------- /ledfx/http.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import jinja2 3 | import aiohttp_jinja2 4 | from aiohttp import web 5 | import aiohttp 6 | from ledfx.api import RestApi 7 | import numpy as np 8 | import json 9 | import ledfx_frontend 10 | import os 11 | import sys 12 | 13 | try: 14 | base_path = sys._MEIPASS 15 | except: 16 | base_path = os.path.abspath(".") 17 | 18 | _LOGGER = logging.getLogger(__name__) 19 | 20 | class HttpServer(object): 21 | def __init__(self, ledfx, host, port): 22 | """Initialize the HTTP server""" 23 | 24 | self.app = web.Application(loop=ledfx.loop) 25 | self.api = RestApi(ledfx) 26 | templates_path = os.path.abspath(os.path.dirname(ledfx_frontend.__file__)) 27 | aiohttp_jinja2.setup( 28 | self.app, 29 | loader=jinja2.FileSystemLoader(templates_path)) 30 | self.register_routes() 31 | 32 | self._ledfx = ledfx 33 | self.host = host 34 | self.port = port 35 | 36 | @aiohttp_jinja2.template('index.html') 37 | async def index(self, request): 38 | return {} 39 | 40 | def register_routes(self): 41 | self.api.register_routes(self.app) 42 | self.app.router.add_static('/static', path=ledfx_frontend.where() + '/static', name='static') 43 | 44 | self.app.router.add_route('get', '/', self.index) 45 | self.app.router.add_route('get', '/{extra:.+}', self.index) 46 | 47 | async def start(self): 48 | self.handler = self.app.make_handler(loop=self._ledfx.loop) 49 | 50 | try: 51 | self.server = await self._ledfx.loop.create_server(self.handler, self.host, self.port) 52 | except OSError as error: 53 | _LOGGER.error("Failed to create HTTP server at port %d: %s", self.port, error) 54 | 55 | self.base_url = ('http://{}:{}').format(self.host, self.port) 56 | print(('Started webinterface at {}').format(self.base_url)) 57 | 58 | async def stop(self): 59 | if self.server: 60 | self.server.close() 61 | await self.server.wait_closed() 62 | await self.app.shutdown() 63 | if self.handler: 64 | await self.handler.shutdown(10) 65 | await self.app.cleanup() -------------------------------------------------------------------------------- /ledfx_frontend/__init__.py: -------------------------------------------------------------------------------- 1 | """ledfx_frontend""" 2 | import os 3 | 4 | def where(): 5 | return os.path.dirname(__file__) -------------------------------------------------------------------------------- /ledfx_frontend/asset-manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": { 3 | "main.css": "/static/css/main.c95fd1ee.chunk.css", 4 | "main.js": "/static/js/main.f987f2e0.chunk.js", 5 | "main.js.map": "/static/js/main.f987f2e0.chunk.js.map", 6 | "runtime-main.js": "/static/js/runtime-main.5d286450.js", 7 | "runtime-main.js.map": "/static/js/runtime-main.5d286450.js.map", 8 | "static/js/2.78a442b4.chunk.js": "/static/js/2.78a442b4.chunk.js", 9 | "static/js/2.78a442b4.chunk.js.map": "/static/js/2.78a442b4.chunk.js.map", 10 | "index.html": "/index.html", 11 | "precache-manifest.b20253c4725abce969d00ebfad32158c.js": "/precache-manifest.b20253c4725abce969d00ebfad32158c.js", 12 | "service-worker.js": "/service-worker.js", 13 | "static/css/main.c95fd1ee.chunk.css.map": "/static/css/main.c95fd1ee.chunk.css.map", 14 | "static/js/2.78a442b4.chunk.js.LICENSE.txt": "/static/js/2.78a442b4.chunk.js.LICENSE.txt" 15 | }, 16 | "entrypoints": [ 17 | "static/js/runtime-main.5d286450.js", 18 | "static/js/2.78a442b4.chunk.js", 19 | "static/css/main.c95fd1ee.chunk.css", 20 | "static/js/main.f987f2e0.chunk.js" 21 | ] 22 | } -------------------------------------------------------------------------------- /ledfx_frontend/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Mattallmighty/LedFx-OLD/9734e7ebf20e9a4b67aca32f932572003badca4d/ledfx_frontend/favicon.ico -------------------------------------------------------------------------------- /ledfx_frontend/index.html: -------------------------------------------------------------------------------- 1 | LedFx
-------------------------------------------------------------------------------- /ledfx_frontend/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Mattallmighty/LedFx-OLD/9734e7ebf20e9a4b67aca32f932572003badca4d/ledfx_frontend/logo192.png -------------------------------------------------------------------------------- /ledfx_frontend/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Mattallmighty/LedFx-OLD/9734e7ebf20e9a4b67aca32f932572003badca4d/ledfx_frontend/logo512.png -------------------------------------------------------------------------------- /ledfx_frontend/precache-manifest.b20253c4725abce969d00ebfad32158c.js: -------------------------------------------------------------------------------- 1 | self.__precacheManifest = (self.__precacheManifest || []).concat([ 2 | { 3 | "revision": "d1346783e3d6a6fbb681ad858adf72d1", 4 | "url": "/index.html" 5 | }, 6 | { 7 | "revision": "0894154d2e8b9a95efb9", 8 | "url": "/static/css/main.c95fd1ee.chunk.css" 9 | }, 10 | { 11 | "revision": "8654872ff0321a49d398", 12 | "url": "/static/js/2.78a442b4.chunk.js" 13 | }, 14 | { 15 | "revision": "d9102df1a3cc2f72cfa727491b26d8fd", 16 | "url": "/static/js/2.78a442b4.chunk.js.LICENSE.txt" 17 | }, 18 | { 19 | "revision": "0894154d2e8b9a95efb9", 20 | "url": "/static/js/main.f987f2e0.chunk.js" 21 | }, 22 | { 23 | "revision": "09b2009ed18140fb7100", 24 | "url": "/static/js/runtime-main.5d286450.js" 25 | } 26 | ]); -------------------------------------------------------------------------------- /ledfx_frontend/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /ledfx_frontend/service-worker.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Welcome to your Workbox-powered service worker! 3 | * 4 | * You'll need to register this file in your web app and you should 5 | * disable HTTP caching for this file too. 6 | * See https://goo.gl/nhQhGp 7 | * 8 | * The rest of the code is auto-generated. Please don't update this file 9 | * directly; instead, make changes to your Workbox build configuration 10 | * and re-run your build process. 11 | * See https://goo.gl/2aRDsh 12 | */ 13 | 14 | importScripts("https://storage.googleapis.com/workbox-cdn/releases/4.3.1/workbox-sw.js"); 15 | 16 | importScripts( 17 | "/precache-manifest.b20253c4725abce969d00ebfad32158c.js" 18 | ); 19 | 20 | self.addEventListener('message', (event) => { 21 | if (event.data && event.data.type === 'SKIP_WAITING') { 22 | self.skipWaiting(); 23 | } 24 | }); 25 | 26 | workbox.core.clientsClaim(); 27 | 28 | /** 29 | * The workboxSW.precacheAndRoute() method efficiently caches and responds to 30 | * requests for URLs in the manifest. 31 | * See https://goo.gl/S9QRab 32 | */ 33 | self.__precacheManifest = [].concat(self.__precacheManifest || []); 34 | workbox.precaching.precacheAndRoute(self.__precacheManifest, {}); 35 | 36 | workbox.routing.registerNavigationRoute(workbox.precaching.getCacheKeyForURL("/index.html"), { 37 | 38 | blacklist: [/^\/_/,/\/[^/?]+\.[^/]+$/], 39 | }); 40 | -------------------------------------------------------------------------------- /ledfx_frontend/static/css/main.c95fd1ee.chunk.css: -------------------------------------------------------------------------------- 1 | body,html{font-family:"Roboto","Helvetica",sans-serif;font-weight:300;overflow:hidden}#root,body,html{height:100%;width:100%} 2 | /*# sourceMappingURL=main.c95fd1ee.chunk.css.map */ -------------------------------------------------------------------------------- /ledfx_frontend/static/css/main.c95fd1ee.chunk.css.map: -------------------------------------------------------------------------------- 1 | {"version":3,"sources":["style.css"],"names":[],"mappings":"AAAA,UACE,2CAA8C,CAC9C,eAAgB,CAGhB,eACF,CAEA,gBALE,WAAY,CACZ,UAOF","file":"main.c95fd1ee.chunk.css","sourcesContent":["html, body {\r\n font-family: 'Roboto', 'Helvetica', sans-serif;\r\n font-weight: 300;\r\n height: 100%;\r\n width: 100%;\r\n overflow: hidden;\r\n}\r\n\r\n#root {\r\n height: 100%;\r\n width: 100%;\r\n}"]} -------------------------------------------------------------------------------- /ledfx_frontend/static/js/2.78a442b4.chunk.js.LICENSE.txt: -------------------------------------------------------------------------------- 1 | /* 2 | object-assign 3 | (c) Sindre Sorhus 4 | @license MIT 5 | */ 6 | 7 | /*! 8 | Copyright (c) 2017 Jed Watson. 9 | Licensed under the MIT License (MIT), see 10 | http://jedwatson.github.io/classnames 11 | */ 12 | 13 | /*! 14 | * Chart.js v2.9.4 15 | * https://www.chartjs.org 16 | * (c) 2020 Chart.js Contributors 17 | * Released under the MIT License 18 | */ 19 | 20 | /** 21 | * A better abstraction over CSS. 22 | * 23 | * @copyright Oleg Isonen (Slobodskoi) / Isonen 2014-present 24 | * @website https://github.com/cssinjs/jss 25 | * @license MIT 26 | */ 27 | 28 | /** @license React v0.19.1 29 | * scheduler.production.min.js 30 | * 31 | * Copyright (c) Facebook, Inc. and its affiliates. 32 | * 33 | * This source code is licensed under the MIT license found in the 34 | * LICENSE file in the root directory of this source tree. 35 | */ 36 | 37 | /** @license React v16.13.1 38 | * react-is.production.min.js 39 | * 40 | * Copyright (c) Facebook, Inc. and its affiliates. 41 | * 42 | * This source code is licensed under the MIT license found in the 43 | * LICENSE file in the root directory of this source tree. 44 | */ 45 | 46 | /** @license React v16.14.0 47 | * react-dom.production.min.js 48 | * 49 | * Copyright (c) Facebook, Inc. and its affiliates. 50 | * 51 | * This source code is licensed under the MIT license found in the 52 | * LICENSE file in the root directory of this source tree. 53 | */ 54 | 55 | /** @license React v16.14.0 56 | * react.production.min.js 57 | * 58 | * Copyright (c) Facebook, Inc. and its affiliates. 59 | * 60 | * This source code is licensed under the MIT license found in the 61 | * LICENSE file in the root directory of this source tree. 62 | */ 63 | 64 | /**! 65 | * @fileOverview Kickass library to create and place poppers near their reference elements. 66 | * @version 1.16.1-lts 67 | * @license 68 | * Copyright (c) 2016 Federico Zivolo and contributors 69 | * 70 | * Permission is hereby granted, free of charge, to any person obtaining a copy 71 | * of this software and associated documentation files (the "Software"), to deal 72 | * in the Software without restriction, including without limitation the rights 73 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 74 | * copies of the Software, and to permit persons to whom the Software is 75 | * furnished to do so, subject to the following conditions: 76 | * 77 | * The above copyright notice and this permission notice shall be included in all 78 | * copies or substantial portions of the Software. 79 | * 80 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 81 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 82 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 83 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 84 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 85 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 86 | * SOFTWARE. 87 | */ 88 | -------------------------------------------------------------------------------- /ledfx_frontend/static/js/runtime-main.5d286450.js: -------------------------------------------------------------------------------- 1 | !function(e){function r(r){for(var n,l,f=r[0],i=r[1],a=r[2],c=0,s=[];c=40.8.0", "wheel"] 5 | build-backend = "setuptools.build_meta:__legacy__" -------------------------------------------------------------------------------- /release.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import argparse 4 | import sys 5 | import re 6 | import subprocess 7 | import shutil 8 | 9 | from ledfx.consts import MAJOR_VERSION, MINOR_VERSION 10 | 11 | def write_version(major, minor): 12 | with open('ledfx/consts.py') as fil: 13 | content = fil.read() 14 | 15 | content = re.sub('MAJOR_VERSION = .*\n', 16 | 'MAJOR_VERSION = {}\n'.format(major), 17 | content) 18 | content = re.sub('MINOR_VERSION = .*\n', 19 | 'MINOR_VERSION = {}\n'.format(minor), 20 | content) 21 | 22 | with open('ledfx/consts.py', 'wt') as fil: 23 | content = fil.write(content) 24 | 25 | def execute_command(command): 26 | return subprocess.check_output(command.split(' ')).decode('UTF-8').rstrip() 27 | 28 | def main(): 29 | parser = argparse.ArgumentParser( 30 | description="Release a new version of LedFx") 31 | parser.add_argument('type', help="The type of release", 32 | choices=['major', 'minor']) 33 | parser.add_argument('branch', help="Branch", 34 | choices=['master', 'dev']) 35 | parser.add_argument('--no_bump', action='store_true', 36 | help='Create a version bump commit.') 37 | 38 | arguments = parser.parse_args() 39 | 40 | branch = execute_command("git rev-parse --abbrev-ref HEAD") 41 | if branch != "master": 42 | print("Releases must be pushed from the master branch.") 43 | return 44 | 45 | current_commit = execute_command("git rev-parse HEAD") 46 | master_commit = execute_command("git rev-parse master@{upstream}") 47 | if current_commit != master_commit: 48 | print("Release must be pushed when up-to-date with origin.") 49 | return 50 | 51 | git_diff = execute_command("git diff HEAD") 52 | if git_diff: 53 | print("Release must be pushed without any staged changes.") 54 | return 55 | 56 | if not arguments.no_bump: 57 | # Bump the version based on the release type 58 | major = MAJOR_VERSION 59 | minor = MINOR_VERSION 60 | if arguments.type == 'major': 61 | major += 1 62 | minor = 0 63 | elif arguments.type == 'minor': 64 | minor += 1 65 | 66 | # Write the new version to consts.py 67 | write_version(major, minor) 68 | 69 | subprocess.run([ 70 | 'git', 'commit', '-am', 'Version Bump for Release {}.{}'.format(major, minor)]) 71 | subprocess.run(['git', 'push', 'origin', 'master']) 72 | 73 | shutil.rmtree("dist", ignore_errors=True) 74 | subprocess.run(['python', 'setup.py', 'sdist', 'bdist_wheel']) 75 | subprocess.run(['python', '-m', 'twine', 'upload', 'dist/*', '--skip-existing']) 76 | 77 | 78 | if __name__ == '__main__': 79 | main() 80 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | # Requirements. These can can be installed with: 2 | # pip install -r requirements.txt 3 | 4 | # You will need a C compiler and proper setup for Cython 5 | # https://wiki.python.org/moin/WindowsCompilers 6 | # or use conda to install 7 | # conda install -c conda-forge aubio 8 | 9 | # Current windows bug requires no later than 1.19.3 10 | numpy==1.19.3 11 | 12 | voluptuous>=0.11.1 13 | pyaudio>=0.2.11 14 | sacn>=1.3 15 | aiohttp<=3.7.0 16 | aiohttp_jinja2>=1.3.0 17 | requests>=2.24.0 18 | pyyaml>=5.1 19 | aubio>=0.4.8 20 | zeroconf>=0.28.6 -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | description = A network based light effect controller 3 | long-description = file: README.rst 4 | platforms = any 5 | classifiers = 6 | Development Status :: 2 - Pre-Alpha 7 | Programming Language :: Python :: 3 8 | Programming Language :: Python :: 3.7 9 | Programming Language :: Python :: 3.8 10 | Programming Language :: Python :: 3.9 11 | License :: OSI Approved :: MIT License 12 | Natural Language :: English 13 | 14 | [tox:tox] 15 | requires = tox-venv 16 | setuptools >= 40.8.0 17 | wheel 18 | 19 | [options] 20 | packages = find: 21 | include_package_data = true 22 | zip_safe = false 23 | 24 | [options.packages.find] 25 | exclude = 26 | tests 27 | tests.* 28 | 29 | [test] 30 | addopts = tests 31 | 32 | [tool:pytest] 33 | testpaths = tests 34 | norecursedirs = 35 | dist 36 | build 37 | .tox 38 | 39 | [build_sphinx] 40 | source_dir = docs/source 41 | build_dir = docs/build 42 | 43 | [flake8] 44 | exclude = 45 | .tox 46 | build 47 | dist 48 | .eggs 49 | docs/conf.py -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from datetime import datetime as dt 4 | from setuptools import setup, find_packages 5 | import ledfx.consts as const 6 | 7 | PROJECT_PACKAGE_NAME = 'ledfx-dev' 8 | PROJECT_VERSION = const.PROJECT_VERSION 9 | PROJECT_LICENSE = 'The MIT License' 10 | PROJECT_AUTHOR = 'Austin Hodges' 11 | PROJECT_AUTHOR_EMAIL = 'austin.b.hodges@gmail.com' 12 | PROJECT_MAINTAINER = 'LedFx Devs' 13 | PROJECT_MAINTAINER_EMAIL = 'ledfx.app@gmail.com' 14 | PROJECT_URL = 'https://github.com/ahodges9/LedFx/tree/dev' 15 | 16 | # Need to install numpy first 17 | SETUP_REQUIRES = [ 18 | 'numpy==1.19.3' 19 | ] 20 | 21 | INSTALL_REQUIRES = [ 22 | # Nasty bug in windows 10 at the moment - https://developercommunity.visualstudio.com/content/problem/1207405/fmod-after-an-update-to-windows-2004-is-causing-a.html 23 | # numpy 1.19.3 has a workaround 24 | 'numpy==1.19.3', 25 | 'voluptuous==0.12.0', 26 | 'pyaudio>=0.2.11', 27 | 'sacn==1.4.6', 28 | # aiohttp 3.7 branch has a regression that spams errors - bug report https://github.com/aio-libs/aiohttp/issues/5212 29 | 'aiohttp==3.6.3', 30 | # yarl needs to be this for aiohttp 31 | 'yarl==1.5.1', 32 | # multidict needs to be this for aiohttp 33 | 'multidict==4.7.6', 34 | 'aiohttp_jinja2>=1.1.0', 35 | 'requests>=2.24.0', 36 | 'pyyaml>=5.3.1', 37 | 'aubio>=0.4.9', 38 | 'zeroconf>=0.28.6', 39 | 'pypiwin32>=223;platform_system=="Windows"', 40 | 'pyupdater>=3.1.1' 41 | ] 42 | 43 | setup( 44 | name=PROJECT_PACKAGE_NAME, 45 | version=PROJECT_VERSION, 46 | license = PROJECT_LICENSE, 47 | author=PROJECT_AUTHOR, 48 | author_email=PROJECT_AUTHOR_EMAIL, 49 | maintainer=PROJECT_MAINTAINER, 50 | maintainer_email=PROJECT_MAINTAINER_EMAIL, 51 | url=PROJECT_URL, 52 | project_urls={ 53 | 'Documentation': 'https://ledfx.readthedocs.io/en/docs/index.html', 54 | 'Website': 'https://ledfx.app', 55 | 'Source': 'https://github.com/ahodges9/LedFx', 56 | 'Discord': 'https://discord.gg/PqXMuthSNx' 57 | }, 58 | install_requires=INSTALL_REQUIRES, 59 | setup_requires=SETUP_REQUIRES, 60 | python_requires=const.REQUIRED_PYTHON_STRING, 61 | include_package_data=True, 62 | # packages=find_packages(), 63 | zip_safe=True, 64 | entry_points={ 65 | 'console_scripts': [ 66 | 'ledfx = ledfx.__main__:main' 67 | ] 68 | }, 69 | package_data={ 70 | 'ledfx_frontend': ['*'], 71 | '': ['*.npy'], 72 | '': ['*.yaml'] 73 | }, 74 | 75 | ) 76 | -------------------------------------------------------------------------------- /win.spec: -------------------------------------------------------------------------------- 1 | # -*- mode: python ; coding: utf-8 -*- 2 | 3 | block_cipher = None 4 | 5 | 6 | a = Analysis(['C:\\Users\\shaun\\ledfx3.7\\ledfx\\ledfx\\__main__.py'], 7 | pathex=['C:\\Users\\shaun\\ledfx3.7\\ledfx', 'C:\\Users\\shaun\\ledfx3.7\\ledfx'], 8 | binaries=[], 9 | datas=[('C:/Users/shaun/ledfx3.7/ledfx/ledfx_frontend', 'ledfx_frontend/'), ('C:/Users/shaun/ledfx3.7/ledfx/ledfx', 'ledfx/'), ('C:/Users/shaun/ledfx3.7/ledfx/icons', 'icons/')], 10 | hiddenimports=['sacn', 'pyaudio', 'aubio', 'numpy', 'math', 'voluptuous', 'numpy', 'aiohttp', 'aiohttp_jinja2'], 11 | hookspath=['c:\\users\\shaun\\ledfx3.7\\lib\\site-packages\\pyupdater\\hooks'], 12 | runtime_hooks=[], 13 | excludes=[], 14 | win_no_prefer_redirects=False, 15 | win_private_assemblies=False, 16 | cipher=block_cipher, 17 | noarchive=False) 18 | pyz = PYZ(a.pure, a.zipped_data, 19 | cipher=block_cipher) 20 | exe = EXE(pyz, 21 | a.scripts, 22 | [], 23 | exclude_binaries=True, 24 | name='win', 25 | debug=False, 26 | bootloader_ignore_signals=False, 27 | strip=False, 28 | upx=True, 29 | console=True) 30 | coll = COLLECT(exe, 31 | a.binaries, 32 | a.zipfiles, 33 | a.datas, 34 | strip=False, 35 | upx=True, 36 | upx_exclude=[], 37 | name='win') 38 | -------------------------------------------------------------------------------- /yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | --------------------------------------------------------------------------------