├── .coveragerc ├── .env ├── .github ├── release-drafter.yml └── workflows │ └── nodejs.yml ├── .gitignore ├── .travis.yml ├── AUTHORS.rst ├── CHANGELOG.rst ├── LICENSE.txt ├── MANIFEST.in ├── README.rst ├── docs ├── Makefile ├── _static │ └── logo.png ├── api.rst ├── conf.py ├── contents.rst ├── developer.rst ├── index.rst ├── make.bat └── quickstart.rst ├── icons ├── discord.png ├── favicon.ico ├── large_black_alpha.png ├── large_black_on_white.png ├── large_white_alpha.png ├── large_white_on_black.png ├── small_black_alpha.png ├── small_black_on_white.png ├── small_white_alpha.png └── small_white_on_black.png ├── jshint.jshintrc ├── ledfx ├── __init__.py ├── __main__.py ├── api │ ├── __init__.py │ ├── audio_devices.py │ ├── config.py │ ├── device.py │ ├── device_effects.py │ ├── devices.py │ ├── effect.py │ ├── effects.py │ ├── info.py │ ├── presets.py │ ├── schema.py │ ├── schema_types.py │ ├── utils.py │ └── websocket.py ├── color.py ├── config.py ├── consts.py ├── core.py ├── devices │ ├── FXMatrix.py │ ├── __init__.py │ ├── e131.py │ └── udp.py ├── effects │ ├── __init__.py │ ├── audio.py │ ├── beat(Reactive).py │ ├── effectlets │ │ ├── __init__.py │ │ ├── droplet_0.npy │ │ ├── droplet_1.npy │ │ └── droplet_2.npy │ ├── energy(Reactive).py │ ├── fade.py │ ├── gradient.py │ ├── math.py │ ├── mel.py │ ├── modulate.py │ ├── pitchSpectrum(Reactive).py │ ├── rain(Reactive).py │ ├── rainbow.py │ ├── scroll(Reactive).py │ ├── singleColor.py │ ├── spectrum(Reactive).py │ ├── strobe.py │ ├── temporal.py │ └── wavelength(Reactive).py ├── events.py ├── frontend │ ├── actions │ │ ├── devices.js │ │ ├── index.js │ │ ├── presets.js │ │ ├── schemas.js │ │ └── settings.js │ ├── assets │ │ ├── img │ │ │ ├── icon │ │ │ │ ├── favicon.ico │ │ │ │ ├── large_black_alpha.png │ │ │ │ ├── large_black_on_white.png │ │ │ │ ├── large_white_alpha.png │ │ │ │ ├── large_white_on_black.png │ │ │ │ ├── small_black_alpha.png │ │ │ │ ├── small_black_on_white.png │ │ │ │ ├── small_white_alpha.png │ │ │ │ └── small_white_on_black.png │ │ │ └── logo.png │ │ └── jss │ │ │ └── style.jsx │ ├── components │ │ ├── AddPresetCard │ │ │ └── AddPresetCard.jsx │ │ ├── DeviceConfigDialog │ │ │ └── DeviceConfigDialog.jsx │ │ ├── DeviceMiniControl │ │ │ └── DeviceMiniControl.jsx │ │ ├── DevicesTable │ │ │ ├── DevicesTable.jsx │ │ │ └── DevicesTableItem.jsx │ │ ├── EffectControl │ │ │ └── EffectControl.jsx │ │ ├── Header │ │ │ ├── Header.jsx │ │ │ └── style.jsx │ │ ├── MelbankGraph │ │ │ └── MelbankGraph.jsx │ │ ├── PixelColorGraph │ │ │ └── PixelColorGraph.jsx │ │ ├── PresetCard │ │ │ ├── PresetCard.jsx │ │ │ └── PresetConfigTable.jsx │ │ ├── PresetConfigDialog │ │ │ └── PresetConfigDialog.jsx │ │ ├── SchemaForm │ │ │ ├── SchemaFormCollection.jsx │ │ │ └── Utils.jsx │ │ └── Sidebar │ │ │ ├── Sidebar.jsx │ │ │ └── style.jsx │ ├── dist │ │ ├── __init__.py │ │ ├── index.html │ │ ├── ledfx_icon.png │ │ └── small_black_alpha.png │ ├── index.jsx │ ├── layouts │ │ └── Default │ │ │ ├── Default.jsx │ │ │ └── style.jsx │ ├── reducers │ │ ├── devices.js │ │ ├── index.js │ │ ├── presets.js │ │ ├── schemas.js │ │ └── settings.js │ ├── routes │ │ ├── index.jsx │ │ └── views.jsx │ ├── style.css │ ├── utils │ │ ├── api │ │ │ └── index.jsx │ │ └── helpers.jsx │ └── views │ │ ├── Dashboard │ │ └── Dashboard.jsx │ │ ├── Developer │ │ └── Developer.jsx │ │ ├── Device │ │ └── Device.jsx │ │ ├── Devices │ │ └── Devices.jsx │ │ ├── Presets │ │ └── Presets.jsx │ │ └── Settings │ │ └── Settings.jsx ├── http.py └── utils.py ├── ledfx_frontend ├── 0468512747540774a71f3b070b96f76d.png ├── 3b38551e8c65303682cb2dd770ce2618.png ├── __init__.py ├── bundle.js ├── index.html ├── ledfx_icon.png ├── small_black_alpha.png └── style.css ├── package.json ├── release.py ├── requirements.txt ├── requirements_docs.txt ├── setup.cfg ├── setup.py └── webpack.config.js /.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=./ledfx/ 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 | 15 | # Python env 16 | env 17 | 18 | # NPM 19 | node_modules/ 20 | npm-debug.log 21 | package-lock.json 22 | 23 | # Project files 24 | .ropeproject 25 | .project 26 | .pydevproject 27 | .settings 28 | .idea 29 | .vscode 30 | 31 | # Package files 32 | *.egg 33 | *.eggs/ 34 | .installed.cfg 35 | *.egg-info 36 | 37 | # Unittest and coverage 38 | htmlcov/* 39 | .coverage 40 | .tox 41 | junit.xml 42 | coverage.xml 43 | 44 | # Build and docs folder/files 45 | build/* 46 | dist/* 47 | sdist/* 48 | docs/api/* 49 | docs/_build/* 50 | cover/* 51 | MANIFEST 52 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | addons: 3 | apt: 4 | packages: 5 | - libudev-dev 6 | matrix: 7 | fast_finish: true 8 | include: 9 | - python: "3.6" 10 | env: TOXENV=py36 11 | # #after_success: coveralls 12 | - python: "3.7" 13 | env: TOXENV=py37 14 | dist: xenial 15 | - python: "3.8-dev" 16 | env: TOXENV=py38 17 | dist: xenial 18 | if: branch = dev AND type = push 19 | allow_failures: 20 | - python: "3.8-dev" 21 | env: TOXENV=py38 22 | dist: xenial 23 | 24 | language: python 25 | cache: 26 | directories: 27 | - $HOME/.cache/pip 28 | - "node_modules" 29 | before_install: 30 | - sudo apt-get update 31 | - sudo apt-get install portaudio19-dev # Install portaudio (requirement for pyaudio) 32 | install: 33 | - npm install 34 | - pip install tox-travis 35 | - pip install -r requirements_docs.txt 36 | 37 | script: 38 | # Build the frontend first 39 | - npm run build 40 | # Then, build the backend 41 | - travis_wait 30 tox --develop 42 | # Finally, build the docs 43 | - travis-sphinx build --nowarn --source=docs 44 | 45 | after_success: 46 | - travis-sphinx deploy 47 | -------------------------------------------------------------------------------- /AUTHORS.rst: -------------------------------------------------------------------------------- 1 | ============ 2 | Contributors 3 | ============ 4 | 5 | * Austin Hodges -------------------------------------------------------------------------------- /CHANGELOG.rst: -------------------------------------------------------------------------------- 1 | ========= 2 | Changelog 3 | ========= 4 | 5 | Version 0.2 6 | =========== 7 | 8 | - More effects and support for UDP devices 9 | - Frontend converted to react and more features added 10 | 11 | Version 0.1 12 | =========== 13 | 14 | - Initial release with basic feature set! 15 | - Added a framework for highly customizable effects and outputs 16 | - Added support for E1.31 devices 17 | - Added some basic effects and audio reaction ones 18 | -------------------------------------------------------------------------------- /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 LICENSE.txt -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | ===================================================== 2 | PLEASE NOTE: DEVELOPMENT HAS MOVED TO https://github.com/LedFx/LedFx 3 | ===================================================== 4 | 5 | We've moved! Active development has moved to the `LedFx Official Github `__. LedFx website: https://ledfx.app/ 6 | 7 | We thank ahodges for his spectacular work and hope he comes back soon. 8 | 9 | LedFx 10 | ================================================================================= 11 | |License| |Discord| 12 | 13 | LedFx is a network based LED effect controller with support for advanced real-time audio effects! LedFx can control multiple devices and works great with cheap ESP8266 nodes allowing for cost effectvice syncronized effects across your entire house! 14 | 15 | For installation instructions see the `documentation `__. 16 | 17 | Demos 18 | --------- 19 | 20 | We are actively adding and perfecting the effects, but here is a quick demo of LedFx running three different effects synced across three different ESP8266 devices: 21 | 22 | .. image:: https://raw.githubusercontent.com/ahodges9/LedFx/gh-pages/demos/ledfx_demo.gif 23 | 24 | .. |Build Status| image:: https://travis-ci.org/ahodges9/LedFx.svg?branch=master 25 | :target: https://travis-ci.org/ahodges9/LedFx 26 | .. |License| image:: https://img.shields.io/badge/license-MIT-blue.svg 27 | .. |Discord| image:: https://img.shields.io/badge/chat-on%20discord-7289da.svg 28 | :target: https://discord.gg/xyyHEquZKQ 29 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = -b dirhtml 6 | SPHINXBUILD = python -msphinx 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) -------------------------------------------------------------------------------- /docs/_static/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ahodges9/LedFx/2abeced175fbff5fd4d31f28e6c5a61ad1d042e2/docs/_static/logo.png -------------------------------------------------------------------------------- /docs/api.rst: -------------------------------------------------------------------------------- 1 | ================================ 2 | REST API 3 | ================================ 4 | 5 | The REST APIs are undergoing active development and many of the below APIs are either not yet implemented or not fully functional. This page mainly serves as a reference of what the final APIs will *eventually* look like. 6 | 7 | GET /api/info 8 | ================================ 9 | Returns basic information about the LedFx instance as JSON 10 | 11 | .. code:: json 12 | 13 | { 14 | url: "http://127.0.0.1:8888", 15 | name: "LedFx", 16 | version: "0.0.1" 17 | } 18 | 19 | GET /api/config 20 | ================================ 21 | Returns the current configuration for LedFx as JSON 22 | 23 | GET /api/log 24 | ================================ 25 | Returns the error logs for the currently active LedFx session 26 | 27 | GET /api/schema/devices 28 | ================================ 29 | Returns all the valid schemas for a LedFx device as JSON 30 | 31 | GET /api/schema/effects 32 | ================================ 33 | Returns all the valid schemas for a LedFx effect as JSON 34 | 35 | GET /api/devices 36 | ================================ 37 | Returns all the devices registered with LedFx as JSON 38 | 39 | POST /api/devices 40 | ================================ 41 | Adds a new device to LedFx based on the provided JSON configuration. 42 | 43 | GET /api/devices/ 44 | ================================ 45 | Returns information about the device with the matching device id as JSON 46 | 47 | PUT /api/devices/ 48 | ================================ 49 | Modifies the information pertaining to the device with the matching device id and returns the new device as JSON 50 | 51 | DELETE /api/devices/ 52 | ================================ 53 | Deletes the device with the matching device id. 54 | 55 | GET /api/effects 56 | ================================ 57 | Returns all the effects currently created in LedFx as JSON 58 | 59 | POST /api/effects 60 | ================================ 61 | Create a new Effect based on the provided JSON configuration. 62 | 63 | GET /api/effects/ 64 | ================================ 65 | Returns information about the effect with the matching effect id as JSON 66 | 67 | PUT /api/effects/ 68 | ================================ 69 | Modifies the configuration of the effect with a matching effect id and returns the new configuration as JSON 70 | 71 | DELETE /api/effects/ 72 | ================================ 73 | Deletes the effect with the matching effect id. 74 | 75 | ================================ 76 | WebSocket API 77 | ================================ 78 | 79 | In addition to the REST APIs LedFx has a WebSocket API for streaming realtime data. The primary use for this is for things like effect visualizations in the frontend. 80 | 81 | Will document this further once it is more well defined. The general structure will be event registration based. -------------------------------------------------------------------------------- /docs/contents.rst: -------------------------------------------------------------------------------- 1 | Table of Contents 2 | ----------------- 3 | 4 | .. toctree:: 5 | :maxdepth: 2 6 | 7 | quickstart 8 | developer 9 | api 10 | 11 | License 12 | ------- 13 | 14 | LedFx is licensed under the MIT license. 15 | -------------------------------------------------------------------------------- /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 | Backend Development 9 | ================================ 10 | 11 | **1.** Clone the dev branch from the LedFx Github repository: 12 | 13 | .. code:: bash 14 | 15 | git clone https://github.com/ahodges9/LedFx.git -b dev 16 | cd LedFx 17 | 18 | **2.** Enable development mode to prevent having to reinstall and instead just load from the git repository: 19 | 20 | .. code:: bash 21 | 22 | python setup.py develop 23 | 24 | **3.** This will let you run LedFx directly from your Git repository via: 25 | 26 | .. code:: bash 27 | 28 | ledfx --open-ui 29 | 30 | macOS 31 | ===== 32 | 33 | **1.** Clone the dev branch from the LedFx Github repository: 34 | 35 | .. code:: bash 36 | 37 | git clone https://github.com/ahodges9/LedFx.git -b dev 38 | cd ./ledfx 39 | 40 | **2.** Create a conda environment for LedFx with Python 3.7 and install dependencies: 41 | 42 | .. code:: bash 43 | 44 | conda create -n ledfx python=3.7 45 | conda activate ledfx 46 | conda config --add channels conda-forge 47 | conda install aubio portaudio 48 | 49 | **3.** Install LedFx and its requirements using pip: 50 | 51 | .. code:: bash 52 | 53 | pip install -r requirements.txt 54 | pip install -e . 55 | ledfx --open-ui 56 | 57 | Frontend Development 58 | ================================ 59 | 60 | 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: 61 | 62 | **1.** Start in the LedFx repo directory: 63 | 64 | .. code:: bash 65 | 66 | pip install npm 67 | npm install 68 | 69 | 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). 70 | 71 | **2.** Start LedFx in development mode and start the watcher: 72 | 73 | .. code:: bash 74 | 75 | ledfx --open-ui 76 | npm run watch 77 | 78 | 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. 79 | 80 | **3.** Build the frontend: 81 | 82 | .. code:: bash 83 | 84 | npm run build 85 | 86 | macOS 87 | ===== 88 | 89 | **1.** Install nodejs and NPM requirements using homebrew: 90 | 91 | .. code:: bash 92 | 93 | brew install nodejs 94 | cd ~/ledfx 95 | npm install 96 | 97 | **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``): 98 | 99 | .. code:: bash 100 | 101 | ledfx --open-ui 102 | npm run watch 103 | 104 | **3.** Build the frontend: 105 | 106 | .. code:: bash 107 | 108 | npm run build 109 | 110 | Document Development 111 | ================================ 112 | 113 | The documentation is written in reStructuredText. Once you are finished making changes you must build the documentation. To build the LedFx documentation simply enter the "docs" folder and run the following: 114 | 115 | .. code:: bash 116 | 117 | make html 118 | 119 | macOS 120 | ===== 121 | 122 | .. code:: bash 123 | 124 | conda activate ledfx 125 | cd ~/ledfx 126 | pip install -r requirements_docs.txt 127 | cd ./docs 128 | make html -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. include:: quickstart.rst 2 | 3 | .. include:: contents.rst -------------------------------------------------------------------------------- /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=sphinx-build 9 | ) 10 | set SOURCEDIR=source 11 | set BUILDDIR=build 12 | set SPHINXPROJ=LedFx 13 | 14 | if "%1" == "" goto help 15 | 16 | %SPHINXBUILD% >NUL 2>NUL 17 | if errorlevel 9009 ( 18 | echo. 19 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 20 | echo.installed, then set the SPHINXBUILD environment variable to point 21 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 22 | echo.may add the Sphinx directory to PATH. 23 | echo. 24 | echo.If you don't have Sphinx installed, grab it from 25 | echo.http://sphinx-doc.org/ 26 | exit /b 1 27 | ) 28 | 29 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% 30 | goto end 31 | 32 | :help 33 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% 34 | 35 | :end 36 | popd 37 | -------------------------------------------------------------------------------- /docs/quickstart.rst: -------------------------------------------------------------------------------- 1 | ================================ 2 | Installation and Setup 3 | ================================ 4 | 5 | LedFx is a network controller that aims to enable synchronization of multiple lights across a network. LedFx doesn't currently support local control of LED strings, so you need a separate device (e.g., ESP8266) to control the LEDs directly. To be able to add your LED strips to LedFx your device needs to be capable of receiving data either via the E1.31 sACN protocol or a generic (simple) UDP protocol. See below for a list of tested ESP8266 firmware that can be used with LedFx. 6 | 7 | Here is everything you need to get started with LedFx: 8 | 9 | #. A Computer (or Raspberry Pi) with Python 3.6 or 3.7 (`Anaconda `_ is recommended on Windows) 10 | #. An E1.31 capable device with addressiable LEDs connected 11 | 12 | - Commercial grade DMX controllers 13 | - ESP8266 modules can be purchased for as little as $2 USD from Aliexpress 14 | 15 | Here is a list of tested ESP8266 firmware that works with LedFx: 16 | 17 | - `ESPixelStick `_ is a great E1.31 based firmware 18 | - `Scott's Audio Reactive Firmware `_ which inspired this project! 19 | - `WLED `_ has lots of firmware effects and supports E1.31 and UDP 20 | 21 | Windows Installation 22 | ==================== 23 | To get started on Windows it is highly recommended that you use `Anaconda `_ to make installation of Cython components easier. 24 | 25 | **1.** Create a `conda virtual environment `_ (this step is optional but highly recommended): 26 | 27 | .. code:: bash 28 | 29 | conda create -n ledfx 30 | conda activate ledfx 31 | 32 | **2.** Install LedFx and all the dependencies using pip and the conda package manager: 33 | 34 | .. code:: bash 35 | 36 | conda config --add channels conda-forge 37 | conda install aubio portaudio pywin32 38 | conda install -c anaconda pyaudio 39 | conda install -c anaconda portaudio 40 | pip install ledfx 41 | 42 | **3.** Launch LedFx with the ``--open-ui`` option to launch the browser: 43 | 44 | .. code:: bash 45 | 46 | ledfx --open-ui 47 | 48 | Linux Installation 49 | ================== 50 | To install on Linux first ensure you have at least Python 3.6 installed (alternatively use `Anaconda `_). 51 | 52 | **1.** Install LedFx and all the dependencies using apt-get and pip: 53 | 54 | .. code:: bash 55 | 56 | sudo apt-get install portaudio19-dev 57 | pip install ledfx 58 | 59 | **2.** Launch LedFx with the ``open-ui`` option to launch the browser: 60 | 61 | .. code:: bash 62 | 63 | ledfx --open-ui 64 | 65 | macOS Installation 66 | ================== 67 | To install on macOS first ensure you have at least Python 3.6 installed (alternatively use `Anaconda `_). 68 | 69 | **1.** Install LedFx and all the dependencies using homebrew and pip: 70 | 71 | .. code:: bash 72 | 73 | brew install portaudio 74 | pip install ledfx 75 | 76 | **2.** Launch LedFx with the ``open-ui`` option to launch the browser: 77 | 78 | .. code:: bash 79 | 80 | ledfx --open-ui 81 | 82 | **1.** Alternatively, create a `conda virtual environment `_: 83 | 84 | .. code:: bash 85 | 86 | conda create -n ledfx python=3.7 87 | conda activate ledfx 88 | 89 | **2.** Install LedFx and all the dependencies using pip and the conda package manager. 90 | 91 | .. code:: bash 92 | 93 | conda config --add channels conda-forge 94 | conda install aubio portaudio 95 | pip install ledfx 96 | 97 | **3.** Launch LedFx with the ``open-ui`` option to launch the browser: 98 | 99 | .. code:: bash 100 | 101 | ledfx --open-ui 102 | 103 | Device Configuration 104 | ==================== 105 | Once you have LedFx running, it's time to add some devices! After you have set up a device with appropriate firmware for integration with LedFx, navigate to the 'Device Management' page and click the "Add Device" button at the lower right of the web page. Add the device using the following configuration based on your firmware: 106 | 107 | * `ESPixelStick `_ 108 | 109 | - Add the device as a E1.31 device. The default E1.31 settings should work fine. 110 | 111 | * `Scott's Audio Reactive Firmware `_ 112 | 113 | - Add the device as a UDP 114 | - Click 'Additional Properties' and check 'Include Indexes' 115 | 116 | * `WLED `_ 117 | 118 | - Enabled E1.31 support from the WLED web-interface 119 | - Add the device as an E1.31 device 120 | - If you have more than 170 LEDs click 'Additional Properties' and set the 'Universe Size' to 510 121 | -------------------------------------------------------------------------------- /icons/discord.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ahodges9/LedFx/2abeced175fbff5fd4d31f28e6c5a61ad1d042e2/icons/discord.png -------------------------------------------------------------------------------- /icons/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ahodges9/LedFx/2abeced175fbff5fd4d31f28e6c5a61ad1d042e2/icons/favicon.ico -------------------------------------------------------------------------------- /icons/large_black_alpha.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ahodges9/LedFx/2abeced175fbff5fd4d31f28e6c5a61ad1d042e2/icons/large_black_alpha.png -------------------------------------------------------------------------------- /icons/large_black_on_white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ahodges9/LedFx/2abeced175fbff5fd4d31f28e6c5a61ad1d042e2/icons/large_black_on_white.png -------------------------------------------------------------------------------- /icons/large_white_alpha.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ahodges9/LedFx/2abeced175fbff5fd4d31f28e6c5a61ad1d042e2/icons/large_white_alpha.png -------------------------------------------------------------------------------- /icons/large_white_on_black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ahodges9/LedFx/2abeced175fbff5fd4d31f28e6c5a61ad1d042e2/icons/large_white_on_black.png -------------------------------------------------------------------------------- /icons/small_black_alpha.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ahodges9/LedFx/2abeced175fbff5fd4d31f28e6c5a61ad1d042e2/icons/small_black_alpha.png -------------------------------------------------------------------------------- /icons/small_black_on_white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ahodges9/LedFx/2abeced175fbff5fd4d31f28e6c5a61ad1d042e2/icons/small_black_on_white.png -------------------------------------------------------------------------------- /icons/small_white_alpha.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ahodges9/LedFx/2abeced175fbff5fd4d31f28e6c5a61ad1d042e2/icons/small_white_alpha.png -------------------------------------------------------------------------------- /icons/small_white_on_black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ahodges9/LedFx/2abeced175fbff5fd4d31f28e6c5a61ad1d042e2/icons/small_white_on_black.png -------------------------------------------------------------------------------- /jshint.jshintrc: -------------------------------------------------------------------------------- 1 | { "esversion":6 } -------------------------------------------------------------------------------- /ledfx/__init__.py: -------------------------------------------------------------------------------- 1 | """ledfx""" -------------------------------------------------------------------------------- /ledfx/__main__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | """ 4 | Entry point for the ledfx controller. To run this script for development 5 | purposes use: 6 | 7 | [console_scripts] 8 | python setup.py develop 9 | ledfx 10 | 11 | For non-development purposes run: 12 | 13 | [console_scripts] 14 | python setup.py install 15 | ledfx 16 | 17 | """ 18 | 19 | import argparse 20 | import sys 21 | import logging 22 | 23 | from ledfx.consts import ( 24 | REQUIRED_PYTHON_VERSION, REQUIRED_PYTHON_STRING, 25 | PROJECT_VERSION) 26 | from ledfx.core import LedFxCore 27 | import ledfx.config as config_helpers 28 | 29 | _LOGGER = logging.getLogger(__name__) 30 | 31 | def validate_python() -> None: 32 | """Validate the python version for when manually running""" 33 | 34 | if sys.version_info[:3] < REQUIRED_PYTHON_VERSION: 35 | print(('Python {} is required.').format(REQUIRED_PYTHON_STRING)) 36 | sys.exit(1) 37 | 38 | def setup_logging(loglevel): 39 | logformat = "[%(asctime)s] %(levelname)s:%(name)s:%(message)s" 40 | logging.basicConfig(level=loglevel, stream=sys.stdout, 41 | format=logformat, datefmt="%Y-%m-%d %H:%M:%S") 42 | 43 | # Suppress some of the overly verbose logs 44 | logging.getLogger('sacn').setLevel(logging.WARNING) 45 | logging.getLogger('aiohttp.access').setLevel(logging.WARNING) 46 | 47 | def parse_args(): 48 | parser = argparse.ArgumentParser( 49 | description="A Networked LED Effect Controller") 50 | parser.add_argument( 51 | '--version', 52 | action='version', 53 | version='ledfx {ver}'.format(ver=PROJECT_VERSION)) 54 | parser.add_argument( 55 | '-c', 56 | '--config', 57 | dest="config", 58 | help="Directory that contains the configuration files", 59 | default=config_helpers.get_default_config_directory(), 60 | type=str) 61 | parser.add_argument( 62 | '--open-ui', 63 | action='store_true', 64 | help='Automatically open the webinterface') 65 | parser.add_argument( 66 | '-v', 67 | '--verbose', 68 | dest="loglevel", 69 | help="set loglevel to INFO", 70 | action='store_const', 71 | const=logging.INFO) 72 | parser.add_argument( 73 | '-vv', 74 | '--very-verbose', 75 | dest="loglevel", 76 | help="set loglevel to DEBUG", 77 | action='store_const', 78 | const=logging.DEBUG) 79 | return parser.parse_args() 80 | 81 | def main(): 82 | """Main entry point allowing external calls""" 83 | 84 | args = parse_args() 85 | config_helpers.ensure_config_directory(args.config) 86 | setup_logging(args.loglevel) 87 | 88 | ledfx = LedFxCore(config_dir = args.config) 89 | _LOGGER.critical("THIS GIT REPO IS NO LONGER MAINTAINED. PLEASE UPDATE YOUR SOURCES TO THE OFFICIAL LEDFX GITHUB - GIT.LEDFX.APP") 90 | ledfx.start(open_ui = args.open_ui) 91 | 92 | if __name__ == "__main__": 93 | sys.exit(main()) 94 | -------------------------------------------------------------------------------- /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.Response(text=json.dumps(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.Response(text=json.dumps(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.Response(text=json.dumps(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.Response(text=json.dumps(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.Response(text=json.dumps(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.Response(text=json.dumps(response), status=404) 19 | 20 | response = device.config 21 | return web.Response(text=json.dumps(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.Response(text=json.dumps(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.Response(text=json.dumps(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.Response(text=json.dumps(response), status=500) 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.Response(text=json.dumps(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.Response(text=json.dumps(response), status=200) -------------------------------------------------------------------------------- /ledfx/api/device_effects.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 EffectsEndpoint(RestEndpoint): 10 | 11 | ENDPOINT_PATH = "/api/devices/{device_id}/effects" 12 | 13 | async def get(self, device_id) -> web.Response: 14 | device = self._ledfx.devices.get(device_id) 15 | if device is None: 16 | response = { 'not found': 404 } 17 | return web.Response(text=json.dumps(response), status=404) 18 | 19 | # Get the active effect 20 | response = { 'effect' : {}} 21 | if device.active_effect: 22 | effect_response = {} 23 | effect_response['config'] = device.active_effect.config 24 | effect_response['name'] = device.active_effect.name 25 | effect_response['type'] = device.active_effect.type 26 | response = { 'effect' : effect_response } 27 | 28 | return web.Response(text=json.dumps(response), status=200) 29 | 30 | async def put(self, device_id, request) -> web.Response: 31 | device = self._ledfx.devices.get(device_id) 32 | if device is None: 33 | response = { 'not found': 404 } 34 | return web.Response(text=json.dumps(response), status=404) 35 | 36 | data = await request.json() 37 | effect_type = data.get('type') 38 | if effect_type is None: 39 | response = { 'status' : 'failed', 'reason': 'Required attribute "type" was not provided' } 40 | return web.Response(text=json.dumps(response), status=500) 41 | 42 | effect_config = data.get('config') 43 | if effect_config is None: 44 | effect_config = {} 45 | 46 | # Create the effect and add it to the device 47 | effect = self._ledfx.effects.create( 48 | ledfx = self._ledfx, 49 | type = effect_type, 50 | config = effect_config) 51 | device.set_effect(effect) 52 | 53 | # Update and save the configuration 54 | for device in self._ledfx.config['devices']: 55 | if (device['id'] == device_id): 56 | #if not ('effect' in device): 57 | device['effect'] = {} 58 | device['effect']['type'] = effect_type 59 | device['effect']['config'] = effect_config 60 | break 61 | save_config( 62 | config = self._ledfx.config, 63 | config_dir = self._ledfx.config_dir) 64 | 65 | effect_response = {} 66 | effect_response['config'] = effect.config 67 | effect_response['name'] = effect.name 68 | effect_response['type'] = effect.type 69 | 70 | response = { 'status' : 'success', 'effect' : effect_response} 71 | return web.Response(text=json.dumps(response), status=200) 72 | 73 | async def post(self, device_id, request) -> web.Response: 74 | device = self._ledfx.devices.get(device_id) 75 | if device is None: 76 | response = { 'not found': 404 } 77 | return web.Response(text=json.dumps(response), status=404) 78 | 79 | data = await request.json() 80 | effect_type = data.get('type') 81 | if effect_type is None: 82 | response = { 'status' : 'failed', 'reason': 'Required attribute "type" was not provided' } 83 | return web.Response(text=json.dumps(response), status=500) 84 | 85 | effect_config = data.get('config') 86 | if effect_config is None: 87 | effect_config = {} 88 | 89 | # Create the effect and add it to the device 90 | effect = self._ledfx.effects.create( 91 | ledfx = self._ledfx, 92 | type = effect_type, 93 | config = effect_config) 94 | device.set_effect(effect) 95 | 96 | # Update and save the configuration 97 | for device in self._ledfx.config['devices']: 98 | if (device['id'] == device_id): 99 | #if not ('effect' in device): 100 | device['effect'] = {} 101 | device['effect']['type'] = effect_type 102 | device['effect']['config'] = effect_config 103 | break 104 | save_config( 105 | config = self._ledfx.config, 106 | config_dir = self._ledfx.config_dir) 107 | 108 | effect_response = {} 109 | effect_response['config'] = effect.config 110 | effect_response['name'] = effect.name 111 | effect_response['type'] = effect.type 112 | 113 | response = { 'status' : 'success', 'effect' : effect_response} 114 | return web.Response(text=json.dumps(response), status=200) 115 | 116 | async def delete(self, device_id) -> web.Response: 117 | device = self._ledfx.devices.get(device_id) 118 | if device is None: 119 | response = { 'not found': 404 } 120 | return web.Response(text=json.dumps(response), status=404) 121 | 122 | # Clear the effect 123 | device.clear_effect() 124 | 125 | for device in self._ledfx.config['devices']: 126 | if (device['id'] == device_id): 127 | del device['effect'] 128 | break 129 | save_config( 130 | config = self._ledfx.config, 131 | config_dir = self._ledfx.config_dir) 132 | 133 | response = { 'status' : 'success', 'effect' : {} } 134 | return web.Response(text=json.dumps(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.Response(text=json.dumps(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.Response(text=json.dumps(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.Response(text=json.dumps(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.Response(text=json.dumps(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.Response(text=json.dumps(response), status=404) 17 | 18 | response = { 'schema' : str(effect.schema()) } 19 | return web.Response(text=json.dumps(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.Response(text=json.dumps(response), status=200) 19 | -------------------------------------------------------------------------------- /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.Response(text=json.dumps(response), status=200) 23 | -------------------------------------------------------------------------------- /ledfx/api/presets.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 PresetsEndpoint(RestEndpoint): 11 | """REST end-point for querying and managing presets""" 12 | 13 | ENDPOINT_PATH = "/api/presets" 14 | 15 | async def get(self) -> web.Response: 16 | """Get all presets""" 17 | response = { 18 | 'status' : 'success' , 19 | 'presets' : self._ledfx.config['presets'] 20 | } 21 | return web.Response(text=json.dumps(response), status=200) 22 | 23 | async def delete(self, request) -> web.Response: 24 | """Delete a preset""" 25 | data = await request.json() 26 | 27 | preset_id = data.get('id') 28 | if preset_id is None: 29 | response = { 'status' : 'failed', 'reason': 'Required attribute "preset_id" was not provided' } 30 | return web.Response(text=json.dumps(response), status=500) 31 | 32 | if not preset_id in self._ledfx.config['presets'].keys(): 33 | response = { 'status' : 'failed', 'reason': 'Preset {} does not exist'.format(preset_id) } 34 | return web.Response(text=json.dumps(response), status=500) 35 | 36 | # Delete the preset from configuration 37 | del self._ledfx.config['presets'][preset_id] 38 | 39 | # Save the config 40 | save_config( 41 | config = self._ledfx.config, 42 | config_dir = self._ledfx.config_dir) 43 | 44 | response = { 'status' : 'success' } 45 | return web.Response(text=json.dumps(response), status=200) 46 | 47 | async def put(self, request) -> web.Response: 48 | """Activate a preset""" 49 | data = await request.json() 50 | 51 | action = data.get('action') 52 | if action is None: 53 | response = { 'status' : 'failed', 'reason': 'Required attribute "action" was not provided' } 54 | return web.Response(text=json.dumps(response), status=500) 55 | 56 | if action not in ['activate', 'rename']: 57 | response = { 'status' : 'failed', 'reason': 'Invalid action "{}"'.format(action) } 58 | return web.Response(text=json.dumps(response), status=500) 59 | 60 | preset_id = data.get('id') 61 | if preset_id is None: 62 | response = { 'status' : 'failed', 'reason': 'Required attribute "preset_id" was not provided' } 63 | return web.Response(text=json.dumps(response), status=500) 64 | 65 | if not preset_id in self._ledfx.config['presets'].keys(): 66 | response = { 'status' : 'failed', 'reason': 'Preset "{}" does not exist'.format(preset_id) } 67 | return web.Response(text=json.dumps(response), status=500) 68 | 69 | preset = self._ledfx.config['presets'][preset_id] 70 | 71 | if action == "activate": 72 | for device in self._ledfx.devices.values(): 73 | # Check device is in preset, make no changes if it isn't 74 | if not device.id in preset['devices'].keys(): 75 | _LOGGER.info(('Device with id {} has no data in preset {}').format(device.id, preset_id)) 76 | continue 77 | 78 | # Set effect of device to that saved in the preset, 79 | # clear active effect of device if no effect in preset 80 | if preset['devices'][device.id]: 81 | # Create the effect and add it to the device 82 | effect = self._ledfx.effects.create( 83 | ledfx = self._ledfx, 84 | type = preset['devices'][device.id]['type'], 85 | config = preset['devices'][device.id]['config']) 86 | device.set_effect(effect) 87 | else: 88 | device.clear_effect() 89 | 90 | elif action == "rename": 91 | name = data.get('name') 92 | if name is None: 93 | response = { 'status' : 'failed', 'reason': 'Required attribute "name" was not provided' } 94 | return web.Response(text=json.dumps(response), status=500) 95 | 96 | # Update and save config 97 | self._ledfx.config['presets'][preset_id]['name'] = name 98 | save_config( 99 | config = self._ledfx.config, 100 | config_dir = self._ledfx.config_dir) 101 | 102 | response = { 'status' : 'success' } 103 | return web.Response(text=json.dumps(response), status=200) 104 | 105 | async def post(self, request) -> web.Response: 106 | """Save current effects of devices as a preset""" 107 | data = await request.json() 108 | 109 | preset_name = data.get('name') 110 | if preset_name is None: 111 | response = { 'status' : 'failed', 'reason': 'Required attribute "preset_name" was not provided' } 112 | return web.Response(text=json.dumps(response), status=500) 113 | 114 | preset_id = generate_id(preset_name) 115 | 116 | preset_config = {} 117 | preset_config['name'] = preset_name 118 | preset_config['devices'] = {} 119 | for device in self._ledfx.devices.values(): 120 | effect = {} 121 | if device.active_effect: 122 | effect['type'] = device.active_effect.type 123 | effect['config'] = device.active_effect.config 124 | #effect['name'] = device.active_effect.name 125 | preset_config['devices'][device.id] = effect 126 | 127 | # Update the preset if it already exists, else create it 128 | self._ledfx.config['presets'][preset_id] = preset_config 129 | 130 | save_config( 131 | config = self._ledfx.config, 132 | config_dir = self._ledfx.config_dir) 133 | 134 | response = { 'status' : 'success', 'preset': {'id': preset_id, 'config': preset_config }} 135 | return web.Response(text=json.dumps(response), status=200) -------------------------------------------------------------------------------- /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.Response(text=json.dumps(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.Response(text=json.dumps(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.Response(text=json.dumps(response), status=200) 33 | else: 34 | return web.Response(text=json.dumps(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.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/config.py: -------------------------------------------------------------------------------- 1 | import voluptuous as vol 2 | import logging 3 | import yaml 4 | import sys 5 | import os 6 | 7 | _LOGGER = logging.getLogger(__name__) 8 | 9 | CONFIG_DIRECTORY = '.ledfx' 10 | CONFIG_FILE_NAME = 'config.yaml' 11 | 12 | CORE_CONFIG_SCHEMA = vol.Schema({ 13 | vol.Optional('host', default = '127.0.0.1'): str, 14 | vol.Optional('port', default = 8888): int, 15 | vol.Optional('dev_mode', default = False): bool, 16 | vol.Optional('max_workers', default = 10): int, 17 | vol.Optional('devices', default = []): list, 18 | vol.Optional('presets', default = {}): dict 19 | }, extra=vol.ALLOW_EXTRA) 20 | 21 | def get_default_config_directory() -> str: 22 | """Get the default configuration directory""" 23 | 24 | base_dir = os.getenv('APPDATA') if os.name == "nt" \ 25 | else os.path.expanduser('~') 26 | return os.path.join(base_dir, CONFIG_DIRECTORY) 27 | 28 | def get_config_file(config_dir: str) -> str: 29 | """Finds a supported configuration fill in the provided directory""" 30 | 31 | config_path = os.path.join(config_dir, CONFIG_FILE_NAME) 32 | return config_path if os.path.isfile(config_path) else None 33 | 34 | def create_default_config(config_dir: str) -> str: 35 | """Creates a default configuration in the provided directory""" 36 | 37 | config_path = os.path.join(config_dir, CONFIG_FILE_NAME) 38 | try: 39 | with open(config_path, 'wt') as file: 40 | yaml.dump(CORE_CONFIG_SCHEMA({}), file, default_flow_style=False) 41 | return config_path 42 | 43 | except IOError: 44 | print(('Unable to create default configuration file {}').format(config_path)) 45 | return None 46 | 47 | def ensure_config_file(config_dir: str) -> str: 48 | """Checks if a config file exsit, and otherwise creates one""" 49 | 50 | ensure_config_directory(config_dir) 51 | config_path = get_config_file(config_dir) 52 | if config_path is None: 53 | config_path = create_default_config(config_dir) 54 | 55 | return config_path 56 | 57 | def ensure_config_directory(config_dir: str) -> None: 58 | """Validate that the config directory is valid.""" 59 | 60 | # If an explict path is provided simply check if it exist and failfast 61 | # if it doesn't. Otherwise, if we have the default directory attempt to 62 | # create the file 63 | if not os.path.isdir(config_dir): 64 | if config_dir != get_default_config_directory(): 65 | print(('Error: Invalid configuration directory {}').format(config_dir)) 66 | sys.exit(1) 67 | 68 | try: 69 | os.mkdir(config_dir) 70 | except OSError: 71 | print(('Error: Unable to create configuration directory {}').format(config_dir)) 72 | sys.exit(1) 73 | 74 | def load_config(config_dir: str) -> dict: 75 | """Validates and loads the configuration file in the provided directory""" 76 | 77 | config_file = ensure_config_file(config_dir) 78 | print(('Loading configuration file from {}').format(config_dir)) 79 | with open(config_file, 'rt') as file: 80 | config_yaml = yaml.safe_load(file) 81 | if config_yaml is None: 82 | config_yaml = {} 83 | return CORE_CONFIG_SCHEMA(config_yaml) 84 | 85 | def save_config(config: dict, config_dir: str) -> None: 86 | """Saves the configuration to the provided directory""" 87 | 88 | config_file = ensure_config_file(config_dir) 89 | _LOGGER.info(('Saving configuration file to {}').format(config_dir)) 90 | with open(config_file, 'w') as file: 91 | yaml.dump(config, file, default_flow_style=False) 92 | -------------------------------------------------------------------------------- /ledfx/consts.py: -------------------------------------------------------------------------------- 1 | import pathlib 2 | 3 | __author__ = "Austin Hodges" 4 | __copyright__ = "Austin Hodges" 5 | __license__ = "mit" 6 | 7 | REQUIRED_PYTHON_VERSION = (3, 6, 0) 8 | REQUIRED_PYTHON_STRING = '>={}.{}.{}'.format(REQUIRED_PYTHON_VERSION[0], 9 | REQUIRED_PYTHON_VERSION[1], 10 | REQUIRED_PYTHON_VERSION[2]) 11 | 12 | MAJOR_VERSION = 0 13 | MINOR_VERSION = 7 14 | PROJECT_VERSION = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) 15 | 16 | __version__ = PROJECT_VERSION 17 | -------------------------------------------------------------------------------- /ledfx/core.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import logging 3 | import sys 4 | import json 5 | import yaml 6 | import threading 7 | from pathlib import Path 8 | import voluptuous as vol 9 | from concurrent.futures import ThreadPoolExecutor 10 | from ledfx.utils import async_fire_and_forget 11 | from ledfx.http import HttpServer 12 | from ledfx.devices import Devices 13 | from ledfx.effects import Effects 14 | from ledfx.config import load_config, save_config 15 | from ledfx.events import Events, LedFxShutdownEvent 16 | 17 | _LOGGER = logging.getLogger(__name__) 18 | 19 | 20 | class LedFxCore(object): 21 | def __init__(self, config_dir): 22 | self.config_dir = config_dir 23 | self.config = load_config(config_dir) 24 | 25 | if sys.platform == 'win32': 26 | self.loop = asyncio.ProactorEventLoop() 27 | else: 28 | self.loop = asyncio.get_event_loop() 29 | executor_opts = {'max_workers': self.config.get('max_workers')} 30 | 31 | self.executor = ThreadPoolExecutor(**executor_opts) 32 | self.loop.set_default_executor(self.executor) 33 | self.loop.set_exception_handler(self.loop_exception_handler) 34 | 35 | self.events = Events(self) 36 | self.http = HttpServer( 37 | ledfx=self, host=self.config['host'], port=self.config['port']) 38 | self.exit_code = None 39 | 40 | def dev_enabled(self): 41 | return self.config['dev_mode'] == True 42 | 43 | def loop_exception_handler(self, loop, context): 44 | kwargs = {} 45 | exception = context.get('exception') 46 | if exception: 47 | kwargs['exc_info'] = (type(exception), exception, 48 | exception.__traceback__) 49 | 50 | _LOGGER.error( 51 | 'Exception in core event loop: {}'.format(context['message']), 52 | **kwargs) 53 | 54 | async def flush_loop(self): 55 | await asyncio.sleep(0, loop=self.loop) 56 | 57 | def start(self, open_ui=False): 58 | async_fire_and_forget(self.async_start(open_ui=open_ui), self.loop) 59 | 60 | # Windows does not seem to handle Ctrl+C well so as a workaround 61 | # register a handler and manually stop the app 62 | if sys.platform == 'win32': 63 | import win32api 64 | 65 | def handle_win32_interrupt(sig, func=None): 66 | self.stop() 67 | return True 68 | 69 | win32api.SetConsoleCtrlHandler(handle_win32_interrupt, 1) 70 | 71 | try: 72 | self.loop.run_forever() 73 | except KeyboardInterrupt: 74 | self.loop.call_soon_threadsafe(self.loop.create_task, 75 | self.async_stop()) 76 | self.loop.run_forever() 77 | except: 78 | # Catch all other exceptions and terminate the application. The loop 79 | # exeception handler will take care of logging the actual error and 80 | # LedFx will cleanly shutdown. 81 | self.loop.run_until_complete(self.async_stop(exit_code = -1)) 82 | pass 83 | finally: 84 | self.loop.stop() 85 | return self.exit_code 86 | 87 | async def async_start(self, open_ui=False): 88 | _LOGGER.info("Starting ledfx") 89 | await self.http.start() 90 | 91 | self.devices = Devices(self) 92 | self.effects = Effects(self) 93 | 94 | # TODO: Deferr 95 | self.devices.create_from_config(self.config['devices']) 96 | 97 | if open_ui: 98 | import webbrowser 99 | webbrowser.open(self.http.base_url) 100 | 101 | await self.flush_loop() 102 | 103 | def stop(self, exit_code=0): 104 | async_fire_and_forget(self.async_stop(exit_code), self.loop) 105 | 106 | async def async_stop(self, exit_code=0): 107 | if not self.loop: 108 | return 109 | 110 | print('Stopping LedFx.') 111 | 112 | # Fire a shutdown event and flush the loop 113 | self.events.fire_event(LedFxShutdownEvent()) 114 | await asyncio.sleep(0, loop=self.loop) 115 | 116 | await self.http.stop() 117 | 118 | # Cancel all the remaining task and wait 119 | tasks = [task for task in asyncio.Task.all_tasks() if task is not 120 | asyncio.tasks.Task.current_task()] 121 | list(map(lambda task: task.cancel(), tasks)) 122 | results = await asyncio.gather(*tasks, return_exceptions=True) 123 | 124 | # Save the configuration before shutting down 125 | save_config(config=self.config, config_dir=self.config_dir) 126 | 127 | await self.flush_loop() 128 | self.executor.shutdown() 129 | self.exit_code = exit_code 130 | self.loop.stop() 131 | -------------------------------------------------------------------------------- /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/e131.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from ledfx.devices import Device 3 | import voluptuous as vol 4 | import numpy as np 5 | import sacn 6 | import time 7 | 8 | _LOGGER = logging.getLogger(__name__) 9 | 10 | 11 | class E131Device(Device): 12 | """E1.31 device support""" 13 | 14 | CONFIG_SCHEMA = vol.Schema({ 15 | vol.Required('ip_address', description='Hostname or IP address of the device'): str, 16 | vol.Required('pixel_count', description='Number of individual pixels'): vol.All(vol.Coerce(int), vol.Range(min=1)), 17 | vol.Optional('universe', description='DMX universe for the device', default=1): vol.All(vol.Coerce(int), vol.Range(min=1)), 18 | vol.Optional('universe_size', description='Size of each DMX universe', default=512): vol.All(vol.Coerce(int), vol.Range(min=1)), 19 | vol.Optional('channel_offset', description='Channel offset within the DMX universe', default=0): vol.All(vol.Coerce(int), vol.Range(min=0)) 20 | }) 21 | 22 | def __init__(self, ledfx, config): 23 | super().__init__(ledfx, config) 24 | 25 | # Allow for configuring in terms of "pixels" or "channels" 26 | if 'pixel_count' in self._config: 27 | self._config['channel_count'] = self._config['pixel_count'] * 3 28 | else: 29 | self._config['pixel_count'] = self._config['channel_count'] // 3 30 | 31 | span = self._config['channel_offset'] + self._config['channel_count'] - 1 32 | self._config['universe_end'] = self._config['universe'] + int(span / self._config['universe_size']) 33 | if span % self._config['universe_size'] == 0: 34 | self._config['universe_end'] -= 1 35 | 36 | self._sacn = None 37 | 38 | @property 39 | def pixel_count(self): 40 | return int(self._config['pixel_count']) 41 | 42 | def activate(self): 43 | if self._sacn: 44 | raise Exception('sACN sender already started.') 45 | 46 | # Configure sACN and start the dedicated thread to flush the buffer 47 | self._sacn = sacn.sACNsender() 48 | for universe in range(self._config['universe'], self._config['universe_end'] + 1): 49 | _LOGGER.info("sACN activating universe {}".format(universe)) 50 | self._sacn.activate_output(universe) 51 | if (self._config['ip_address'] == None): 52 | self._sacn[universe].multicast = True 53 | else: 54 | self._sacn[universe].destination = self._config['ip_address'] 55 | self._sacn[universe].multicast = False 56 | #self._sacn.fps = 60 57 | self._sacn.start() 58 | 59 | _LOGGER.info("sACN sender started.") 60 | super().activate() 61 | 62 | def deactivate(self): 63 | super().deactivate() 64 | 65 | if not self._sacn: 66 | raise Exception('sACN sender not started.') 67 | 68 | # Turn off all the LEDs when deactivating. With how the sender 69 | # works currently we need to sleep to ensure the pixels actually 70 | # get updated. Need to replace the sACN sender such that flush 71 | # directly writes the pixels. 72 | self.flush(np.zeros(self._config['channel_count'])) 73 | time.sleep(1.5) 74 | 75 | self._sacn.stop() 76 | self._sacn = None 77 | _LOGGER.info("sACN sender stopped.") 78 | 79 | 80 | def flush(self, data): 81 | """Flush the data to all the E1.31 channels account for spanning universes""" 82 | 83 | if not self._sacn: 84 | raise Exception('sACN sender not started.') 85 | if data.size != self._config['channel_count']: 86 | raise Exception('Invalid buffer size. ({} != {})'.format( 87 | data.size, self._config['channel_count'])) 88 | 89 | data = data.flatten() 90 | current_index = 0 91 | for universe in range(self._config['universe'], self._config['universe_end'] + 1): 92 | # Calculate offset into the provide input buffer for the channel. There are some 93 | # cleaner ways this can be done... This is just the quick and dirty 94 | universe_start = (universe - self._config['universe']) * self._config['universe_size'] 95 | universe_end = (universe - self._config['universe'] + 1) * self._config['universe_size'] 96 | 97 | dmx_start = max(universe_start, self._config['channel_offset']) % self._config['universe_size'] 98 | dmx_end = min(universe_end, self._config['channel_offset'] + self._config['channel_count']) % self._config['universe_size'] 99 | if dmx_end == 0: 100 | dmx_end = self._config['universe_size'] 101 | 102 | input_start = current_index 103 | input_end = current_index + dmx_end - dmx_start 104 | current_index = input_end 105 | 106 | dmx_data = np.array(self._sacn[universe].dmx_data) 107 | dmx_data[dmx_start:dmx_end] = data[input_start:input_end] 108 | 109 | self._sacn[universe].dmx_data = dmx_data.clip(0,255) 110 | 111 | # # Hack up a manual flush of the E1.31 data vs having a background thread 112 | # if self._sacn._output_thread._socket: 113 | # for output in list(self._sacn._output_thread._outputs.values()): 114 | # self._sacn._output_thread.send_out(output) -------------------------------------------------------------------------------- /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/beat(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 BeatAudioEffect(AudioReactiveEffect, GradientEffect): 7 | 8 | NAME = "Beat" 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 > 0.7: 24 | # self.pixels = self.apply_gradient(1.0) 25 | # else: 26 | # self.pixels = self.apply_gradient(0.0) 27 | if magnitude > 1.0: 28 | magnitude = 1.0 29 | self.pixels = self.apply_gradient(magnitude) 30 | -------------------------------------------------------------------------------- /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/ahodges9/LedFx/2abeced175fbff5fd4d31f28e6c5a61ad1d042e2/ledfx/effects/effectlets/droplet_0.npy -------------------------------------------------------------------------------- /ledfx/effects/effectlets/droplet_1.npy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ahodges9/LedFx/2abeced175fbff5fd4d31f28e6c5a61ad1d042e2/ledfx/effects/effectlets/droplet_1.npy -------------------------------------------------------------------------------- /ledfx/effects/effectlets/droplet_2.npy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ahodges9/LedFx/2abeced175fbff5fd4d31f28e6c5a61ad1d042e2/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_lows', description='Color of low, bassy sounds', default = "red"): vol.In(list(COLORS.keys())), 13 | vol.Optional('color_mids', description='Color of midrange sounds', default = "green"): vol.In(list(COLORS.keys())), 14 | vol.Optional('color_high', description='Color of high sounds', default = "blue"): vol.In(list(COLORS.keys())), 15 | vol.Optional('sensitivity', description='Responsiveness to changes in sound', default = 0.7): vol.All(vol.Coerce(float), vol.Range(min=0.2, max=0.99)), 16 | vol.Optional('mixing_mode', description='Mode of combining colours', default = "overlap"): vol.In(["additive", "overlap"]), 17 | }) 18 | 19 | def config_updated(self, config): 20 | # scale decay value between 0.1 and 0.2 21 | decay_sensitivity = (self._config["sensitivity"]-0.2)*0.25 22 | self._p_filter = self.create_filter( 23 | alpha_decay = decay_sensitivity, 24 | alpha_rise = self._config["sensitivity"]) 25 | 26 | self.lows_colour = np.array(COLORS[self._config['color_lows']], dtype=float) 27 | self.mids_colour = np.array(COLORS[self._config['color_mids']], dtype=float) 28 | self.high_colour = np.array(COLORS[self._config['color_high']], dtype=float) 29 | 30 | def audio_data_updated(self, data): 31 | 32 | # Calculate the low, mids, and high indexes scaling based on the pixel count 33 | lows_idx = int(np.mean(self.pixel_count * data.melbank_lows())) 34 | mids_idx = int(np.mean(self.pixel_count * data.melbank_mids())) 35 | highs_idx = int(np.mean(self.pixel_count * data.melbank_highs())) 36 | 37 | # Build the new energy profile based on the mids, highs and lows setting 38 | # the colors as red, green, and blue channel respectively 39 | p = np.zeros(np.shape(self.pixels)) 40 | if self._config["mixing_mode"] == "additive": 41 | p[:lows_idx] = self.lows_colour 42 | p[:mids_idx] += self.mids_colour 43 | p[:highs_idx] += self.high_colour 44 | elif self._config["mixing_mode"] == "overlap": 45 | p[:lows_idx] = self.lows_colour 46 | p[:mids_idx] = self.mids_colour 47 | p[:highs_idx] = self.high_colour 48 | 49 | # Filter and update the pixel values 50 | self.pixels = self._p_filter.update(p) 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('gradient_method', description='Function used to generate gradient', default = 'bezier'): vol.In(["cubic_ease", "bezier"]), 18 | }) 19 | 20 | def config_updated(self, config): 21 | self.location = 1 22 | self.forward = True 23 | 24 | def effect_loop(self): 25 | if self.location in (0, 500): 26 | self.forward = not self.forward 27 | if self.forward: 28 | self.location += 1 29 | else: 30 | self.location -= 1 31 | color = self.get_gradient_color(self.location/500.0) 32 | self.pixels = np.tile(color, (self.pixel_count, 1)) -------------------------------------------------------------------------------- /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='Modulate brightness', default = False): bool, 22 | vol.Optional('modulation_effect', default = "sine", description="Modulation effect"): vol.In(list(["sine", "breath", "flutter"])), 23 | vol.Optional('modulation_speed', default = 1.0, description="Modulation 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 | elif self._config["modulation_effect"] == "flutter": 60 | self._counter += self._config["modulation_speed"] 61 | if self._counter == 9*_rate: 62 | self._counter = 0 63 | 64 | pixels[int(self._breath_cycle[int(self._counter)] * self.pixel_count):, :] = 0 65 | return pixels 66 | 67 | else: 68 | # LOG that unknown mode selected somehow? 69 | return pixels -------------------------------------------------------------------------------- /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 of the 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 | win_s = 1024 22 | hop_s = 48000 // 60 23 | tolerance = 0.8 24 | 25 | # TODO: Move into the base audio effect class 26 | self.pitch_o = aubio.pitch("schmitt", win_s, hop_s, 48000) 27 | self.pitch_o.set_unit("midi") 28 | self.pitch_o.set_tolerance(tolerance) 29 | 30 | self.avg_midi = None 31 | 32 | 33 | def audio_data_updated(self, data): 34 | y = data.interpolated_melbank(self.pixel_count, filtered = False) 35 | midi_value = self.pitch_o(data.audio_sample())[0] 36 | note_color = COLORS['black'] 37 | if not self.avg_midi: 38 | self.avg_midi = midi_value 39 | 40 | # Average out the midi values to be a little more stable 41 | if midi_value >= MIN_MIDI: 42 | self.avg_midi = self.avg_midi * (1.0 - self._config['responsiveness']) + midi_value * self._config['responsiveness'] 43 | 44 | # Grab the note color based on where it falls in the midi range 45 | if self.avg_midi >= MIN_MIDI: 46 | midi_scaled = (self.avg_midi - MIN_MIDI) / (MAX_MIDI - MIN_MIDI) 47 | 48 | note_color = self.get_gradient_color(midi_scaled) 49 | 50 | # Mix in the new color based on the filterbank information and fade out 51 | # the old colors 52 | new_pixels = self.pixels 53 | for index in range(self.pixel_count): 54 | new_color = mix_colors(self.pixels[index], note_color, y[index]) 55 | new_color = mix_colors(new_color, COLORS['black'], self._config['fade_rate']) 56 | new_pixels[index] = new_color 57 | 58 | # Set the pixels 59 | self.pixels = new_pixels 60 | -------------------------------------------------------------------------------- /ledfx/effects/rain(Reactive).py: -------------------------------------------------------------------------------- 1 | from ledfx.effects.audio import AudioReactiveEffect 2 | from ledfx.effects.effectlets import EFFECTLET_LIST 3 | from ledfx.color import COLORS 4 | import voluptuous as vol 5 | import numpy as np 6 | from random import randint 7 | import os.path 8 | 9 | class RainAudioEffect(AudioReactiveEffect): 10 | 11 | NAME = "Rain" 12 | CONFIG_SCHEMA = vol.Schema({ 13 | vol.Optional('mirror', description='Mirror the effect', default = True): bool, 14 | # TODO drops should be controlled by some sort of effectlet class, which will provide a list of available drop names rather than just this static range 15 | vol.Optional('raindrop_animation', description='Animation style for each drop', default = EFFECTLET_LIST[0]): vol.In(list(EFFECTLET_LIST)), 16 | vol.Optional('lows_colour', description='Colour for low sounds, ie beats', default = 'white'): vol.In(list(COLORS.keys())), 17 | vol.Optional('mids_colour', description='Colour for mid sounds, ie vocals', default = 'red'): vol.In(list(COLORS.keys())), 18 | vol.Optional('high_colour', description='Colour for high sounds, ie hi hat', default = 'blue'): vol.In(list(COLORS.keys())), 19 | vol.Optional('lows_sensitivity', description='Sensitivity to low sounds', default = 0.2): vol.All(vol.Coerce(float), vol.Range(min=0.0, max=1.0)), 20 | vol.Optional('mids_sensitivity', description='Sensitivity to mid sounds', default = 0.1): vol.All(vol.Coerce(float), vol.Range(min=0.0, max=1.0)), 21 | vol.Optional('high_sensitivity', description='Sensitivity to high sounds', default = 0.03): vol.All(vol.Coerce(float), vol.Range(min=0.0, max=1.0)), 22 | }) 23 | 24 | def config_updated(self, config): 25 | # this could be cleaner but it's temporary, until an effectlet class is made to handle this stuff 26 | self.drop_animation = np.load(os.path.join(os.path.dirname(__file__), "effectlets/" + config['raindrop_animation'])) 27 | 28 | self.n_frames, self.frame_width = np.shape(self.drop_animation) 29 | self.frame_centre_index = self.frame_width//2 30 | self.frame_side_lengths = self.frame_centre_index - 1 31 | 32 | self.intensity_filter = self.create_filter( 33 | alpha_decay = 0.5, 34 | alpha_rise = 0.99) 35 | self.filtered_intensities = np.zeros(3) 36 | 37 | self.first_call = True 38 | 39 | def new_drop(self, location, colour): 40 | """ 41 | Add a new drop animation 42 | TODO (?) this method overwrites a running drop animation in the same location 43 | would need a significant restructure to fix 44 | """ 45 | self.drop_frames[location] = 1 46 | self.drop_colours[:, location] = colour 47 | 48 | def update_drop_frames(self): 49 | # TODO these should be made in config_updated or __init__ when pixel count is available there 50 | if self.first_call: 51 | self.drop_frames = np.zeros(self.pixel_count, dtype=int) 52 | self.drop_colours = np.zeros((3, self.pixel_count)) 53 | self.first_call = False 54 | 55 | # Set any drops at final frame back to 0 and remove colour data 56 | finished_drops = self.drop_frames >= self.n_frames - 1 57 | self.drop_frames[finished_drops] = 0 58 | self.drop_colours[:, finished_drops] = 0 59 | # Add one to any running frames 60 | self.drop_frames[self.drop_frames > 0] += 1 61 | 62 | def get_drops(self): 63 | """ 64 | Get coloured pixel data of all drops overlaid 65 | """ 66 | # 2d array containing colour intensity data 67 | overlaid_frames = np.zeros((3, self.pixel_count + self.frame_width)) 68 | # Indexes of active drop animations 69 | drop_indices = np.flatnonzero(self.drop_frames) 70 | # TODO vectorize this to remove for loop 71 | for index in drop_indices: 72 | coloured_frame = [self.drop_animation[self.drop_frames[index]] * self.drop_colours[colour, index] for colour in range(3)] 73 | overlaid_frames[:, index:index+self.frame_width] += coloured_frame 74 | 75 | np.clip(overlaid_frames, 0, 255, out=overlaid_frames) 76 | return overlaid_frames[:, self.frame_side_lengths:self.frame_side_lengths+self.pixel_count].T 77 | 78 | 79 | def audio_data_updated(self, data): 80 | 81 | # Calculate the low, mids, and high indexes scaling based on the pixel count 82 | intensities = np.array([np.mean(data.melbank_lows()), 83 | np.mean(data.melbank_mids()), 84 | np.mean(data.melbank_highs())]) 85 | 86 | 87 | self.update_drop_frames() 88 | 89 | if intensities[0] - self.filtered_intensities[0] > self._config["lows_sensitivity"]: 90 | self.new_drop(randint(0, self.pixel_count-1), COLORS.get(self._config['lows_colour'])) 91 | if intensities[1] - self.filtered_intensities[1] > self._config["mids_sensitivity"]: 92 | self.new_drop(randint(0, self.pixel_count-1), COLORS.get(self._config['mids_colour'])) 93 | if intensities[2] - self.filtered_intensities[2] > self._config["high_sensitivity"]: 94 | self.new_drop(randint(0, self.pixel_count-1), COLORS.get(self._config['high_colour'])) 95 | 96 | self.filtered_intensities = self.intensity_filter.update(intensities) 97 | 98 | 99 | self.pixels = self.get_drops() 100 | -------------------------------------------------------------------------------- /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.Coerce(float) 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)), 15 | vol.Optional('decay', description='Decay rate of the scroll', default = 0.97): vol.All(vol.Coerce(float), vol.Range(min=0.2, 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.py: -------------------------------------------------------------------------------- 1 | from ledfx.effects.temporal import TemporalEffect 2 | import voluptuous as vol 3 | import numpy as np 4 | 5 | class Strobe(TemporalEffect): 6 | 7 | NAME = "Strobe" 8 | CONFIG_SCHEMA = vol.Schema({ 9 | vol.Optional('delay', description='Strobe delay', default = 50): vol.All(vol.Coerce(int), vol.Range(min=1, max=100)) 10 | }) 11 | 12 | def config_updated(self, config): 13 | self.counter = self._config["delay"] 14 | self.flipflop = True 15 | 16 | def effect_loop(self): 17 | self.counter -= 1 18 | if self.counter == 0: 19 | self.counter += self._config["delay"] 20 | self.flipflop = not self.flipflop 21 | if self.flipflop: 22 | self.pixels = np.full((self.pixel_count, 3), 255) 23 | else: 24 | self.pixels = np.zeros((self.pixel_count, 3)) 25 | 26 | -------------------------------------------------------------------------------- /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.Coerce(float) 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/actions/devices.js: -------------------------------------------------------------------------------- 1 | import fetch from "cross-fetch"; 2 | 3 | const apiUrl = window.location.protocol + "//" + window.location.host + "/api"; 4 | 5 | export const REQUEST_DEVICE_LIST = "REQUEST_DEVICE_LIST"; 6 | export const RECEIVE_DEVICE_LIST = "RECEIVE_DEVICE_LIST"; 7 | export const RECEIVE_DEVICE_ENTRY = "RECEIVE_DEVICE_ENTRY"; 8 | export const REQUEST_DEVICE_UPDATE = "REQUEST_DEVICE_UPDATE"; 9 | export const RECEIVE_DEVICE_UPDATE = "RECEIVE_DEVICE_UPDATE"; 10 | export const RECEIVE_DEVICE_EFFECT_UPDATE = "RECEIVE_DEVICE_EFFECT_UPDATE"; 11 | export const INVALIDATE_DEVICE = "INVALIDATE_DEVICE"; 12 | export const SET_DEVICE_EFFECT = "SET_DEVICE_EFFECT"; 13 | 14 | function requestDeviceList() { 15 | return { 16 | type: REQUEST_DEVICE_LIST 17 | }; 18 | } 19 | 20 | function receiveDeviceList(json) { 21 | return { 22 | type: RECEIVE_DEVICE_LIST, 23 | devices: json.devices, 24 | receivedAt: Date.now() 25 | }; 26 | } 27 | 28 | function receiveDevice(json) { 29 | return { 30 | type: RECEIVE_DEVICE_ENTRY, 31 | device: json.device, 32 | delete: json.delete, 33 | receivedAt: Date.now() 34 | }; 35 | } 36 | 37 | export function getSystemConfig() { 38 | return fetch(`${apiUrl}/config`) 39 | .then(response => response.json()); 40 | } 41 | 42 | export function addDevice(type, config) { 43 | return dispatch => { 44 | const data = { 45 | type: type, 46 | config: config 47 | }; 48 | fetch(`${apiUrl}/devices`, { 49 | method: "POST", 50 | headers: { 51 | Accept: "application/json", 52 | "Content-Type": "application/json" 53 | }, 54 | body: JSON.stringify(data) 55 | }) 56 | .then(response => response.json()) 57 | .then(json => dispatch(receiveDevice(json))); 58 | }; 59 | } 60 | 61 | export function deleteDevice(id) { 62 | let deleteJson = { delete: true, device: {id: id} } 63 | return dispatch => { 64 | fetch(`${apiUrl}/devices/${id}`, { 65 | method: 'DELETE'}) 66 | .then(response => dispatch(receiveDevice(deleteJson))); 67 | } 68 | } 69 | 70 | export function fetchDeviceList() { 71 | return dispatch => { 72 | dispatch(requestDeviceList()); 73 | return fetch(`${apiUrl}/devices`) 74 | .then(response => response.json()) 75 | .then(json => dispatch(receiveDeviceList(json))); 76 | }; 77 | } 78 | 79 | export function setDeviceEffect(deviceId, effectType, effectConfig) { 80 | return dispatch => { 81 | if (effectType) 82 | { 83 | fetch(`${apiUrl}/devices/${deviceId}/effects`, { 84 | method: "PUT", 85 | headers: { 86 | Accept: "application/json", 87 | "Content-Type": "application/json" 88 | }, 89 | body: JSON.stringify({ 90 | type: effectType, 91 | config: effectConfig 92 | }) 93 | }) 94 | .then(response => response.json()) 95 | .then(json => dispatch(receiveDeviceEffectUpdate(deviceId, json))); 96 | } 97 | else 98 | { 99 | fetch(`${apiUrl}/devices/${deviceId}/effects`, { 100 | method: "DELETE" 101 | }) 102 | .then(response => response.json()) 103 | .then(json => dispatch(receiveDeviceEffectUpdate(deviceId, json))); 104 | } 105 | }; 106 | } 107 | 108 | function invalidateDevice(deviceId) { 109 | return { 110 | type: INVALIDATE_DEVICE, 111 | deviceId 112 | }; 113 | } 114 | 115 | function requestDeviceUpdate(deviceId) { 116 | return { 117 | type: REQUEST_DEVICE_UPDATE, 118 | deviceId 119 | }; 120 | } 121 | 122 | function receiveDeviceUpdate(deviceId, json) { 123 | return { 124 | type: RECEIVE_DEVICE_UPDATE, 125 | deviceId, 126 | config: json.config.map(config => config), 127 | receivedAt: Date.now() 128 | }; 129 | } 130 | 131 | function receiveDeviceEffectUpdate(deviceId, json) { 132 | return { 133 | type: RECEIVE_DEVICE_EFFECT_UPDATE, 134 | deviceId, 135 | effect: json.effect, 136 | receivedAt: Date.now() 137 | }; 138 | } 139 | 140 | function fetchDevice(deviceId) { 141 | return dispatch => { 142 | dispatch(requestPosts(deviceId)); 143 | return fetch(`${apiUrl}/devices/${deviceId}`) 144 | .then(response => response.json()) 145 | .then(json => dispatch(receiveDeviceUpdate(deviceId, json))) 146 | }; 147 | } 148 | 149 | export function fetchDeviceEffects(deviceId) { 150 | return dispatch => { 151 | return fetch(`${apiUrl}/devices/${deviceId}/effects`) 152 | .then(response => response.json()) 153 | .then(json => dispatch(receiveDeviceEffectUpdate(deviceId, json))); 154 | }; 155 | } 156 | 157 | function shouldFetchDevice(state, deviceId) { 158 | const device = state.devicesById[deviceId]; 159 | if (!device) { 160 | return true; 161 | } else if (device.isFetching) { 162 | return false; 163 | } else { 164 | return device.didInvalidate; 165 | } 166 | } 167 | 168 | export function fetchDeviceIfNeeded(deviceId) { 169 | return (dispatch, getState) => { 170 | if (shouldFetchDevice(getState(), deviceId)) { 171 | return dispatch(fetchDevice(deviceId)); 172 | } 173 | }; 174 | } 175 | -------------------------------------------------------------------------------- /ledfx/frontend/actions/index.js: -------------------------------------------------------------------------------- 1 | export * from './devices' 2 | export * from './schemas' 3 | export * from './presets' 4 | export * from './settings' -------------------------------------------------------------------------------- /ledfx/frontend/actions/presets.js: -------------------------------------------------------------------------------- 1 | import fetch from "cross-fetch"; 2 | 3 | const apiUrl = window.location.protocol + "//" + window.location.host + "/api"; 4 | 5 | export const ADD_PRESET = "ADD_PRESET" 6 | export const DELETE_PRESET = "DELETE_PRESET" 7 | export const GET_PRESETS = "GET_PRESETS" 8 | export const ACTIVATE_PRESET = "ACTIVATE_PRESET" 9 | export const RENAME_PRESET = "RENAME_PRESET" 10 | 11 | export function addPreset(name) { 12 | return dispatch => { 13 | const data = { 14 | name: name 15 | }; 16 | return fetch(`${apiUrl}/presets`, { 17 | method: "POST", 18 | headers: { 19 | Accept: "application/json", 20 | "Content-Type": "application/json" 21 | }, 22 | body: JSON.stringify(data) 23 | }) 24 | .then(response => response.json()) 25 | .then(json => dispatch({ 26 | type: ADD_PRESET, 27 | response: json 28 | })) 29 | .then(() => dispatch(getPresets())) 30 | }; 31 | } 32 | 33 | export function deletePreset(id) { 34 | return dispatch => { 35 | const data = { 36 | id: id 37 | }; 38 | return fetch(`${apiUrl}/presets`, { 39 | method: "DELETE", 40 | headers: { 41 | Accept: "application/json", 42 | "Content-Type": "application/json" 43 | }, 44 | body: JSON.stringify(data) 45 | }) 46 | .then(response => response.json()) 47 | .then(json => dispatch({ 48 | type: DELETE_PRESET, 49 | response: json 50 | })) 51 | .then(() => dispatch(getPresets())) 52 | }; 53 | } 54 | 55 | export function activatePreset(id) { 56 | return dispatch => { 57 | const data = { 58 | id: id, 59 | action: 'activate' 60 | }; 61 | fetch(`${apiUrl}/presets`, { 62 | method: "PUT", 63 | headers: { 64 | Accept: "application/json", 65 | "Content-Type": "application/json" 66 | }, 67 | body: JSON.stringify(data) 68 | }) 69 | .then(response => response.json()) 70 | .then(json => dispatch({ 71 | type: ACTIVATE_PRESET, 72 | response: json 73 | })); 74 | }; 75 | } 76 | 77 | export function renamePreset(id, name) { 78 | return dispatch => { 79 | const data = { 80 | id: id, 81 | action: 'rename', 82 | name: name 83 | }; 84 | fetch(`${apiUrl}/presets`, { 85 | method: "PUT", 86 | headers: { 87 | Accept: "application/json", 88 | "Content-Type": "application/json" 89 | }, 90 | body: JSON.stringify(data) 91 | }) 92 | .then(response => response.json()) 93 | .then(json => dispatch({ 94 | type: RENAME_PRESET, 95 | response: json 96 | })); 97 | }; 98 | } 99 | 100 | 101 | export function getPresets() { 102 | return dispatch => { 103 | fetch(`${apiUrl}/presets`, { 104 | method: "GET", 105 | headers: { 106 | Accept: "application/json", 107 | "Content-Type": "application/json" 108 | }, 109 | }) 110 | .then(response => response.json()) 111 | .then(json => dispatch({ 112 | type: GET_PRESETS, 113 | presets: json.presets, 114 | receivedAt: Date.now() 115 | })) 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /ledfx/frontend/actions/schemas.js: -------------------------------------------------------------------------------- 1 | import fetch from "cross-fetch"; 2 | 3 | const apiUrl = window.location.protocol + "//" + window.location.host + "/api"; 4 | 5 | export const REQUEST_SCHEMAS = "REQUEST_SCHEMAS"; 6 | export const RECEIVE_SCHEMAS = "RECEIVE_SCHEMAS"; 7 | 8 | function requestSchemas() { 9 | return { 10 | type: REQUEST_SCHEMAS 11 | }; 12 | } 13 | 14 | function receiveSchemas(json) { 15 | return { 16 | type: RECEIVE_SCHEMAS, 17 | schemas: json, 18 | receivedAt: Date.now() 19 | }; 20 | } 21 | 22 | function fetchSchemas() { 23 | return dispatch => { 24 | dispatch(requestSchemas()); 25 | return fetch(`${apiUrl}/schema`) 26 | .then(response => response.json()) 27 | .then(json => dispatch(receiveSchemas(json))); 28 | }; 29 | } 30 | 31 | function shouldFetchSchemas(state) { 32 | if (Object.keys(state.schemas).length === 0) { 33 | return true; 34 | } else { 35 | return false; 36 | } 37 | } 38 | 39 | export function fetchSchemasIfNeeded() { 40 | return (dispatch, getState) => { 41 | if (shouldFetchSchemas(getState())) { 42 | return dispatch(fetchSchemas()); 43 | } 44 | }; 45 | } 46 | -------------------------------------------------------------------------------- /ledfx/frontend/actions/settings.js: -------------------------------------------------------------------------------- 1 | const apiUrl = window.location.protocol + "//" + window.location.host + "/api"; 2 | 3 | export const GET_AUDIO_INPUTS = "GET_AUDIO_INPUTS" 4 | export const SET_AUDIO_INPUT = "GET_AUDIO_INPUT" 5 | 6 | export function setAudioDevice(index) { 7 | return dispatch => { 8 | const data = { 9 | index: parseInt(index) 10 | }; 11 | fetch(`${apiUrl}/audio/devices`, { 12 | method: "PUT", 13 | headers: { 14 | Accept: "application/json", 15 | "Content-Type": "application/json" 16 | }, 17 | body: JSON.stringify(data) 18 | }) 19 | .then(response => response.json()) 20 | .then(json => dispatch({ 21 | type: SET_AUDIO_INPUT, 22 | response: json 23 | })) 24 | .then(() => dispatch(getAudioDevices())); 25 | }; 26 | } 27 | 28 | 29 | export function getAudioDevices() { 30 | return dispatch => { 31 | fetch(`${apiUrl}/audio/devices`, { 32 | method: "GET", 33 | headers: { 34 | Accept: "application/json", 35 | "Content-Type": "application/json" 36 | }, 37 | }) 38 | .then(response => response.json()) 39 | .then(json => dispatch({ 40 | type: GET_AUDIO_INPUTS, 41 | audioDevices: json, 42 | receivedAt: Date.now() 43 | })) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /ledfx/frontend/assets/img/icon/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ahodges9/LedFx/2abeced175fbff5fd4d31f28e6c5a61ad1d042e2/ledfx/frontend/assets/img/icon/favicon.ico -------------------------------------------------------------------------------- /ledfx/frontend/assets/img/icon/large_black_alpha.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ahodges9/LedFx/2abeced175fbff5fd4d31f28e6c5a61ad1d042e2/ledfx/frontend/assets/img/icon/large_black_alpha.png -------------------------------------------------------------------------------- /ledfx/frontend/assets/img/icon/large_black_on_white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ahodges9/LedFx/2abeced175fbff5fd4d31f28e6c5a61ad1d042e2/ledfx/frontend/assets/img/icon/large_black_on_white.png -------------------------------------------------------------------------------- /ledfx/frontend/assets/img/icon/large_white_alpha.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ahodges9/LedFx/2abeced175fbff5fd4d31f28e6c5a61ad1d042e2/ledfx/frontend/assets/img/icon/large_white_alpha.png -------------------------------------------------------------------------------- /ledfx/frontend/assets/img/icon/large_white_on_black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ahodges9/LedFx/2abeced175fbff5fd4d31f28e6c5a61ad1d042e2/ledfx/frontend/assets/img/icon/large_white_on_black.png -------------------------------------------------------------------------------- /ledfx/frontend/assets/img/icon/small_black_alpha.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ahodges9/LedFx/2abeced175fbff5fd4d31f28e6c5a61ad1d042e2/ledfx/frontend/assets/img/icon/small_black_alpha.png -------------------------------------------------------------------------------- /ledfx/frontend/assets/img/icon/small_black_on_white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ahodges9/LedFx/2abeced175fbff5fd4d31f28e6c5a61ad1d042e2/ledfx/frontend/assets/img/icon/small_black_on_white.png -------------------------------------------------------------------------------- /ledfx/frontend/assets/img/icon/small_white_alpha.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ahodges9/LedFx/2abeced175fbff5fd4d31f28e6c5a61ad1d042e2/ledfx/frontend/assets/img/icon/small_white_alpha.png -------------------------------------------------------------------------------- /ledfx/frontend/assets/img/icon/small_white_on_black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ahodges9/LedFx/2abeced175fbff5fd4d31f28e6c5a61ad1d042e2/ledfx/frontend/assets/img/icon/small_white_on_black.png -------------------------------------------------------------------------------- /ledfx/frontend/assets/img/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ahodges9/LedFx/2abeced175fbff5fd4d31f28e6c5a61ad1d042e2/ledfx/frontend/assets/img/logo.png -------------------------------------------------------------------------------- /ledfx/frontend/assets/jss/style.jsx: -------------------------------------------------------------------------------- 1 | 2 | const drawerWidth = 260; 3 | 4 | 5 | export { 6 | //variables 7 | drawerWidth 8 | }; 9 | -------------------------------------------------------------------------------- /ledfx/frontend/components/AddPresetCard/AddPresetCard.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { connect } from 'react-redux' 4 | import { makeStyles } from '@material-ui/core/styles'; 5 | 6 | import Card from '@material-ui/core/Card'; 7 | import CardContent from '@material-ui/core/CardContent'; 8 | import CardActions from '@material-ui/core/CardActions'; 9 | import TextField from '@material-ui/core/TextField'; 10 | import Button from '@material-ui/core/Button'; 11 | 12 | import { addPreset } from 'frontend/actions'; 13 | 14 | const useStyles = makeStyles({ 15 | button: { 16 | display: "block", 17 | width: "100", 18 | float: "right" 19 | }, 20 | action: { 21 | padding: "0" 22 | } 23 | }); 24 | 25 | const AddPresetCard = ({ presets, addPreset }) => { 26 | 27 | const [ name, setName ] = useState('') 28 | const classes = useStyles() 29 | 30 | return ( 31 | 32 | 33 |

Add Preset

34 | Save current effects of all devices as a preset 35 | 36 | setName(e.target.value)} 41 | /> 42 | 53 | 54 |
55 | 56 |
57 | ); 58 | } 59 | 60 | const validateInput = (input, presets) => (Object.keys(presets).includes(input) || input === "") 61 | 62 | const mapStateToProps = state => ({ 63 | presets: state.presets 64 | }) 65 | 66 | const mapDispatchToProps = (dispatch) => ({ 67 | addPreset: (presetName) => dispatch(addPreset(presetName)) 68 | }) 69 | 70 | export default connect(mapStateToProps, mapDispatchToProps)(AddPresetCard); -------------------------------------------------------------------------------- /ledfx/frontend/components/DeviceConfigDialog/DeviceConfigDialog.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import withStyles from "@material-ui/core/styles/withStyles"; 4 | import { connect } from "react-redux"; 5 | 6 | import Button from "@material-ui/core/Button"; 7 | import Dialog from "@material-ui/core/Dialog"; 8 | import DialogContent from "@material-ui/core/DialogContent"; 9 | import DialogTitle from "@material-ui/core/DialogTitle"; 10 | import DialogActions from "@material-ui/core/DialogActions"; 11 | import DialogContentText from "@material-ui/core/DialogContentText"; 12 | 13 | import SchemaFormCollection from "frontend/components/SchemaForm/SchemaFormCollection.jsx"; 14 | import { addDevice } from"frontend/actions"; 15 | 16 | import fetch from "cross-fetch"; 17 | 18 | const styles = theme => ({ 19 | button: { 20 | float: "right" 21 | } 22 | }); 23 | 24 | class DeviceConfigDialog extends React.Component { 25 | constructor(props) { 26 | super(props); 27 | } 28 | 29 | handleClose = () => { 30 | this.props.onClose(); 31 | }; 32 | 33 | handleSubmit = (type, config) => { 34 | this.props.dispatch(addDevice(type, config)); 35 | this.props.onClose(); 36 | }; 37 | 38 | render() { 39 | const { classes, dispatch, schemas, onClose, ...otherProps } = this.props; 40 | return ( 41 | 47 | Add Device 48 | 49 | 50 | To add a device to LedFx, please first select the type of device you 51 | wish to add then provide the necessary configuration. 52 | 53 | 58 | 65 | 72 | 73 | 74 | 75 | ); 76 | } 77 | } 78 | 79 | function mapStateToProps(state) { 80 | const { schemas } = state; 81 | 82 | return { 83 | schemas 84 | }; 85 | } 86 | 87 | export default connect(mapStateToProps)(withStyles(styles)(DeviceConfigDialog)); 88 | -------------------------------------------------------------------------------- /ledfx/frontend/components/DeviceMiniControl/DeviceMiniControl.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import withStyles from "@material-ui/core/styles/withStyles"; 4 | import { connect } from "react-redux"; 5 | 6 | import Card from "@material-ui/core/Card"; 7 | import CardContent from "@material-ui/core/CardContent"; 8 | import Button from "@material-ui/core/Button"; 9 | import Switch from '@material-ui/core/Switch'; 10 | import TableRow from '@material-ui/core/TableRow'; 11 | import TableCell from '@material-ui/core/TableCell'; 12 | import Grid from "@material-ui/core/Grid"; 13 | import Typography from "@material-ui/core/Typography"; 14 | import { NavLink } from "react-router-dom"; 15 | 16 | import { setDeviceEffect } from "frontend/actions"; 17 | 18 | const styles = theme => ({ 19 | button: { 20 | margin: theme.spacing.unit, 21 | float: "right" 22 | }, 23 | submitControls: { 24 | margin: theme.spacing.unit, 25 | display: "block", 26 | width: "100%" 27 | }, 28 | tableCell: { 29 | lineHeight: "1.2", 30 | padding: "12px 8px", 31 | verticalAlign: "middle" 32 | }, 33 | deviceLink: { 34 | textDecoration: "none", 35 | "&,&:hover": { 36 | color: "#000000" 37 | } 38 | }, 39 | header: { 40 | margin: 0 41 | }, 42 | subHeader: { 43 | margin: 0, 44 | color: "#333333" 45 | } 46 | }); 47 | 48 | class DeviceMiniControl extends React.Component { 49 | 50 | isDeviceOn = () => { 51 | return this.props.device.effect && this.props.device.effect.name; 52 | } 53 | 54 | toggleOn = () => { 55 | if (this.isDeviceOn()) 56 | { 57 | this.props.dispatch(setDeviceEffect(this.props.device.id, null, null)); 58 | } 59 | else 60 | { 61 | this.props.dispatch(setDeviceEffect(this.props.device.id, 'wavelength', null)); 62 | } 63 | } 64 | 65 | render() { 66 | const { classes, device } = this.props; 67 | 68 | return ( 69 | 70 | 71 | 72 | {device.config.name} 73 | 74 | 75 | Effect: {device.effect.name} 76 | 77 | 78 | {/* 79 | 80 | */} 81 | 82 | 89 | 90 | 91 | 95 | 96 | 97 | ); 98 | } 99 | 100 | } 101 | 102 | DeviceMiniControl.propTypes = { 103 | classes: PropTypes.object.isRequired, 104 | device: PropTypes.object.isRequired 105 | }; 106 | 107 | export default connect()(withStyles(styles)(DeviceMiniControl)); 108 | -------------------------------------------------------------------------------- /ledfx/frontend/components/DevicesTable/DevicesTable.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'; 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 | import DevicesTableItem from 'frontend/components/DevicesTable/DevicesTableItem.jsx' 12 | import { deleteDevice } from 'frontend/actions' 13 | 14 | const styles = theme => ({ 15 | root: { 16 | width: '100%', 17 | maxWidth: 360, 18 | backgroundColor: theme.palette.background.paper, 19 | }, 20 | nested: { 21 | paddingLeft: theme.spacing.unit * 4, 22 | }, 23 | table: { 24 | marginBottom: "0", 25 | width: "100%", 26 | maxWidth: "100%", 27 | backgroundColor: "transparent", 28 | borderSpacing: "0", 29 | borderCollapse: "collapse" 30 | }, 31 | tableResponsive: { 32 | width: "100%", 33 | overflowX: "auto" 34 | }, 35 | tableCell: { 36 | lineHeight: "1.42857143", 37 | padding: "12px 8px", 38 | verticalAlign: "middle" 39 | } 40 | }); 41 | 42 | class DevicesTable extends React.Component { 43 | 44 | handleDeleteDevice = deviceId => { 45 | this.props.dispatch(deleteDevice(deviceId)) 46 | } 47 | 48 | render() { 49 | const { classes, devicesById } = this.props; 50 | 51 | return ( 52 |
53 | 54 | 55 | 56 | Name 57 | IP Address 58 | Pixel Count 59 | Type 60 | Manage 61 | 62 | 63 | 64 | 65 | { 66 | Object.keys(devicesById).map(device_id => { 67 | return ( 68 | 69 | ); 70 | })} 71 | 72 |
73 |
74 | ); 75 | } 76 | } 77 | 78 | DevicesTable.propTypes = { 79 | classes: PropTypes.object.isRequired, 80 | devicesById: PropTypes.object.isRequired, 81 | }; 82 | 83 | function mapStateToProps(state) { 84 | const { devicesById } = state 85 | 86 | return { 87 | devicesById 88 | } 89 | } 90 | 91 | export default connect(mapStateToProps)(withStyles(styles)(DevicesTable)); -------------------------------------------------------------------------------- /ledfx/frontend/components/DevicesTable/DevicesTableItem.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { withStyles } from '@material-ui/core/styles'; 4 | import { NavLink } from "react-router-dom"; 5 | 6 | import TableRow from '@material-ui/core/TableRow'; 7 | import TableCell from '@material-ui/core/TableCell'; 8 | import Button from '@material-ui/core/Button'; 9 | import DeleteIcon from '@material-ui/icons/Delete'; 10 | 11 | import red from '@material-ui/core/colors/red'; 12 | 13 | const styles = theme => ({ 14 | tableCell: { 15 | lineHeight: "1.42857143", 16 | padding: "12px 8px", 17 | verticalAlign: "middle" 18 | }, 19 | button: { 20 | margin: 0, 21 | padding: 0, 22 | minWidth: 32 23 | }, 24 | deleteButton: { 25 | minWidth: 32, 26 | color: theme.palette.getContrastText(red[500]), 27 | backgroundColor: red[500], 28 | '&:hover': { 29 | backgroundColor: red[700], 30 | }, 31 | }, 32 | deviceLink: { 33 | textDecoration: "none", 34 | "&,&:hover": { 35 | color: "#000000" 36 | } 37 | } 38 | }); 39 | 40 | import { Link } from 'react-router-dom' 41 | 42 | class DevicesTableItem extends React.Component { 43 | 44 | handleDeleteDevice = () => { 45 | this.props.onDelete(this.props.device.id) 46 | } 47 | 48 | render() { 49 | const { classes, device, onDelete } = this.props; 50 | return ( 51 | 52 | 53 | 57 | {device.config.name} 58 | 59 | 60 | 61 | {device.config.ip_address} 62 | 63 | 64 | {device.config.pixel_count} 65 | 66 | 67 | {device.type} 68 | 69 | 70 | 73 | 74 | 75 | ); 76 | } 77 | } 78 | 79 | DevicesTableItem.propTypes = { 80 | classes: PropTypes.object.isRequired, 81 | device: PropTypes.object.isRequired, 82 | onDelete: PropTypes.func.isRequired 83 | }; 84 | 85 | export default withStyles(styles)(DevicesTableItem); -------------------------------------------------------------------------------- /ledfx/frontend/components/EffectControl/EffectControl.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 | 6 | import { 7 | setDeviceEffect, 8 | fetchDeviceEffects 9 | } from "frontend/actions"; 10 | 11 | import Grid from "@material-ui/core/Grid"; 12 | import Card from "@material-ui/core/Card"; 13 | import CardContent from "@material-ui/core/CardContent"; 14 | import Button from "@material-ui/core/Button"; 15 | import SchemaFormCollection from "frontend/components/SchemaForm/SchemaFormCollection.jsx"; 16 | import Typography from "@material-ui/core/Typography"; 17 | 18 | const styles = theme => ({ 19 | button: { 20 | margin: theme.spacing.unit, 21 | float: "right" 22 | }, 23 | submitControls: { 24 | margin: theme.spacing.unit, 25 | display: "block", 26 | width: "100%" 27 | }, 28 | }); 29 | 30 | 31 | class EffectControl extends React.Component { 32 | 33 | componentDidMount() { 34 | this.props.dispatch(fetchDeviceEffects(this.props.device.id)); 35 | } 36 | 37 | handleClearEffect = () => { 38 | this.props.dispatch(setDeviceEffect(this.props.device.id, null, null)) 39 | }; 40 | 41 | handleSetEffect = (type, config) => { 42 | this.props.dispatch(setDeviceEffect(this.props.device.id, type, config)) 43 | }; 44 | 45 | render() { 46 | const { classes, schemas, effect } = this.props; 47 | 48 | if (schemas.effects) { 49 | var effectvalue = ""; 50 | if(effect !== undefined && effect !== null && effect.effect !== null) 51 | effectvalue = effect.effect.type; 52 | return ( 53 |
54 | 55 | Effect Control 56 | 57 | 62 |
63 | 71 | 78 |
79 |
80 |
81 | ); 82 | } 83 | 84 | return (

Loading

) 85 | } 86 | } 87 | 88 | EffectControl.propTypes = { 89 | classes: PropTypes.object.isRequired, 90 | schemas: PropTypes.object.isRequired, 91 | device: PropTypes.object.isRequired, 92 | effect: PropTypes.object.isRequired 93 | }; 94 | 95 | function mapStateToProps(state) { 96 | const { schemas } = state; 97 | 98 | return { 99 | schemas 100 | }; 101 | } 102 | 103 | export default connect(mapStateToProps)(withStyles(styles)(EffectControl)); 104 | -------------------------------------------------------------------------------- /ledfx/frontend/components/Header/Header.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import classNames from "classnames"; 3 | import PropTypes from "prop-types"; 4 | import { connect } from "react-redux"; 5 | 6 | import withStyles from "@material-ui/core/styles/withStyles"; 7 | import AppBar from "@material-ui/core/AppBar"; 8 | import Toolbar from "@material-ui/core/Toolbar"; 9 | import IconButton from "@material-ui/core/IconButton"; 10 | import Hidden from "@material-ui/core/Hidden"; 11 | import Button from "@material-ui/core/Button"; 12 | import Typography from "@material-ui/core/Typography"; 13 | import Menu from "@material-ui/icons/Menu"; 14 | 15 | import headerStyle from "./style.jsx"; 16 | import viewRoutes from "frontend/routes/views.jsx"; 17 | 18 | class Header extends React.Component { 19 | 20 | getPageName() { 21 | var name; 22 | viewRoutes.map((prop, key) => { 23 | if (prop.path === this.props.location.pathname) { 24 | name = prop.navbarName; 25 | } 26 | return null; 27 | }); 28 | if (!name) { 29 | const path = this.props.location.pathname 30 | if (path.startsWith("/devices/")) { 31 | const deviceId = path.replace("/devices/", ""); 32 | const deviceName = this.props.devicesById[deviceId] != undefined ? 33 | this.props.devicesById[deviceId].config.name : "" 34 | name = "Devices / " + deviceName 35 | } 36 | else if (path.startsWith("/developer/")) { 37 | name = "Developer / Custom" 38 | } 39 | } 40 | return name; 41 | } 42 | 43 | render() { 44 | const { classes, color } = this.props; 45 | 46 | return ( 47 | 48 | 49 | 50 | {this.getPageName()} 51 | 52 | 53 | 57 | 58 | 59 | 60 | 61 | 62 | ); 63 | } 64 | } 65 | 66 | Header.propTypes = { 67 | classes: PropTypes.object.isRequired 68 | }; 69 | 70 | function mapStateToProps(state) { 71 | const { devicesById } = state 72 | 73 | return { 74 | devicesById 75 | } 76 | } 77 | 78 | export default connect(mapStateToProps)(withStyles(headerStyle)(Header)); 79 | -------------------------------------------------------------------------------- /ledfx/frontend/components/Header/style.jsx: -------------------------------------------------------------------------------- 1 | import { 2 | drawerWidth 3 | } from "frontend/assets/jss/style.jsx"; 4 | 5 | 6 | const headerStyle = theme => ({ 7 | appBar: { 8 | backgroundColor: "transparent", 9 | boxShadow: "none", 10 | position: 'absolute', 11 | marginLeft: drawerWidth, 12 | [theme.breakpoints.up('md')]: { 13 | width: `calc(100% - ${drawerWidth}px)`, 14 | }, 15 | }, 16 | 17 | flex: { 18 | flex: 1, 19 | fontSize: 18, 20 | fontWeight: 300, 21 | } 22 | }); 23 | 24 | export default headerStyle; 25 | -------------------------------------------------------------------------------- /ledfx/frontend/components/MelbankGraph/MelbankGraph.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import Card from "@material-ui/core/Card"; 4 | import CardContent from "@material-ui/core/CardContent"; 5 | import withStyles from "@material-ui/core/styles/withStyles"; 6 | import { Line } from 'react-chartjs-2'; 7 | import Sockette from 'sockette'; 8 | 9 | const styles = theme => ({ 10 | content: { 11 | minWidth: 120, 12 | maxWidth: '100%' 13 | } 14 | }); 15 | 16 | class PixelColorGraph extends React.Component { 17 | 18 | constructor(props) { 19 | super(props) 20 | 21 | this.websocketPacketId = 1 22 | this.deviceUpdateSubscription = null 23 | 24 | this.state = { 25 | chartData: { 26 | labels: [], 27 | datasets: [ 28 | { 29 | label: "Melbank", 30 | lineTension: 0.1, 31 | backgroundColor: "rgba(0,0,0,0.1)", 32 | borderColor: "rgba(0,0,0,1)", 33 | pointRadius: 0, 34 | data: [], 35 | }], 36 | }, 37 | chartOptions: { 38 | responsive: true, 39 | maintainAspectRatio: false, 40 | tooltips: {enabled: false}, 41 | hover: {mode: null}, 42 | animation: { 43 | duration: 0, 44 | }, 45 | hover: { 46 | animationDuration: 0, 47 | }, 48 | responsiveAnimationDuration: 0, 49 | scales: { 50 | xAxes: [{ 51 | gridLines: { 52 | display: false 53 | }, 54 | ticks: { 55 | maxTicksLimit: 7, 56 | callback: function(value, index, values) { 57 | return value + ' Hz'; 58 | } 59 | } 60 | }], 61 | yAxes: [{ 62 | ticks: { 63 | min: 0, 64 | max: 2.0, 65 | stepSize: 0.5 66 | }, 67 | gridLines: { 68 | color: "rgba(0, 0, 0, .125)", 69 | } 70 | }], 71 | }, 72 | legend: { 73 | display: false 74 | } 75 | } 76 | } 77 | } 78 | 79 | handleMessage = e => { 80 | var chartData = this.state.chartData; 81 | var messageData = JSON.parse(e.data); 82 | chartData.labels = messageData.frequencies 83 | chartData.datasets[0].data = messageData.melbank 84 | 85 | // Adjust the axes based on the max 86 | var melbankMax = Math.max.apply(Math, messageData.melbank); 87 | var chartOptions = this.state.chartOptions; 88 | chartOptions.scales.yAxes[0].ticks.min = 0 89 | chartOptions.scales.yAxes[0].ticks.max = Math.max(chartOptions.scales.yAxes[0].ticks.max, melbankMax) 90 | chartOptions.scales.yAxes[0].ticks.stepSize = chartOptions.scales.yAxes[0].ticks.max / 4 91 | 92 | this.setState(...this.state, {chartData: chartData, chartOptions: chartOptions}) 93 | } 94 | 95 | handleOpen = e => { 96 | this.enablePixelVisualization(); 97 | } 98 | 99 | handleClose = e => { 100 | } 101 | 102 | enablePixelVisualization = () => { 103 | this.state.ws.json({ 104 | id: this.websocketPacketId, 105 | type: 'subscribe_event', 106 | event_type: 'graph_update', 107 | event_filter: { 'graph_id': this.props.graphId } 108 | }) 109 | this.deviceUpdateSubscription = this.websocketPacketId; 110 | this.websocketPacketId++; 111 | } 112 | 113 | disablePixelVisualization = () => { 114 | this.state.ws.json({ 115 | id: this.websocketPacketId, 116 | type: 'unsubscribe_event', 117 | subscription_id: this.deviceUpdateSubscription 118 | }) 119 | this.deviceUpdateSubscription = null; 120 | this.websocketPacketId++; 121 | } 122 | 123 | connectWebsocket = () => { 124 | const websocketUrl = 'ws://' + window.location.host + '/api/websocket'; 125 | const ws = new Sockette(websocketUrl, { 126 | timeout: 5e3, 127 | maxAttempts: 10, 128 | onopen: this.handleOpen, 129 | onmessage: this.handleMessage, 130 | onclose: this.handleClose, 131 | onerror: e => console.log('WebSocket Error:', e) 132 | }); 133 | 134 | this.setState(...this.state, {ws: ws}); 135 | } 136 | 137 | disconnectWebsocket = () => { 138 | if (this.state.ws != undefined) { 139 | this.state.ws.close(1000); 140 | this.setState(...this.state, {ws: undefined}); 141 | } 142 | } 143 | 144 | componentDidMount() { 145 | this.connectWebsocket() 146 | } 147 | 148 | componentWillUnmount() { 149 | this.disconnectWebsocket(); 150 | } 151 | 152 | render() { 153 | const { classes } = this.props; 154 | 155 | return ( 156 | 157 | 158 | 159 | 160 | 161 | ); 162 | } 163 | } 164 | 165 | PixelColorGraph.propTypes = { 166 | classes: PropTypes.object.isRequired, 167 | graphId: PropTypes.string.isRequired 168 | }; 169 | 170 | export default withStyles(styles)(PixelColorGraph); -------------------------------------------------------------------------------- /ledfx/frontend/components/PixelColorGraph/PixelColorGraph.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import Card from "@material-ui/core/Card"; 4 | import CardContent from "@material-ui/core/CardContent"; 5 | import withStyles from "@material-ui/core/styles/withStyles"; 6 | import { Line } from 'react-chartjs-2'; 7 | import Sockette from 'sockette'; 8 | 9 | const styles = theme => ({ 10 | content: { 11 | minWidth: 120, 12 | maxWidth: '100%' 13 | } 14 | }); 15 | 16 | class PixelColorGraph extends React.Component { 17 | 18 | constructor(props) { 19 | super(props) 20 | 21 | this.websocketActive = false; 22 | this.websocketPacketId = 1 23 | this.deviceUpdateSubscription = null 24 | this.state = this.getChartOptionsForDevice(props.device) 25 | } 26 | 27 | getChartOptionsForDevice(device) 28 | { 29 | return { 30 | chartData: { 31 | labels: Array.apply(null, {length: device.config.pixel_count}).map( 32 | Function.call, Number), 33 | datasets: [ 34 | { 35 | label: "Red", 36 | lineTension: 0.1, 37 | backgroundColor: "rgba(255,0,0,0.1)", 38 | borderColor: "rgba(255,0,0,1)", 39 | pointRadius: 0, 40 | data: new Array(device.config.pixel_count).fill(0), 41 | }, 42 | { 43 | label: "Green", 44 | lineTension: 0.1, 45 | backgroundColor: "rgba(0,255,0,0.1)", 46 | borderColor: "rgba(0,255,0,1)", 47 | pointRadius: 0, 48 | data: new Array(device.config.pixel_count).fill(0), 49 | }, 50 | { 51 | label: "Blue", 52 | lineTension: 0.1, 53 | backgroundColor: "rgba(0,0,255,0.1)", 54 | borderColor: "rgba(0,0,255,1)", 55 | pointRadius: 0, 56 | data: new Array(device.config.pixel_count).fill(0), 57 | }], 58 | }, 59 | chartOptions: { 60 | responsive: true, 61 | maintainAspectRatio: false, 62 | tooltips: {enabled: false}, 63 | hover: {mode: null}, 64 | animation: { 65 | duration: 0, 66 | }, 67 | hover: { 68 | animationDuration: 0, 69 | }, 70 | responsiveAnimationDuration: 0, 71 | scales: { 72 | xAxes: [{ 73 | gridLines: { 74 | display: false 75 | }, 76 | ticks: { 77 | max: device.config.pixel_count, 78 | min: 0, 79 | maxTicksLimit: 7 80 | } 81 | }], 82 | yAxes: [{ 83 | ticks: { 84 | display: false, 85 | min: 0, 86 | max: 256, 87 | stepSize: 64 88 | }, 89 | gridLines: { 90 | display: false, 91 | color: "rgba(0, 0, 0, .125)", 92 | } 93 | }], 94 | }, 95 | legend: { 96 | display: false 97 | } 98 | } 99 | } 100 | } 101 | 102 | 103 | handleMessage = e => { 104 | var messageData = JSON.parse(e.data); 105 | 106 | // Ensure this message is for the current device. This can happen 107 | // during transistions between devices where the component stays 108 | // loaded 109 | if (messageData.device_id != this.props.device.id) { 110 | return; 111 | } 112 | 113 | var chartData = this.state.chartData 114 | chartData.datasets[0].data = messageData.pixels[0] 115 | chartData.datasets[1].data = messageData.pixels[1] 116 | chartData.datasets[2].data = messageData.pixels[2] 117 | this.setState(...this.state, {chartData: chartData}) 118 | } 119 | 120 | handleOpen = e => { 121 | this.enablePixelVisualization(this.props.device); 122 | this.websocketActive = true 123 | } 124 | 125 | handleClose = e => { 126 | this.websocketActive = false 127 | } 128 | 129 | enablePixelVisualization = (device) => { 130 | this.state.ws.json({ 131 | id: this.websocketPacketId, 132 | type: 'subscribe_event', 133 | event_type: 'device_update', 134 | event_filter: { 'device_id': device.id } 135 | }) 136 | this.deviceUpdateSubscription = this.websocketPacketId; 137 | this.websocketPacketId++; 138 | } 139 | 140 | disablePixelVisualization = () => { 141 | this.state.ws.json({ 142 | id: this.websocketPacketId, 143 | type: 'unsubscribe_event', 144 | subscription_id: this.deviceUpdateSubscription 145 | }) 146 | this.deviceUpdateSubscription = null; 147 | this.websocketPacketId++; 148 | } 149 | 150 | connectWebsocket = () => { 151 | const websocketUrl = 'ws://' + window.location.host + '/api/websocket'; 152 | const ws = new Sockette(websocketUrl, { 153 | timeout: 5e3, 154 | maxAttempts: 10, 155 | onopen: this.handleOpen, 156 | onmessage: this.handleMessage, 157 | onclose: this.handleClose, 158 | onerror: e => console.log('WebSocket Error:', e) 159 | }); 160 | 161 | this.setState(...this.state, {ws: ws}); 162 | } 163 | 164 | disconnectWebsocket = () => { 165 | if (this.state.ws != undefined && 166 | this.websocketActive) { 167 | this.state.ws.close(1000); 168 | this.setState(...this.state, {ws: undefined}); 169 | } 170 | } 171 | 172 | componentDidMount() { 173 | this.connectWebsocket() 174 | } 175 | 176 | componentWillUnmount() { 177 | this.disconnectWebsocket(); 178 | } 179 | 180 | componentWillReceiveProps(nextProps) { 181 | if (this.websocketActive) { 182 | this.disablePixelVisualization() 183 | this.enablePixelVisualization(nextProps.device) 184 | this.setState(...this.state, 185 | this.getChartOptionsForDevice(nextProps.device)) 186 | } 187 | } 188 | 189 | render() { 190 | const { classes, device } = this.props; 191 | 192 | return ( 193 | 194 | ); 195 | } 196 | } 197 | 198 | PixelColorGraph.propTypes = { 199 | classes: PropTypes.object.isRequired, 200 | device: PropTypes.object.isRequired 201 | }; 202 | 203 | export default withStyles(styles)(PixelColorGraph); -------------------------------------------------------------------------------- /ledfx/frontend/components/PresetCard/PresetCard.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'; 5 | import Card from '@material-ui/core/Card'; 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 red from '@material-ui/core/colors/red'; 10 | 11 | import PresetConfigTable from "frontend/components/PresetCard/PresetConfigTable"; 12 | 13 | import { activatePreset, deletePreset } from 'frontend/actions'; 14 | 15 | const styles = theme => ({ 16 | deleteButton: { 17 | color: theme.palette.getContrastText(red[500]), 18 | backgroundColor: red[500], 19 | '&:hover': { 20 | backgroundColor: red[700], 21 | }, 22 | margin: theme.spacing.unit, 23 | float: "right" 24 | }, 25 | button: { 26 | margin: theme.spacing.unit, 27 | float: "right" 28 | }, 29 | submitControls: { 30 | margin: theme.spacing.unit, 31 | display: "block", 32 | width: "100%" 33 | }, 34 | 35 | }); 36 | 37 | class PresetCard extends React.Component { 38 | 39 | render() { 40 | const { classes, preset, activatePreset, deletePreset } = this.props; 41 | 42 | return ( 43 | 44 | 45 |

{preset.name}

46 | { preset.devices && } 47 |
48 | 49 | 59 | 69 | 70 |
71 | ); 72 | } 73 | } 74 | 75 | PresetCard.propTypes = { 76 | classes: PropTypes.object.isRequired, 77 | preset: PropTypes.object.isRequired, 78 | }; 79 | 80 | const mapDispatchToProps = (dispatch) => ({ 81 | deletePreset: (presetId) => dispatch(deletePreset(presetId)), 82 | activatePreset: (presetId) => dispatch(activatePreset(presetId)) 83 | }) 84 | 85 | export default connect(null , mapDispatchToProps)(withStyles(styles)(PresetCard)); -------------------------------------------------------------------------------- /ledfx/frontend/components/PresetCard/PresetConfigTable.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 PresetConfigTable 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 | PresetConfigTable.propTypes = { 53 | classes: PropTypes.object.isRequired, 54 | devices: PropTypes.object.isRequired 55 | }; 56 | 57 | export default withStyles(styles)(PresetConfigTable); -------------------------------------------------------------------------------- /ledfx/frontend/components/PresetConfigDialog/PresetConfigDialog.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import withStyles from "@material-ui/core/styles/withStyles"; 4 | import { connect } from "react-redux"; 5 | 6 | import Button from "@material-ui/core/Button"; 7 | import Dialog from "@material-ui/core/Dialog"; 8 | import DialogContent from "@material-ui/core/DialogContent"; 9 | import DialogTitle from "@material-ui/core/DialogTitle"; 10 | import DialogActions from "@material-ui/core/DialogActions"; 11 | import DialogContentText from "@material-ui/core/DialogContentText"; 12 | 13 | import SchemaFormCollection from "frontend/components/SchemaForm/SchemaFormCollection.jsx"; 14 | import { addDevice } from"frontend/actions"; 15 | 16 | import fetch from "cross-fetch"; 17 | 18 | const styles = theme => ({ 19 | button: { 20 | float: "right" 21 | } 22 | }); 23 | 24 | class PresetsConfigDialog extends React.Component { 25 | constructor(props) { 26 | super(props); 27 | } 28 | 29 | handleClose = () => { 30 | this.props.onClose(); 31 | }; 32 | 33 | handleSubmit = (type, config) => { 34 | this.props.dispatch(addDevice(type, config)); 35 | this.props.onClose(); 36 | }; 37 | 38 | render() { 39 | const { classes, dispatch, schemas, onClose, ...otherProps } = this.props; 40 | return ( 41 | 47 | Add Preset 48 | 49 | 50 | To add a preset to LedFx, please first configure the effects you wish to save, 51 | select the type of preset you wish, and then provide the necessary configuration. 52 | 53 | 58 | 65 | 72 | 73 | 74 | 75 | ); 76 | } 77 | } 78 | 79 | function mapStateToProps(state) { 80 | const { schemas } = state; 81 | 82 | return { 83 | schemas 84 | }; 85 | } 86 | 87 | export default connect(mapStateToProps)(withStyles(styles)(PresetsConfigDialog)); 88 | -------------------------------------------------------------------------------- /ledfx/frontend/components/SchemaForm/Utils.jsx: -------------------------------------------------------------------------------- 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 = typeof projection === "string" ? _objectpath2.default.parse(projection) : projection; 11 | 12 | if (typeof valueToSet !== "undefined" && parts.length === 1) { 13 | // special case, just setting one variable 14 | obj[parts[0]] = valueToSet; 15 | return obj; 16 | } 17 | 18 | if (typeof valueToSet !== "undefined" && typeof obj[parts[0]] === "undefined") { 19 | // We need to look ahead to check if array is appropriate 20 | obj[parts[0]] = parts.length > 2 && numRe.test(parts[1]) ? [] : {}; 21 | } 22 | 23 | if (typeof type !== "undefined" && ["number", "integer"].indexOf(type) > -1 && typeof valueToSet === "undefined") { 24 | // number or integer can undefined 25 | obj[parts[0]] = valueToSet; 26 | return obj; 27 | } 28 | 29 | var value = obj[parts[0]]; 30 | for (var i = 1; i < parts.length; i += 1) { 31 | // Special case: We allow JSON Form syntax for arrays using empty brackets 32 | // These will of course not work here so we exit if they are found. 33 | if (parts[i] === "") { 34 | return undefined; 35 | } 36 | if (typeof valueToSet !== "undefined") { 37 | if (i === parts.length - 1) { 38 | // last step. Let's set the value 39 | value[parts[i]] = valueToSet; 40 | return valueToSet; 41 | } 42 | // Make sure to create new objects on the way if they are not there. 43 | // We need to look ahead to check if array is appropriate 44 | var tmp = value[parts[i]]; 45 | if (typeof tmp === "undefined" || tmp === null) { 46 | tmp = numRe.test(parts[i + 1]) ? [] : {}; 47 | value[parts[i]] = tmp; 48 | } 49 | value = tmp; 50 | } else if (value) { 51 | // Just get nex value. 52 | value = value[parts[i]]; 53 | } 54 | } 55 | return value; 56 | } 57 | 58 | export function extractValue(e, prop) { 59 | let value = undefined; 60 | switch (prop.type) { 61 | case "integer": 62 | case "number": 63 | if (e.target.value.indexOf(".") == -1) { 64 | value = parseInt(e.target.value); 65 | } else { 66 | value = parseFloat(e.target.value); 67 | } 68 | 69 | if (isNaN(value) || value === null) { 70 | value = undefined; 71 | } 72 | break; 73 | case "boolean": 74 | value = e.target.checked; 75 | break; 76 | default: 77 | value = e.target.value; 78 | 79 | if (value === "") { 80 | value = undefined; 81 | } 82 | } 83 | 84 | return value; 85 | } 86 | 87 | export function validateValue(v, prop) { 88 | let schema = { type: "object", properties: { } }; 89 | schema.properties[prop.title] = prop 90 | schema.required = prop.required ? [prop.title] : [] 91 | 92 | let value = {}; 93 | if (v !== undefined) { 94 | value[prop.title] = v 95 | } 96 | 97 | let result = tv4.validateResult(value, schema); 98 | return result; 99 | } 100 | 101 | export function validateData(formData, schema) { 102 | // let schema = { type: "object", properties: formData }; 103 | let result = tv4.validateResult(formData, schema); 104 | 105 | return result; 106 | } 107 | 108 | -------------------------------------------------------------------------------- /ledfx/frontend/components/Sidebar/style.jsx: -------------------------------------------------------------------------------- 1 | import { 2 | drawerWidth 3 | } from "frontend/assets/jss/style.jsx"; 4 | 5 | const sidebarStyle = theme => ({ 6 | drawerPaper: { 7 | width: drawerWidth, 8 | [theme.breakpoints.up('md')]: { 9 | width: drawerWidth, 10 | position: 'fixed', 11 | height: "100%" 12 | } 13 | }, 14 | logo: { 15 | position: "relative", 16 | padding: "15px 15px", 17 | zIndex: "4", 18 | "&:after": { 19 | content: '""', 20 | position: "absolute", 21 | bottom: "0", 22 | height: "1px", 23 | right: "15px", 24 | width: "calc(100% - 30px)", 25 | backgroundColor: "rgba(180, 180, 180, 0.3)" 26 | } 27 | }, 28 | logoLink: { 29 | padding: "5px 0", 30 | display: "block", 31 | fontSize: "18px", 32 | textAlign: "left", 33 | fontWeight: "400", 34 | lineHeight: "30px", 35 | textDecoration: "none", 36 | backgroundColor: "transparent", 37 | "&,&:hover": { 38 | color: "#FFFFFF" 39 | } 40 | }, 41 | logoImage: { 42 | width: "30px", 43 | display: "inline-block", 44 | maxHeight: "30px", 45 | marginLeft: "10px", 46 | marginRight: "15px" 47 | }, 48 | img: { 49 | width: "35px", 50 | top: "17px", 51 | position: "absolute", 52 | verticalAlign: "middle", 53 | border: "0" 54 | }, 55 | background: { 56 | position: "absolute", 57 | zIndex: "1", 58 | height: "100%", 59 | width: "100%", 60 | display: "block", 61 | top: "0", 62 | left: "0", 63 | backgroundSize: "cover", 64 | backgroundPosition: "center center", 65 | "&:after": { 66 | position: "absolute", 67 | zIndex: "3", 68 | width: "100%", 69 | height: "100%", 70 | content: '""', 71 | display: "block", 72 | background: "#000", 73 | opacity: ".8" 74 | } 75 | }, 76 | list: { 77 | marginTop: "20px", 78 | paddingLeft: "0", 79 | paddingTop: "0", 80 | paddingBottom: "0", 81 | marginBottom: "0", 82 | listStyle: "none", 83 | position: "unset" 84 | }, 85 | item: { 86 | position: "relative", 87 | display: "block", 88 | textDecoration: "none", 89 | "&:hover,&:focus,&:visited,&": { 90 | color: "#FFFFFF" 91 | } 92 | }, 93 | itemLink: { 94 | width: "auto", 95 | transition: "all 300ms linear", 96 | margin: "10px 15px 0", 97 | borderRadius: "3px", 98 | position: "relative", 99 | display: "block", 100 | padding: "10px 15px", 101 | backgroundColor: "transparent" 102 | }, 103 | itemIcon: { 104 | width: "24px", 105 | minWidth: "24px", 106 | height: "30px", 107 | float: "left", 108 | marginRight: "15px", 109 | textAlign: "center", 110 | verticalAlign: "middle", 111 | color: "rgba(255, 255, 255, 0.8)" 112 | }, 113 | itemText: { 114 | margin: "0", 115 | lineHeight: "30px", 116 | fontSize: "14px", 117 | fontWeight: 300, 118 | color: "#FFFFFF" 119 | }, 120 | devicesItemText: { 121 | margin: "0", 122 | marginLeft: "10px", 123 | lineHeight: "30px", 124 | fontSize: "14px", 125 | fontWeight: 300, 126 | color: "#FFFFFF", 127 | textDecoration: "none" 128 | }, 129 | activeView: { 130 | backgroundColor: [theme.palette.primary.main], 131 | boxShadow: [theme.shadows[12]], 132 | "&:hover,&:focus,&:visited,&": { 133 | backgroundColor: [theme.palette.primary.main], 134 | boxShadow: [theme.shadows[12]] 135 | }, 136 | color: "#FFFFFF" 137 | }, 138 | sidebarWrapper: { 139 | position: "relative", 140 | height: "calc(100vh - 70px)", 141 | overflow: "auto", 142 | zIndex: "4", 143 | overflowScrolling: "touch" 144 | }, 145 | }); 146 | 147 | export default sidebarStyle; 148 | -------------------------------------------------------------------------------- /ledfx/frontend/dist/__init__.py: -------------------------------------------------------------------------------- 1 | """LedFx Frontend""" 2 | import os 3 | 4 | def where(): 5 | return os.path.dirname(__file__) -------------------------------------------------------------------------------- /ledfx/frontend/dist/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | LedFx 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /ledfx/frontend/dist/ledfx_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ahodges9/LedFx/2abeced175fbff5fd4d31f28e6c5a61ad1d042e2/ledfx/frontend/dist/ledfx_icon.png -------------------------------------------------------------------------------- /ledfx/frontend/dist/small_black_alpha.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ahodges9/LedFx/2abeced175fbff5fd4d31f28e6c5a61ad1d042e2/ledfx/frontend/dist/small_black_alpha.png -------------------------------------------------------------------------------- /ledfx/frontend/index.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | import { Provider } from 'react-redux'; 4 | import thunkMiddleware from 'redux-thunk' 5 | import rootReducer from 'frontend/reducers'; 6 | import { createBrowserHistory } from "history"; 7 | import { createStore, applyMiddleware } from 'redux' 8 | import { Router, Route, Switch } from "react-router-dom"; 9 | 10 | import "frontend/style.css"; 11 | import indexRoutes from "frontend/routes"; 12 | 13 | const hist = createBrowserHistory(); 14 | const store = createStore(rootReducer, 15 | applyMiddleware( 16 | thunkMiddleware 17 | )); 18 | 19 | ReactDOM.render( 20 | 21 | 22 | 23 | { 24 | indexRoutes.map((prop, key) => { 25 | return ; 26 | }) 27 | } 28 | 29 | 30 | , 31 | document.getElementById('main') 32 | ); 33 | -------------------------------------------------------------------------------- /ledfx/frontend/layouts/Default/Default.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { connect } from "react-redux"; 3 | import { 4 | fetchDeviceList, 5 | fetchSchemasIfNeeded 6 | } from "frontend/actions"; 7 | import PropTypes from "prop-types"; 8 | import { Switch, Route, Redirect } from "react-router-dom"; 9 | 10 | import Header from "frontend/components/Header/Header.jsx"; 11 | import Sidebar from "frontend/components/Sidebar/Sidebar.jsx"; 12 | import withStyles from "@material-ui/core/styles/withStyles"; 13 | import viewRoutes from "frontend/routes/views.jsx"; 14 | import dashboardStyle from "./style.jsx"; 15 | import logo from "frontend/assets/img/icon/small_white_alpha.png"; 16 | 17 | import { MuiThemeProvider, createMuiTheme } from "@material-ui/core/styles"; 18 | import cyan from "@material-ui/core/colors/cyan"; 19 | import green from "@material-ui/core/colors/green"; 20 | 21 | import CssBaseline from "@material-ui/core/CssBaseline"; 22 | 23 | const defaultTheme = createMuiTheme({ 24 | palette: { 25 | primary: cyan, 26 | secondary: green 27 | }, 28 | overrides: { 29 | MuiFormControl: { 30 | root: { 31 | margin: 8, 32 | minWidth: 225, 33 | flex: "1 0 30%" 34 | }, 35 | }, 36 | }, 37 | }); 38 | 39 | class DefaultLayout extends React.Component { 40 | constructor(props) { 41 | super(props); 42 | this.state = { 43 | mobileOpen: false, 44 | theme: defaultTheme 45 | }; 46 | } 47 | 48 | componentDidUpdate(e) { 49 | if (e.history.location.pathname !== e.location.pathname) { 50 | this.refs.root.scrollTop = 0; 51 | if (this.state.mobileOpen) { 52 | var newState = Object.assign({}, this.state, { mobileOpen: false }); 53 | this.setState(newState); 54 | } 55 | } 56 | } 57 | 58 | componentDidMount() { 59 | this.props.dispatch(fetchDeviceList()); 60 | this.props.dispatch(fetchSchemasIfNeeded()); 61 | } 62 | 63 | render() { 64 | const { classes, rest } = this.props; 65 | 66 | var handleDrawerToggle = () => { 67 | var newState = Object.assign({}, this.state, { 68 | mobileOpen: !this.state.mobileOpen 69 | }); 70 | this.setState(newState); 71 | }; 72 | 73 | return ( 74 |
75 | 76 | 77 |
81 | 86 | 87 |
88 |
89 | 90 | {viewRoutes.map((prop, key) => { 91 | if (prop.redirect) { 92 | return ; 93 | } 94 | return ( 95 | 101 | ); 102 | })} 103 | 104 |
105 | 106 |
107 | ); 108 | } 109 | } 110 | 111 | DefaultLayout.propTypes = { 112 | classes: PropTypes.object.isRequired, 113 | devicesById: PropTypes.object.isRequired 114 | }; 115 | 116 | function mapStateToProps(state) { 117 | const { devicesById, schemas } = state; 118 | 119 | return { 120 | devicesById, 121 | schemas 122 | }; 123 | } 124 | 125 | export default connect(mapStateToProps)( 126 | withStyles(dashboardStyle)(DefaultLayout) 127 | ); 128 | -------------------------------------------------------------------------------- /ledfx/frontend/layouts/Default/style.jsx: -------------------------------------------------------------------------------- 1 | import { 2 | drawerWidth 3 | } from "frontend/assets/jss/style.jsx"; 4 | 5 | const appStyle = theme => ({ 6 | root: { 7 | overflow: 'hidden', 8 | display: 'flex', 9 | width: '100%', 10 | }, 11 | content: { 12 | flexGrow: 1, 13 | backgroundColor: theme.palette.background.default, 14 | padding: theme.spacing.unit * 3, 15 | minWidth: 200, 16 | [theme.breakpoints.up('md')]: { 17 | marginLeft: drawerWidth 18 | } 19 | }, 20 | toolbar: theme.mixins.toolbar, 21 | }); 22 | 23 | export default appStyle; 24 | -------------------------------------------------------------------------------- /ledfx/frontend/reducers/devices.js: -------------------------------------------------------------------------------- 1 | import { 2 | REQUEST_DEVICE_LIST, 3 | RECEIVE_DEVICE_LIST, 4 | INVALIDATE_DEVICE, 5 | REQUEST_DEVICE_UPDATE, 6 | RECEIVE_DEVICE_UPDATE, 7 | RECEIVE_DEVICE_EFFECT_UPDATE, 8 | RECEIVE_DEVICE_ENTRY 9 | } from 'frontend/actions' 10 | 11 | function device( 12 | state = { 13 | isFetching: false, 14 | didInvalidate: false, 15 | config: {}, 16 | effects: {}, 17 | }, 18 | action 19 | ) { 20 | switch (action.type) { 21 | case INVALIDATE_DEVICE: 22 | return {...state, didInvalidate: true } 23 | case REQUEST_DEVICE_UPDATE: 24 | return { 25 | ...state, 26 | isFetching: true, 27 | didInvalidate: false 28 | } 29 | case RECEIVE_DEVICE_UPDATE: 30 | return { 31 | ...state, 32 | isFetching: false, 33 | didInvalidate: false, 34 | config: action.config, 35 | lastUpdated: action.receivedAt 36 | } 37 | case RECEIVE_DEVICE_EFFECT_UPDATE: 38 | return { 39 | ...state, 40 | effect: action.effect, 41 | lastUpdated: action.receivedAt 42 | } 43 | default: 44 | return state 45 | } 46 | } 47 | 48 | function deviceList(state = {}, action) { 49 | switch (action.type) { 50 | case REQUEST_DEVICE_LIST: 51 | return state; 52 | case RECEIVE_DEVICE_LIST: 53 | return Object.assign({}, state, action.devices) 54 | case RECEIVE_DEVICE_ENTRY: 55 | if (action.delete) { 56 | let newState = state; 57 | delete newState[action.device.id] 58 | return newState 59 | } else { 60 | let newState = state 61 | newState[action.device.id] = action.device 62 | return newState 63 | } 64 | default: 65 | return state; 66 | } 67 | } 68 | 69 | export function devicesById(state = {}, action) { 70 | switch (action.type) { 71 | case INVALIDATE_DEVICE: 72 | case REQUEST_DEVICE_UPDATE: 73 | case RECEIVE_DEVICE_UPDATE: 74 | case RECEIVE_DEVICE_EFFECT_UPDATE: 75 | return Object.assign({}, state, { 76 | [action.deviceId]: device(state[action.deviceId], action) 77 | }) 78 | case REQUEST_DEVICE_LIST: 79 | case RECEIVE_DEVICE_LIST: 80 | case RECEIVE_DEVICE_ENTRY: 81 | return Object.assign({}, state, deviceList(state, action)) 82 | default: 83 | return state 84 | } 85 | } -------------------------------------------------------------------------------- /ledfx/frontend/reducers/index.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux' 2 | import { devicesById } from './devices' 3 | import { schemas } from './schemas' 4 | import { presets } from './presets' 5 | import { settings } from './settings' 6 | 7 | const rootReducer = combineReducers({ 8 | devicesById, 9 | schemas, 10 | presets, 11 | settings 12 | }) 13 | 14 | export default rootReducer -------------------------------------------------------------------------------- /ledfx/frontend/reducers/presets.js: -------------------------------------------------------------------------------- 1 | import { 2 | GET_PRESETS 3 | } from 'frontend/actions' 4 | 5 | export function presets(state = {}, action) { 6 | switch (action.type) { 7 | case GET_PRESETS: 8 | return action.presets; 9 | default: 10 | return state; 11 | } 12 | } -------------------------------------------------------------------------------- /ledfx/frontend/reducers/schemas.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux' 2 | import { 3 | RECEIVE_SCHEMAS, 4 | } from 'frontend/actions' 5 | 6 | export function schemas(state = {}, action) { 7 | switch (action.type) { 8 | case RECEIVE_SCHEMAS: 9 | return Object.assign({}, state, action.schemas) 10 | default: 11 | return state; 12 | } 13 | } -------------------------------------------------------------------------------- /ledfx/frontend/reducers/settings.js: -------------------------------------------------------------------------------- 1 | import { 2 | GET_AUDIO_INPUTS, 3 | SET_AUDIO_INPUT 4 | } from 'frontend/actions' 5 | 6 | export function settings(state = {}, action) { 7 | console.log(action) 8 | switch (action.type) { 9 | case GET_AUDIO_INPUTS: 10 | const audioDevices = action.audioDevices 11 | return {...state, audioDevices} 12 | default: 13 | return state 14 | } 15 | } -------------------------------------------------------------------------------- /ledfx/frontend/routes/index.jsx: -------------------------------------------------------------------------------- 1 | import DefaultLayout from "frontend/layouts/Default/Default.jsx"; 2 | 3 | const indexRoutes = [{ path: "/", component: DefaultLayout }]; 4 | 5 | export default indexRoutes; 6 | -------------------------------------------------------------------------------- /ledfx/frontend/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 "frontend/views/Dashboard/Dashboard.jsx"; 11 | import DevicesView from "frontend/views/Devices/Devices.jsx"; 12 | import PresetsView from "frontend/views/Presets/Presets.jsx"; 13 | import DeviceView from "frontend/views/Device/Device.jsx"; 14 | import SettingsView from "frontend/views/Settings/Settings.jsx"; 15 | import DeveloperView from "frontend/views/Developer/Developer.jsx"; 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/:device_id", 27 | navbarName: "Devices", 28 | sidebarName: "Devices", 29 | icon: List, 30 | component: DeviceView, 31 | }, 32 | { 33 | path: "/presets", 34 | sidebarName: "Presets Management", 35 | navbarName: "Presets Management", 36 | icon: SaveAltIcon, 37 | component: PresetsView, 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 | -------------------------------------------------------------------------------- /ledfx/frontend/style.css: -------------------------------------------------------------------------------- 1 | html, body { 2 | font-family: 'Roboto', 'Helvetica', sans-serif; 3 | font-weight: 300; 4 | } -------------------------------------------------------------------------------- /ledfx/frontend/utils/api/index.jsx: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | 3 | const url = window.location.protocol + '//' + window.location.host + '/api/'; 4 | 5 | function callApi(method, api, data) { 6 | return axios({ 7 | method: method, 8 | url: url + api, 9 | data: data 10 | }); 11 | } 12 | 13 | function getDevices() { 14 | return callApi('get', 'devices').then(response => { 15 | const devices = response.data.devices 16 | return Object.keys(devices).map(key => { 17 | return { 18 | key: key, 19 | id: key, 20 | name: devices[key].name, 21 | config: devices[key] 22 | } 23 | }) 24 | }) 25 | } 26 | 27 | function deleteDevice(device_id) { 28 | return callApi('delete', 'devices/' + device_id) 29 | } 30 | 31 | function addDevice(config) { 32 | return callApi('put', 'devices', config) 33 | } 34 | 35 | function getDevice(device_id) { 36 | return callApi('get', 'devices/' + device_id).then(response => { 37 | const device = response.data 38 | return { 39 | key: device_id, 40 | id: device_id, 41 | name: device.name, 42 | config: device 43 | } 44 | }) 45 | } 46 | 47 | function getDeviceEffects(device_id) { 48 | return callApi('get', 'devices/' + device_id + '/effects').then(response => { 49 | return response.data; 50 | }) 51 | } 52 | 53 | export { callApi, getDevices, deleteDevice, addDevice, getDevice, getDeviceEffects } -------------------------------------------------------------------------------- /ledfx/frontend/utils/helpers.jsx: -------------------------------------------------------------------------------- 1 | export const includeKeyInObject = (key, object) => ({ id: key, ...object}) -------------------------------------------------------------------------------- /ledfx/frontend/views/Dashboard/Dashboard.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 | 6 | import Card from '@material-ui/core/Card'; 7 | import CardContent from '@material-ui/core/CardContent'; 8 | import Typography from '@material-ui/core/Typography'; 9 | 10 | import Table from '@material-ui/core/Table'; 11 | import TableBody from '@material-ui/core/TableBody'; 12 | import Grid from "@material-ui/core/Grid"; 13 | import PixelColorGraph from "frontend/components/PixelColorGraph/PixelColorGraph.jsx"; 14 | import DeviceMiniControl from 'frontend/components/DeviceMiniControl/DeviceMiniControl.jsx'; 15 | import AddPresetCard from "frontend/components/AddPresetCard/AddPresetCard"; 16 | 17 | const styles = theme => ({ 18 | root: { 19 | flexGrow: 1 20 | }, 21 | card: { 22 | width: "100%", 23 | overflowX: "auto" 24 | }, 25 | table: { 26 | width: "100%", 27 | maxWidth: "100%", 28 | backgroundColor: "transparent", 29 | borderSpacing: "0", 30 | }, 31 | }); 32 | 33 | class DashboardView extends React.Component { 34 | 35 | render() { 36 | const { classes, devicesById } = this.props; 37 | 38 | if (Object.keys(devicesById) == 0) 39 | { 40 | return ( 41 |
42 | 43 | 44 |

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

45 |
46 |
47 |
48 | ); 49 | } 50 | 51 | return ( 52 |
53 | 54 | 55 | { 56 | Object.keys(devicesById).map(id => { 57 | return ( 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | ); 73 | }) 74 | } 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 |
83 | ); 84 | } 85 | } 86 | 87 | DashboardView.propTypes = { 88 | devicesById: PropTypes.object.isRequired, 89 | }; 90 | 91 | function mapStateToProps(state) { 92 | const { devicesById } = state 93 | 94 | return { 95 | devicesById 96 | } 97 | } 98 | 99 | export default connect(mapStateToProps)(withStyles(styles)(DashboardView)); 100 | -------------------------------------------------------------------------------- /ledfx/frontend/views/Developer/Developer.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | 4 | import Grid from "@material-ui/core/Grid"; 5 | import Card from "@material-ui/core/Card"; 6 | import CardContent from "@material-ui/core/CardContent"; 7 | 8 | import MelbankGraph from "frontend/components/MelbankGraph/MelbankGraph.jsx"; 9 | 10 | class DeveloperView extends React.Component { 11 | 12 | componentDidMount() { 13 | const { device_id } = this.props.match.params; 14 | } 15 | 16 | render() { 17 | const { classes } = this.props; 18 | const { graphString } = this.props.match.params; 19 | 20 | let graphList = graphString.split("+") 21 | let graphDom = Object.keys(graphList).map(graphIndex => { 22 | return ( 23 | 24 |

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

25 | 26 |
27 | ); 28 | }); 29 | 30 | return ( 31 | 32 | {graphDom} 33 | 34 | ); 35 | } 36 | } 37 | 38 | export default DeveloperView; 39 | -------------------------------------------------------------------------------- /ledfx/frontend/views/Device/Device.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | 4 | import Grid from "@material-ui/core/Grid"; 5 | import Card from "@material-ui/core/Card"; 6 | import CardContent from "@material-ui/core/CardContent"; 7 | 8 | import { callApi, getDevice, getDeviceEffects} from "frontend/utils/api"; 9 | import { connect } from "react-redux"; 10 | import EffectControl from "frontend/components/EffectControl/EffectControl.jsx"; 11 | import PixelColorGraph from "frontend/components/PixelColorGraph/PixelColorGraph.jsx"; 12 | 13 | class DeviceView extends React.Component { 14 | constructor() { 15 | super(); 16 | this.state = { 17 | device : null, 18 | effect : null 19 | }; 20 | } 21 | 22 | componentDidMount() { 23 | const { device_id } = this.props.match.params; 24 | 25 | this.state.device = null; 26 | getDevice(device_id) 27 | .then(device => { 28 | this.setState({ device: device }); 29 | }) 30 | .catch(error => console.log(error)); 31 | 32 | this.state.effect = null; 33 | getDeviceEffects(device_id) 34 | .then(effect => { 35 | this.setState({ effect: effect }); 36 | }) 37 | .catch(error => console.log(error)); 38 | } 39 | 40 | componentWillReceiveProps(nextProps) { 41 | var device = null; 42 | if (this.props.devicesById) 43 | { 44 | this.state.device = null; 45 | device = this.props.devicesById[nextProps.match.params.device_id] 46 | this.setState(...this.state, {device: device}); 47 | } 48 | 49 | if(device !== undefined && device !== null) 50 | { 51 | this.state.effect = null; 52 | getDeviceEffects(device.id) 53 | .then(effect => { 54 | this.setState({ effect: effect }); 55 | }) 56 | .catch(error => console.log(error)); 57 | } 58 | 59 | } 60 | 61 | render() { 62 | const { classes } = this.props; 63 | const { device_id } = this.props.match.params; 64 | const { device, effect } = this.state; 65 | 66 | if (device) 67 | { 68 | return ( 69 | 70 | {renderPixelGraph(device, effect)} 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | ); 80 | } 81 | return (

Loading

) 82 | } 83 | } 84 | 85 | 86 | const renderPixelGraph = (device, effect) => { 87 | if (device.effect && device.effect.name) { 88 | console.log(effect) 89 | return ( 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | ) 98 | } 99 | } 100 | 101 | DeviceView.propTypes = { 102 | devicesById: PropTypes.object.isRequired, 103 | }; 104 | 105 | function mapStateToProps(state) { 106 | const { devicesById } = state 107 | 108 | return { 109 | devicesById 110 | } 111 | } 112 | 113 | export default connect(mapStateToProps)(DeviceView); 114 | -------------------------------------------------------------------------------- /ledfx/frontend/views/Devices/Devices.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import withStyles from "@material-ui/core/styles/withStyles"; 4 | 5 | import Typography from '@material-ui/core/Typography'; 6 | //import Slider from '@material-ui/core/Slider'; 7 | import Input from '@material-ui/core/Input'; 8 | import { connect } from "react-redux"; 9 | 10 | import Grid from "@material-ui/core/Grid"; 11 | import Card from "@material-ui/core/Card"; 12 | import CardContent from "@material-ui/core/CardContent"; 13 | import Button from "@material-ui/core/Button"; 14 | import AddIcon from "@material-ui/icons/Add"; 15 | 16 | import DevicesTable from "frontend/components/DevicesTable/DevicesTable.jsx"; 17 | import DeviceConfigDialog from "frontend/components/DeviceConfigDialog/DeviceConfigDialog.jsx"; 18 | 19 | const styles = theme => ({ 20 | cardResponsive: { 21 | width: "100%", 22 | overflowX: "auto" 23 | }, 24 | button: { 25 | position: "absolute", 26 | bottom: theme.spacing.unit * 2, 27 | right: theme.spacing.unit * 2 28 | }, 29 | dialogButton: { 30 | float: "right" 31 | } 32 | }); 33 | 34 | class DevicesView extends React.Component { 35 | constructor(props) { 36 | super(props); 37 | 38 | this.state = { 39 | addDialogOpened: false 40 | }; 41 | } 42 | 43 | openAddDeviceDialog = () => { 44 | this.setState(...this.state, { addDialogOpened: true }); 45 | }; 46 | 47 | closeAddDeviceDialog = () => { 48 | this.setState(...this.state, { addDialogOpened: false }); 49 | }; 50 | 51 | render() { 52 | const { classes, schemas } = this.props; 53 | return ( 54 |
55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 73 | 77 |
78 | ); 79 | } 80 | } 81 | 82 | export default withStyles(styles)(DevicesView); 83 | -------------------------------------------------------------------------------- /ledfx/frontend/views/Presets/Presets.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import withStyles from "@material-ui/core/styles/withStyles"; 4 | 5 | import Typography from '@material-ui/core/Typography'; 6 | // import Slider from '@material-ui/core/Slider'; 7 | import Input from '@material-ui/core/Input'; 8 | import { connect } from "react-redux"; 9 | 10 | import Grid from "@material-ui/core/Grid"; 11 | import Card from "@material-ui/core/Card"; 12 | import CardContent from "@material-ui/core/CardContent"; 13 | import Button from "@material-ui/core/Button"; 14 | import AddIcon from "@material-ui/icons/Add"; 15 | 16 | import PresetsCard from "frontend/components/PresetCard/PresetCard.jsx"; 17 | import PresetsConfigDialog from "frontend/components/PresetConfigDialog/PresetConfigDialog.jsx"; 18 | import AddPresetCard from "frontend/components/AddPresetCard/AddPresetCard"; 19 | import { getPresets } from 'frontend/actions'; 20 | import { includeKeyInObject } from 'frontend/utils/helpers'; 21 | 22 | const styles = theme => ({ 23 | cardResponsive: { 24 | width: "100%", 25 | overflowX: "auto" 26 | }, 27 | button: { 28 | position: "absolute", 29 | bottom: theme.spacing.unit * 2, 30 | right: theme.spacing.unit * 2 31 | }, 32 | dialogButton: { 33 | float: "right" 34 | } 35 | }); 36 | 37 | class PresetsView extends React.Component { 38 | constructor(props) { 39 | super(props); 40 | } 41 | 42 | componentDidMount = () => { 43 | this.props.getPresets() 44 | } 45 | 46 | render() { 47 | const { classes } = this.props; 48 | return ( 49 |
50 | 51 | 52 | 53 | 54 | 55 | 56 | {renderPresets(this.props.presets)} 57 | 58 | 59 | 60 |
61 | ); 62 | } 63 | } 64 | 65 | const renderPresets = (presets) => Object.keys(presets).map((key) => ()) 66 | 67 | 68 | const mapStateToProps = state => ({ 69 | presets: state.presets 70 | }) 71 | 72 | const mapDispatchToProps = (dispatch) => ({ 73 | getPresets: () => dispatch(getPresets()) 74 | }) 75 | 76 | export default connect(mapStateToProps, mapDispatchToProps)(withStyles(styles)(PresetsView)); -------------------------------------------------------------------------------- /ledfx/frontend/views/Settings/Settings.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from "react"; 2 | import { connect } from "react-redux"; 3 | 4 | import { makeStyles } from '@material-ui/core/styles'; 5 | import Grid from "@material-ui/core/Grid"; 6 | import Card from "@material-ui/core/Card"; 7 | import CardContent from "@material-ui/core/CardContent"; 8 | import InputLabel from '@material-ui/core/InputLabel'; 9 | import MenuItem from '@material-ui/core/MenuItem'; 10 | import FormHelperText from '@material-ui/core/FormHelperText'; 11 | import FormControl from '@material-ui/core/FormControl'; 12 | import Select from '@material-ui/core/Select'; 13 | 14 | import { getAudioDevices, setAudioDevice } from 'frontend/actions'; 15 | 16 | const useStyles = makeStyles(theme => ({ 17 | root: { 18 | '& > *': { 19 | margin: theme.spacing(1), 20 | }, 21 | }, 22 | })) 23 | 24 | const SettingsView = ({ getAudioDevices, setAudioDevice, settings }) => { 25 | const classes = useStyles(); 26 | 27 | useEffect(() => { 28 | getAudioDevices() 29 | }, []) 30 | 31 | const { audioDevices } = settings 32 | 33 | return ( 34 |
35 | {audioDevices && ()} 36 |
37 | ); 38 | } 39 | 40 | const AudioCard = ({ audioDevices, setAudioDevice }) => { 41 | const activeDeviceIndex = audioDevices['active_device_index'] 42 | 43 | const [selectedIndex, setSelectedIndex] = useState(activeDeviceIndex) 44 | 45 | const handleAudioSelected = (index) => { 46 | setSelectedIndex(index) 47 | setAudioDevice(index) 48 | } 49 | 50 | return ( 51 | 52 |

Audio

53 |

Current device: {audioDevices.devices[activeDeviceIndex]}

54 | 55 | 62 | 63 |
64 |
65 | ) 66 | } 67 | 68 | const renderAudioInputSelect = (audioInputs) => { 69 | return Object.keys(audioInputs).map((key) => ({audioInputs[key]})) 73 | } 74 | 75 | const mapStateToProps = state => ({ 76 | settings: state.settings 77 | }) 78 | 79 | const mapDispatchToProps = (dispatch) => ({ 80 | getAudioDevices: () => dispatch(getAudioDevices()), 81 | setAudioDevice: (index) => dispatch(setAudioDevice(index)) 82 | }) 83 | 84 | export default connect(mapStateToProps, mapDispatchToProps)(SettingsView); 85 | -------------------------------------------------------------------------------- /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 | 11 | _LOGGER = logging.getLogger(__name__) 12 | 13 | class HttpServer(object): 14 | def __init__(self, ledfx, host, port): 15 | """Initialize the HTTP server""" 16 | 17 | self.app = web.Application(loop=ledfx.loop) 18 | self.api = RestApi(ledfx) 19 | aiohttp_jinja2.setup( 20 | self.app, 21 | loader=jinja2.PackageLoader('ledfx_frontend', '.')) 22 | self.register_routes() 23 | 24 | self._ledfx = ledfx 25 | self.host = host 26 | self.port = port 27 | 28 | @aiohttp_jinja2.template('index.html') 29 | async def index(self, request): 30 | return {} 31 | 32 | def register_routes(self): 33 | self.api.register_routes(self.app) 34 | self.app.router.add_static( 35 | '/static', path=ledfx_frontend.where(), name='static') 36 | 37 | self.app.router.add_route('get', '/', self.index) 38 | self.app.router.add_route('get', '/{extra:.+}', self.index) 39 | 40 | async def start(self): 41 | self.handler = self.app.make_handler(loop=self._ledfx.loop) 42 | 43 | try: 44 | self.server = await self._ledfx.loop.create_server( 45 | self.handler, self.host, self.port) 46 | except OSError as error: 47 | _LOGGER.error("Failed to create HTTP server at port %d: %s", 48 | self.port, error) 49 | 50 | self.base_url = ('http://{}:{}').format(self.host, self.port) 51 | print(('Started webinterface at {}').format(self.base_url)) 52 | 53 | async def stop(self): 54 | if self.server: 55 | self.server.close() 56 | await self.server.wait_closed() 57 | await self.app.shutdown() 58 | if self.handler: 59 | await self.handler.shutdown(10) 60 | await self.app.cleanup() -------------------------------------------------------------------------------- /ledfx_frontend/0468512747540774a71f3b070b96f76d.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ahodges9/LedFx/2abeced175fbff5fd4d31f28e6c5a61ad1d042e2/ledfx_frontend/0468512747540774a71f3b070b96f76d.png -------------------------------------------------------------------------------- /ledfx_frontend/3b38551e8c65303682cb2dd770ce2618.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ahodges9/LedFx/2abeced175fbff5fd4d31f28e6c5a61ad1d042e2/ledfx_frontend/3b38551e8c65303682cb2dd770ce2618.png -------------------------------------------------------------------------------- /ledfx_frontend/__init__.py: -------------------------------------------------------------------------------- 1 | """LedFx Frontend""" 2 | import os 3 | 4 | def where(): 5 | return os.path.dirname(__file__) -------------------------------------------------------------------------------- /ledfx_frontend/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | LedFx 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /ledfx_frontend/ledfx_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ahodges9/LedFx/2abeced175fbff5fd4d31f28e6c5a61ad1d042e2/ledfx_frontend/ledfx_icon.png -------------------------------------------------------------------------------- /ledfx_frontend/small_black_alpha.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ahodges9/LedFx/2abeced175fbff5fd4d31f28e6c5a61ad1d042e2/ledfx_frontend/small_black_alpha.png -------------------------------------------------------------------------------- /ledfx_frontend/style.css: -------------------------------------------------------------------------------- 1 | html, body { 2 | font-family: 'Roboto', 'Helvetica', sans-serif; 3 | font-weight: 300; 4 | } 5 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "LedFx", 3 | "version": "0.2.0", 4 | "description": "LedFx", 5 | "main": "index.js", 6 | "scripts": { 7 | "build": "webpack -p --progress --config webpack.config.js", 8 | "dev": "webpack --progress -d --config webpack.config.js", 9 | "test": "echo \"Error: no test specified\" && exit 1", 10 | "watch": "webpack --progress -d --config webpack.config.js --watch" 11 | }, 12 | "author": "Austin Hodges", 13 | "license": "MIT", 14 | "repository": { 15 | "type": "git", 16 | "url": "https://github.com/ahodges9/ledfx.git" 17 | }, 18 | "devDependencies": { 19 | "@babel/runtime": "^7.6.2", 20 | "axios": "^0.18.1", 21 | "babel-core": "^6.26.3", 22 | "babel-loader": "^7.1.5", 23 | "babel-minify-webpack-plugin": "^0.3.1", 24 | "babel-plugin-import-rename": "^1.0.1", 25 | "babel-plugin-module-resolver": "^3.2.0", 26 | "babel-plugin-transform-class-properties": "^6.24.1", 27 | "babel-plugin-transform-es3-member-expression-literals": "^6.22.0", 28 | "babel-plugin-transform-es3-property-literals": "^6.22.0", 29 | "babel-plugin-transform-object-rest-spread": "^6.26.0", 30 | "babel-plugin-transform-react-jsx": "^6.24.1", 31 | "babel-preset-es2015": "^6.24.1", 32 | "babel-preset-react": "^6.24.1", 33 | "chart.js": "^2.8.0", 34 | "copy-webpack-plugin": "^4.6.0", 35 | "cross-fetch": "^2.2.3", 36 | "css-loader": "^1.0.1", 37 | "file-loader": "^1.1.11", 38 | "history": "^4.10.1", 39 | "mini-css-extract-plugin": "^0.4.5", 40 | "react": "^16.12.0", 41 | "react-chartjs-2": "^2.8.0", 42 | "react-dom": "^16.12.0", 43 | "react-event-listener": "^0.6.6", 44 | "react-redux": "^5.1.2", 45 | "react-router-dom": "^4.3.1", 46 | "react-schema-form": "^0.6.15", 47 | "recompose": "^0.30.0", 48 | "redux": "^4.0.4", 49 | "redux-thunk": "^2.3.0", 50 | "sockette": "^2.0.6", 51 | "tv4": "^1.3.0", 52 | "webpack": "^4.41.2", 53 | "webpack-cli": "^3.3.10" 54 | }, 55 | "dependencies": { 56 | "@material-ui/core": "^4.8.3", 57 | "@material-ui/icons": "^4.5.1", 58 | "core-js": "^3.4.1", 59 | "glob": "^7.1.6", 60 | "spotify-web-api-node": "^4.0.0" 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /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('--no_bump', action='store_true', 34 | help='Create a version bump commit.') 35 | 36 | arguments = parser.parse_args() 37 | 38 | branch = execute_command("git rev-parse --abbrev-ref HEAD") 39 | if branch != "master": 40 | print("Releases must be pushed from the master branch.") 41 | return 42 | 43 | current_commit = execute_command("git rev-parse HEAD") 44 | master_commit = execute_command("git rev-parse master@{upstream}") 45 | if current_commit != master_commit: 46 | print("Release must be pushed when up-to-date with origin.") 47 | return 48 | 49 | git_diff = execute_command("git diff HEAD") 50 | if git_diff: 51 | print("Release must be pushed without any staged changes.") 52 | return 53 | 54 | if not arguments.no_bump: 55 | # Bump the version based on the release type 56 | major = MAJOR_VERSION 57 | minor = MINOR_VERSION 58 | if arguments.type == 'major': 59 | major += 1 60 | minor = 0 61 | elif arguments.type == 'minor': 62 | minor += 1 63 | 64 | # Write the new version to consts.py 65 | write_version(major, minor) 66 | 67 | subprocess.run([ 68 | 'git', 'commit', '-am', 'Version Bump for Release {}.{}'.format(major, minor)]) 69 | subprocess.run(['git', 'push', 'origin', 'master']) 70 | 71 | shutil.rmtree("dist", ignore_errors=True) 72 | subprocess.run(['python', 'setup.py', 'sdist', 'bdist_wheel']) 73 | subprocess.run(['python', '-m', 'twine', 'upload', 'dist/*', '--skip-existing']) 74 | 75 | 76 | if __name__ == '__main__': 77 | main() 78 | -------------------------------------------------------------------------------- /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 | numpy>=1.13.3 10 | voluptuous==0.11.1 11 | pyaudio==0.2.11 12 | sacn==1.3 13 | aiohttp==3.3.2 14 | aiohttp_jinja2==1.0.0 15 | pyyaml>=5.1 16 | aubio>=0.4.8 -------------------------------------------------------------------------------- /requirements_docs.txt: -------------------------------------------------------------------------------- 1 | sphinx==1.7.6 2 | sphinxcontrib-websupport==1.1.0 3 | sphinx-autodoc-typehints==1.3.0 4 | sphinx-autodoc-annotation==1.0.post1 5 | sphinx_rtd_theme==0.4.1 6 | travis-sphinx==2.2.1 -------------------------------------------------------------------------------- /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 8 | 9 | [options] 10 | packages = find: 11 | include_package_data = true 12 | zip_safe = false 13 | 14 | [options.packages.find] 15 | exclude = 16 | tests 17 | tests.* 18 | 19 | [test] 20 | addopts = tests 21 | 22 | [tool:pytest] 23 | testpaths = tests 24 | norecursedirs = 25 | dist 26 | build 27 | .tox 28 | 29 | [build_sphinx] 30 | source_dir = docs/source 31 | build_dir = docs/build 32 | 33 | [flake8] 34 | exclude = 35 | .tox 36 | build 37 | dist 38 | .eggs 39 | 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' 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_URL = 'http://github.com/ahodges9/ledfx' 13 | 14 | # Need to install numpy first 15 | SETUP_REQUIRES = [ 16 | 'numpy>=1.13.3' 17 | ] 18 | 19 | INSTALL_REQUIRES = [ 20 | 'numpy>=1.13.3', 21 | 'voluptuous==0.11.1', 22 | 'pyaudio>=0.2.11', 23 | 'sacn==1.3', 24 | 'aiohttp==3.3.2', 25 | 'aiohttp_jinja2==1.0.0', 26 | 'requests>=2.22.0', 27 | 'pyyaml>=5.1', 28 | 'aubio>=0.4.8', 29 | 'pypiwin32>=223;platform_system=="Windows"' 30 | ] 31 | 32 | setup( 33 | name=PROJECT_PACKAGE_NAME, 34 | version=PROJECT_VERSION, 35 | license = PROJECT_LICENSE, 36 | author=PROJECT_AUTHOR, 37 | author_email=PROJECT_AUTHOR_EMAIL, 38 | url=PROJECT_URL, 39 | install_requires=INSTALL_REQUIRES, 40 | setup_requires=SETUP_REQUIRES, 41 | python_requires=const.REQUIRED_PYTHON_STRING, 42 | include_package_data=True, 43 | zip_safe=False, 44 | entry_points={ 45 | 'console_scripts': [ 46 | 'ledfx = ledfx.__main__:main' 47 | ] 48 | }, 49 | package_data={ 50 | 'ledfx_frontend':['*'], 51 | '': ['*.npy'] 52 | }, 53 | 54 | ) 55 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const webpack = require("webpack"); 3 | const MiniCssExtractPlugin = require("mini-css-extract-plugin"); 4 | const CopyWebpackPlugin = require("copy-webpack-plugin"); 5 | 6 | const config = { 7 | entry: __dirname + "/ledfx/frontend/index.jsx", 8 | module: { 9 | rules: [ 10 | { 11 | test: /\.jsx?/, 12 | exclude: /node_modules/, 13 | use: [ 14 | { 15 | loader: "babel-loader", 16 | options: { 17 | presets: ["es2015", "react"], 18 | plugins: [ 19 | "transform-class-properties", 20 | "transform-react-jsx", 21 | "transform-object-rest-spread" 22 | ] 23 | } 24 | } 25 | ] 26 | }, 27 | { 28 | test: /\.css$/, 29 | use: [ 30 | { 31 | loader: MiniCssExtractPlugin.loader 32 | }, 33 | "css-loader" 34 | ] 35 | }, 36 | { 37 | test: /\.(png|jpg|gif)$/, 38 | use: [ 39 | { 40 | loader: "file-loader", 41 | options: {} 42 | } 43 | ] 44 | } 45 | ] 46 | }, 47 | output: { 48 | path: __dirname + "/ledfx_frontend", 49 | publicPath: "/static/", 50 | filename: "bundle.js" 51 | }, 52 | resolve: { 53 | extensions: [".js", ".jsx", ".css"], 54 | modules: [path.resolve("./ledfx"), path.resolve("./node_modules")] 55 | }, 56 | plugins: [ 57 | new CopyWebpackPlugin([ 58 | {from: 'ledfx/frontend/dist', to: __dirname + "/ledfx_frontend"} 59 | ]), 60 | new MiniCssExtractPlugin({ 61 | filename: "style.css", 62 | }) 63 | ] 64 | }; 65 | 66 | module.exports = config; 67 | --------------------------------------------------------------------------------