├── ledfxcontroller ├── api │ ├── helpers.py │ ├── effects.py │ ├── effect.py │ ├── devices.py │ ├── __init__.py │ ├── device_effects.py │ ├── device.py │ └── websocket.py ├── __init__.py ├── frontend │ ├── external │ │ ├── font-awesome │ │ │ ├── fonts │ │ │ │ ├── FontAwesome.otf │ │ │ │ ├── fontawesome-webfont.eot │ │ │ │ ├── fontawesome-webfont.ttf │ │ │ │ ├── fontawesome-webfont.woff │ │ │ │ └── fontawesome-webfont.woff2 │ │ │ ├── less │ │ │ │ ├── screen-reader.less │ │ │ │ ├── fixed-width.less │ │ │ │ ├── larger.less │ │ │ │ ├── list.less │ │ │ │ ├── core.less │ │ │ │ ├── stacked.less │ │ │ │ ├── font-awesome.less │ │ │ │ ├── bordered-pulled.less │ │ │ │ ├── rotated-flipped.less │ │ │ │ ├── path.less │ │ │ │ ├── animated.less │ │ │ │ └── mixins.less │ │ │ ├── scss │ │ │ │ ├── _fixed-width.scss │ │ │ │ ├── _screen-reader.scss │ │ │ │ ├── _larger.scss │ │ │ │ ├── _list.scss │ │ │ │ ├── _core.scss │ │ │ │ ├── font-awesome.scss │ │ │ │ ├── _stacked.scss │ │ │ │ ├── _bordered-pulled.scss │ │ │ │ ├── _rotated-flipped.scss │ │ │ │ ├── _path.scss │ │ │ │ ├── _animated.scss │ │ │ │ └── _mixins.scss │ │ │ └── css │ │ │ │ └── font-awesome.css.map │ │ ├── jquery-easing │ │ │ ├── jquery.easing.compatibility.js │ │ │ ├── jquery.easing.min.js │ │ │ └── jquery.easing.js │ │ ├── bootstrap │ │ │ └── css │ │ │ │ ├── bootstrap-reboot.min.css │ │ │ │ └── bootstrap-reboot.css │ │ └── datatables │ │ │ ├── dataTables.bootstrap4.js │ │ │ └── dataTables.bootstrap4.css │ ├── index.html │ ├── js │ │ ├── base.min.js │ │ ├── device.js │ │ ├── base.js │ │ └── graph.js │ ├── dev_tools.html │ ├── base.html │ ├── device.html │ └── css │ │ ├── style.min.css │ │ └── style.css ├── effects │ ├── rainbow.py │ ├── math.py │ ├── spectrum.py │ ├── wavelength.py │ ├── temporal.py │ ├── gradient.py │ ├── __init__.py │ ├── mel.py │ └── audio.py ├── consts.py ├── devices │ ├── udp_rgb.py │ ├── e131.py │ └── __init__.py ├── color.py ├── __main__.py ├── http.py ├── core.py ├── config.py └── utils.py ├── MANIFEST.in ├── web_interface.png ├── AUTHORS.rst ├── config └── sample_config.yaml ├── tests ├── conftest.py └── test_ledfx.py ├── requirements.txt ├── CHANGELOG.rst ├── .gitignore ├── .coveragerc ├── setup.cfg ├── LICENSE.txt ├── setup.py └── README.rst /ledfxcontroller/api/helpers.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ledfxcontroller/__init__.py: -------------------------------------------------------------------------------- 1 | """LedFxController""" -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.rst 2 | graft ledfxcontroller/frontend -------------------------------------------------------------------------------- /web_interface.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Aircoookie/LedFx/HEAD/web_interface.png -------------------------------------------------------------------------------- /AUTHORS.rst: -------------------------------------------------------------------------------- 1 | ============ 2 | Contributors 3 | ============ 4 | 5 | * Austin Hodges 6 | -------------------------------------------------------------------------------- /ledfxcontroller/frontend/external/font-awesome/fonts/FontAwesome.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Aircoookie/LedFx/HEAD/ledfxcontroller/frontend/external/font-awesome/fonts/FontAwesome.otf -------------------------------------------------------------------------------- /ledfxcontroller/frontend/external/font-awesome/fonts/fontawesome-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Aircoookie/LedFx/HEAD/ledfxcontroller/frontend/external/font-awesome/fonts/fontawesome-webfont.eot -------------------------------------------------------------------------------- /ledfxcontroller/frontend/external/font-awesome/fonts/fontawesome-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Aircoookie/LedFx/HEAD/ledfxcontroller/frontend/external/font-awesome/fonts/fontawesome-webfont.ttf -------------------------------------------------------------------------------- /ledfxcontroller/frontend/external/font-awesome/fonts/fontawesome-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Aircoookie/LedFx/HEAD/ledfxcontroller/frontend/external/font-awesome/fonts/fontawesome-webfont.woff -------------------------------------------------------------------------------- /ledfxcontroller/frontend/external/font-awesome/fonts/fontawesome-webfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Aircoookie/LedFx/HEAD/ledfxcontroller/frontend/external/font-awesome/fonts/fontawesome-webfont.woff2 -------------------------------------------------------------------------------- /ledfxcontroller/frontend/external/font-awesome/less/screen-reader.less: -------------------------------------------------------------------------------- 1 | // Screen Readers 2 | // ------------------------- 3 | 4 | .sr-only { .sr-only(); } 5 | .sr-only-focusable { .sr-only-focusable(); } 6 | -------------------------------------------------------------------------------- /ledfxcontroller/frontend/external/font-awesome/less/fixed-width.less: -------------------------------------------------------------------------------- 1 | // Fixed Width Icons 2 | // ------------------------- 3 | .@{fa-css-prefix}-fw { 4 | width: (18em / 14); 5 | text-align: center; 6 | } 7 | -------------------------------------------------------------------------------- /ledfxcontroller/frontend/external/font-awesome/scss/_fixed-width.scss: -------------------------------------------------------------------------------- 1 | // Fixed Width Icons 2 | // ------------------------- 3 | .#{$fa-css-prefix}-fw { 4 | width: (18em / 14); 5 | text-align: center; 6 | } 7 | -------------------------------------------------------------------------------- /config/sample_config.yaml: -------------------------------------------------------------------------------- 1 | 2 | host: 127.0.0.1 3 | port: 8888 4 | 5 | devices: 6 | - type: e131 7 | name: Sample Device 8 | host: 192.168.1.100 9 | pixel_count: 100 10 | max_brightness: 0.25 -------------------------------------------------------------------------------- /ledfxcontroller/frontend/external/font-awesome/scss/_screen-reader.scss: -------------------------------------------------------------------------------- 1 | // Screen Readers 2 | // ------------------------- 3 | 4 | .sr-only { @include sr-only(); } 5 | .sr-only-focusable { @include sr-only-focusable(); } 6 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | """ 4 | https://pytest.org/latest/plugins.html 5 | """ 6 | from __future__ import print_function, absolute_import, division 7 | 8 | # import pytest 9 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | # Requirements. These can can be installed with: 2 | # pip install -r requirements.txt 3 | 4 | numpy==1.13.3 5 | voluptuous==0.11.1 6 | pyaudio==0.2.11 7 | sacn==1.3 8 | aiohttp==3.3.2 9 | aiohttp_jinja2==1.0.0 10 | pyyaml>=3.11,<4 11 | -------------------------------------------------------------------------------- /ledfxcontroller/frontend/index.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% set title = "Dashboard" %} 4 | 5 | {% block content %} 6 | {% if devices %} 7 |

You have {{ devices |length }} device(s) configured!

8 | {% else %} 9 |

No devices in configuration file

10 | {% endif %} 11 | {% endblock %} -------------------------------------------------------------------------------- /CHANGELOG.rst: -------------------------------------------------------------------------------- 1 | ========= 2 | Changelog 3 | ========= 4 | 5 | Version 0.1.0a0 6 | =========== 7 | 8 | - Initial alpha release with very basic end-to-end support 9 | - Added a framework for highly customizable effects and outputs 10 | - Added some basic effects and audio reaction ones 11 | - Added very basic web interface to allow configuration 12 | -------------------------------------------------------------------------------- /ledfxcontroller/frontend/external/font-awesome/less/larger.less: -------------------------------------------------------------------------------- 1 | // Icon Sizes 2 | // ------------------------- 3 | 4 | /* makes the font 33% larger relative to the icon container */ 5 | .@{fa-css-prefix}-lg { 6 | font-size: (4em / 3); 7 | line-height: (3em / 4); 8 | vertical-align: -15%; 9 | } 10 | .@{fa-css-prefix}-2x { font-size: 2em; } 11 | .@{fa-css-prefix}-3x { font-size: 3em; } 12 | .@{fa-css-prefix}-4x { font-size: 4em; } 13 | .@{fa-css-prefix}-5x { font-size: 5em; } 14 | -------------------------------------------------------------------------------- /ledfxcontroller/api/effects.py: -------------------------------------------------------------------------------- 1 | from ledfxcontroller.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 = { 'status' : 'success' , 'effects' : self.ledfx.effects.types() } 14 | return web.Response(text=json.dumps(response), status=200) 15 | -------------------------------------------------------------------------------- /ledfxcontroller/frontend/external/font-awesome/scss/_larger.scss: -------------------------------------------------------------------------------- 1 | // Icon Sizes 2 | // ------------------------- 3 | 4 | /* makes the font 33% larger relative to the icon container */ 5 | .#{$fa-css-prefix}-lg { 6 | font-size: (4em / 3); 7 | line-height: (3em / 4); 8 | vertical-align: -15%; 9 | } 10 | .#{$fa-css-prefix}-2x { font-size: 2em; } 11 | .#{$fa-css-prefix}-3x { font-size: 3em; } 12 | .#{$fa-css-prefix}-4x { font-size: 4em; } 13 | .#{$fa-css-prefix}-5x { font-size: 5em; } 14 | -------------------------------------------------------------------------------- /ledfxcontroller/frontend/external/font-awesome/less/list.less: -------------------------------------------------------------------------------- 1 | // List Icons 2 | // ------------------------- 3 | 4 | .@{fa-css-prefix}-ul { 5 | padding-left: 0; 6 | margin-left: @fa-li-width; 7 | list-style-type: none; 8 | > li { position: relative; } 9 | } 10 | .@{fa-css-prefix}-li { 11 | position: absolute; 12 | left: -@fa-li-width; 13 | width: @fa-li-width; 14 | top: (2em / 14); 15 | text-align: center; 16 | &.@{fa-css-prefix}-lg { 17 | left: (-@fa-li-width + (4em / 14)); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /ledfxcontroller/frontend/external/font-awesome/scss/_list.scss: -------------------------------------------------------------------------------- 1 | // List Icons 2 | // ------------------------- 3 | 4 | .#{$fa-css-prefix}-ul { 5 | padding-left: 0; 6 | margin-left: $fa-li-width; 7 | list-style-type: none; 8 | > li { position: relative; } 9 | } 10 | .#{$fa-css-prefix}-li { 11 | position: absolute; 12 | left: -$fa-li-width; 13 | width: $fa-li-width; 14 | top: (2em / 14); 15 | text-align: center; 16 | &.#{$fa-css-prefix}-lg { 17 | left: -$fa-li-width + (4em / 14); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /ledfxcontroller/frontend/external/font-awesome/less/core.less: -------------------------------------------------------------------------------- 1 | // Base Class Definition 2 | // ------------------------- 3 | 4 | .@{fa-css-prefix} { 5 | display: inline-block; 6 | font: normal normal normal @fa-font-size-base/@fa-line-height-base FontAwesome; // shortening font declaration 7 | font-size: inherit; // can't have font-size inherit on line above, so need to override 8 | text-rendering: auto; // optimizelegibility throws things off #1094 9 | -webkit-font-smoothing: antialiased; 10 | -moz-osx-font-smoothing: grayscale; 11 | 12 | } 13 | -------------------------------------------------------------------------------- /ledfxcontroller/frontend/external/font-awesome/scss/_core.scss: -------------------------------------------------------------------------------- 1 | // Base Class Definition 2 | // ------------------------- 3 | 4 | .#{$fa-css-prefix} { 5 | display: inline-block; 6 | font: normal normal normal #{$fa-font-size-base}/#{$fa-line-height-base} FontAwesome; // shortening font declaration 7 | font-size: inherit; // can't have font-size inherit on line above, so need to override 8 | text-rendering: auto; // optimizelegibility throws things off #1094 9 | -webkit-font-smoothing: antialiased; 10 | -moz-osx-font-smoothing: grayscale; 11 | 12 | } 13 | -------------------------------------------------------------------------------- /ledfxcontroller/frontend/external/font-awesome/scss/font-awesome.scss: -------------------------------------------------------------------------------- 1 | /*! 2 | * Font Awesome 4.7.0 by @davegandy - http://fontawesome.io - @fontawesome 3 | * License - http://fontawesome.io/license (Font: SIL OFL 1.1, CSS: MIT License) 4 | */ 5 | 6 | @import "variables"; 7 | @import "mixins"; 8 | @import "path"; 9 | @import "core"; 10 | @import "larger"; 11 | @import "fixed-width"; 12 | @import "list"; 13 | @import "bordered-pulled"; 14 | @import "animated"; 15 | @import "rotated-flipped"; 16 | @import "stacked"; 17 | @import "icons"; 18 | @import "screen-reader"; 19 | -------------------------------------------------------------------------------- /ledfxcontroller/effects/rainbow.py: -------------------------------------------------------------------------------- 1 | from ledfxcontroller.effects.temporal import TemporalEffect 2 | from ledfxcontroller.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.Required('frequency', default = 1.0): 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 -------------------------------------------------------------------------------- /ledfxcontroller/frontend/external/font-awesome/less/stacked.less: -------------------------------------------------------------------------------- 1 | // Stacked Icons 2 | // ------------------------- 3 | 4 | .@{fa-css-prefix}-stack { 5 | position: relative; 6 | display: inline-block; 7 | width: 2em; 8 | height: 2em; 9 | line-height: 2em; 10 | vertical-align: middle; 11 | } 12 | .@{fa-css-prefix}-stack-1x, .@{fa-css-prefix}-stack-2x { 13 | position: absolute; 14 | left: 0; 15 | width: 100%; 16 | text-align: center; 17 | } 18 | .@{fa-css-prefix}-stack-1x { line-height: inherit; } 19 | .@{fa-css-prefix}-stack-2x { font-size: 2em; } 20 | .@{fa-css-prefix}-inverse { color: @fa-inverse; } 21 | -------------------------------------------------------------------------------- /ledfxcontroller/frontend/external/font-awesome/scss/_stacked.scss: -------------------------------------------------------------------------------- 1 | // Stacked Icons 2 | // ------------------------- 3 | 4 | .#{$fa-css-prefix}-stack { 5 | position: relative; 6 | display: inline-block; 7 | width: 2em; 8 | height: 2em; 9 | line-height: 2em; 10 | vertical-align: middle; 11 | } 12 | .#{$fa-css-prefix}-stack-1x, .#{$fa-css-prefix}-stack-2x { 13 | position: absolute; 14 | left: 0; 15 | width: 100%; 16 | text-align: center; 17 | } 18 | .#{$fa-css-prefix}-stack-1x { line-height: inherit; } 19 | .#{$fa-css-prefix}-stack-2x { font-size: 2em; } 20 | .#{$fa-css-prefix}-inverse { color: $fa-inverse; } 21 | -------------------------------------------------------------------------------- /ledfxcontroller/frontend/external/font-awesome/less/font-awesome.less: -------------------------------------------------------------------------------- 1 | /*! 2 | * Font Awesome 4.7.0 by @davegandy - http://fontawesome.io - @fontawesome 3 | * License - http://fontawesome.io/license (Font: SIL OFL 1.1, CSS: MIT License) 4 | */ 5 | 6 | @import "variables.less"; 7 | @import "mixins.less"; 8 | @import "path.less"; 9 | @import "core.less"; 10 | @import "larger.less"; 11 | @import "fixed-width.less"; 12 | @import "list.less"; 13 | @import "bordered-pulled.less"; 14 | @import "animated.less"; 15 | @import "rotated-flipped.less"; 16 | @import "stacked.less"; 17 | @import "icons.less"; 18 | @import "screen-reader.less"; 19 | -------------------------------------------------------------------------------- /.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 | # Project files 16 | .ropeproject 17 | .project 18 | .pydevproject 19 | .settings 20 | .idea 21 | .vscode 22 | 23 | # Package files 24 | *.egg 25 | *.eggs/ 26 | .installed.cfg 27 | *.egg-info 28 | 29 | # Unittest and coverage 30 | htmlcov/* 31 | .coverage 32 | .tox 33 | junit.xml 34 | coverage.xml 35 | 36 | # Build and docs folder/files 37 | build/* 38 | dist/* 39 | sdist/* 40 | docs/api/* 41 | docs/_build/* 42 | cover/* 43 | MANIFEST 44 | -------------------------------------------------------------------------------- /.coveragerc: -------------------------------------------------------------------------------- 1 | # .coveragerc to control coverage.py 2 | [run] 3 | branch = True 4 | source = */ledfxcontroller/* 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 | -------------------------------------------------------------------------------- /ledfxcontroller/api/effect.py: -------------------------------------------------------------------------------- 1 | from ledfxcontroller.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 | -------------------------------------------------------------------------------- /ledfxcontroller/consts.py: -------------------------------------------------------------------------------- 1 | import pathlib 2 | 3 | __version__ = "0.1.0a0" 4 | __author__ = "Austin Hodges" 5 | __copyright__ = "Austin Hodges" 6 | __license__ = "mit" 7 | 8 | REQUIRED_PYTHON_VERSION = (3, 5, 3) 9 | REQUIRED_PYTHON_STRING = '>={}.{}.{}'.format( 10 | REQUIRED_PYTHON_VERSION[0], 11 | REQUIRED_PYTHON_VERSION[1], 12 | REQUIRED_PYTHON_VERSION[2]) 13 | 14 | PROJECT_ROOT = pathlib.Path(__file__).parent 15 | PROJECT_VERSION = __version__ 16 | 17 | COLOR_TABLE = { 18 | "Red":(255,0,0), 19 | "Orange":(255,40,0), 20 | "Yellow":(255,255,0), 21 | "Green":(0,255,0), 22 | "Blue":(0,0,255), 23 | "Light Blue":(1,247,161), 24 | "Purple":(80,5,252), 25 | "Pink":(255,0,178), 26 | "White":(255,255,255) 27 | } -------------------------------------------------------------------------------- /ledfxcontroller/frontend/external/font-awesome/less/bordered-pulled.less: -------------------------------------------------------------------------------- 1 | // Bordered & Pulled 2 | // ------------------------- 3 | 4 | .@{fa-css-prefix}-border { 5 | padding: .2em .25em .15em; 6 | border: solid .08em @fa-border-color; 7 | border-radius: .1em; 8 | } 9 | 10 | .@{fa-css-prefix}-pull-left { float: left; } 11 | .@{fa-css-prefix}-pull-right { float: right; } 12 | 13 | .@{fa-css-prefix} { 14 | &.@{fa-css-prefix}-pull-left { margin-right: .3em; } 15 | &.@{fa-css-prefix}-pull-right { margin-left: .3em; } 16 | } 17 | 18 | /* Deprecated as of 4.4.0 */ 19 | .pull-right { float: right; } 20 | .pull-left { float: left; } 21 | 22 | .@{fa-css-prefix} { 23 | &.pull-left { margin-right: .3em; } 24 | &.pull-right { margin-left: .3em; } 25 | } 26 | -------------------------------------------------------------------------------- /ledfxcontroller/frontend/external/font-awesome/scss/_bordered-pulled.scss: -------------------------------------------------------------------------------- 1 | // Bordered & Pulled 2 | // ------------------------- 3 | 4 | .#{$fa-css-prefix}-border { 5 | padding: .2em .25em .15em; 6 | border: solid .08em $fa-border-color; 7 | border-radius: .1em; 8 | } 9 | 10 | .#{$fa-css-prefix}-pull-left { float: left; } 11 | .#{$fa-css-prefix}-pull-right { float: right; } 12 | 13 | .#{$fa-css-prefix} { 14 | &.#{$fa-css-prefix}-pull-left { margin-right: .3em; } 15 | &.#{$fa-css-prefix}-pull-right { margin-left: .3em; } 16 | } 17 | 18 | /* Deprecated as of 4.4.0 */ 19 | .pull-right { float: right; } 20 | .pull-left { float: left; } 21 | 22 | .#{$fa-css-prefix} { 23 | &.pull-left { margin-right: .3em; } 24 | &.pull-right { margin-left: .3em; } 25 | } 26 | -------------------------------------------------------------------------------- /ledfxcontroller/frontend/external/font-awesome/less/rotated-flipped.less: -------------------------------------------------------------------------------- 1 | // Rotated & Flipped Icons 2 | // ------------------------- 3 | 4 | .@{fa-css-prefix}-rotate-90 { .fa-icon-rotate(90deg, 1); } 5 | .@{fa-css-prefix}-rotate-180 { .fa-icon-rotate(180deg, 2); } 6 | .@{fa-css-prefix}-rotate-270 { .fa-icon-rotate(270deg, 3); } 7 | 8 | .@{fa-css-prefix}-flip-horizontal { .fa-icon-flip(-1, 1, 0); } 9 | .@{fa-css-prefix}-flip-vertical { .fa-icon-flip(1, -1, 2); } 10 | 11 | // Hook for IE8-9 12 | // ------------------------- 13 | 14 | :root .@{fa-css-prefix}-rotate-90, 15 | :root .@{fa-css-prefix}-rotate-180, 16 | :root .@{fa-css-prefix}-rotate-270, 17 | :root .@{fa-css-prefix}-flip-horizontal, 18 | :root .@{fa-css-prefix}-flip-vertical { 19 | filter: none; 20 | } 21 | -------------------------------------------------------------------------------- /ledfxcontroller/frontend/external/font-awesome/scss/_rotated-flipped.scss: -------------------------------------------------------------------------------- 1 | // Rotated & Flipped Icons 2 | // ------------------------- 3 | 4 | .#{$fa-css-prefix}-rotate-90 { @include fa-icon-rotate(90deg, 1); } 5 | .#{$fa-css-prefix}-rotate-180 { @include fa-icon-rotate(180deg, 2); } 6 | .#{$fa-css-prefix}-rotate-270 { @include fa-icon-rotate(270deg, 3); } 7 | 8 | .#{$fa-css-prefix}-flip-horizontal { @include fa-icon-flip(-1, 1, 0); } 9 | .#{$fa-css-prefix}-flip-vertical { @include fa-icon-flip(1, -1, 2); } 10 | 11 | // Hook for IE8-9 12 | // ------------------------- 13 | 14 | :root .#{$fa-css-prefix}-rotate-90, 15 | :root .#{$fa-css-prefix}-rotate-180, 16 | :root .#{$fa-css-prefix}-rotate-270, 17 | :root .#{$fa-css-prefix}-flip-horizontal, 18 | :root .#{$fa-css-prefix}-flip-vertical { 19 | filter: none; 20 | } 21 | -------------------------------------------------------------------------------- /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 :: 4 - Beta 7 | Programming Language :: Python 8 | 9 | [options] 10 | packages = find: 11 | include_package_data = true 12 | zip_safe = false 13 | 14 | # [options.entry_points] 15 | # console_scripts = 16 | # ledfx = ledfxcontroller.__main__:main 17 | 18 | [options.packages.find] 19 | exclude = 20 | tests 21 | tests.* 22 | 23 | [test] 24 | addopts = tests 25 | 26 | [tool:pytest] 27 | testpaths = tests 28 | norecursedirs = 29 | dist 30 | build 31 | .tox 32 | 33 | [build_sphinx] 34 | source_dir = docs 35 | build_dir = docs/_build 36 | 37 | [flake8] 38 | exclude = 39 | .tox 40 | build 41 | dist 42 | .eggs 43 | docs/conf.py -------------------------------------------------------------------------------- /ledfxcontroller/frontend/external/font-awesome/less/path.less: -------------------------------------------------------------------------------- 1 | /* FONT PATH 2 | * -------------------------- */ 3 | 4 | @font-face { 5 | font-family: 'FontAwesome'; 6 | src: url('@{fa-font-path}/fontawesome-webfont.eot?v=@{fa-version}'); 7 | src: url('@{fa-font-path}/fontawesome-webfont.eot?#iefix&v=@{fa-version}') format('embedded-opentype'), 8 | url('@{fa-font-path}/fontawesome-webfont.woff2?v=@{fa-version}') format('woff2'), 9 | url('@{fa-font-path}/fontawesome-webfont.woff?v=@{fa-version}') format('woff'), 10 | url('@{fa-font-path}/fontawesome-webfont.ttf?v=@{fa-version}') format('truetype'), 11 | url('@{fa-font-path}/fontawesome-webfont.svg?v=@{fa-version}#fontawesomeregular') format('svg'); 12 | // src: url('@{fa-font-path}/FontAwesome.otf') format('opentype'); // used when developing fonts 13 | font-weight: normal; 14 | font-style: normal; 15 | } 16 | -------------------------------------------------------------------------------- /ledfxcontroller/frontend/external/font-awesome/scss/_path.scss: -------------------------------------------------------------------------------- 1 | /* FONT PATH 2 | * -------------------------- */ 3 | 4 | @font-face { 5 | font-family: 'FontAwesome'; 6 | src: url('#{$fa-font-path}/fontawesome-webfont.eot?v=#{$fa-version}'); 7 | src: url('#{$fa-font-path}/fontawesome-webfont.eot?#iefix&v=#{$fa-version}') format('embedded-opentype'), 8 | url('#{$fa-font-path}/fontawesome-webfont.woff2?v=#{$fa-version}') format('woff2'), 9 | url('#{$fa-font-path}/fontawesome-webfont.woff?v=#{$fa-version}') format('woff'), 10 | url('#{$fa-font-path}/fontawesome-webfont.ttf?v=#{$fa-version}') format('truetype'), 11 | url('#{$fa-font-path}/fontawesome-webfont.svg?v=#{$fa-version}#fontawesomeregular') format('svg'); 12 | // src: url('#{$fa-font-path}/FontAwesome.otf') format('opentype'); // used when developing fonts 13 | font-weight: normal; 14 | font-style: normal; 15 | } 16 | -------------------------------------------------------------------------------- /ledfxcontroller/frontend/external/font-awesome/less/animated.less: -------------------------------------------------------------------------------- 1 | // Animated Icons 2 | // -------------------------- 3 | 4 | .@{fa-css-prefix}-spin { 5 | -webkit-animation: fa-spin 2s infinite linear; 6 | animation: fa-spin 2s infinite linear; 7 | } 8 | 9 | .@{fa-css-prefix}-pulse { 10 | -webkit-animation: fa-spin 1s infinite steps(8); 11 | animation: fa-spin 1s infinite steps(8); 12 | } 13 | 14 | @-webkit-keyframes fa-spin { 15 | 0% { 16 | -webkit-transform: rotate(0deg); 17 | transform: rotate(0deg); 18 | } 19 | 100% { 20 | -webkit-transform: rotate(359deg); 21 | transform: rotate(359deg); 22 | } 23 | } 24 | 25 | @keyframes fa-spin { 26 | 0% { 27 | -webkit-transform: rotate(0deg); 28 | transform: rotate(0deg); 29 | } 30 | 100% { 31 | -webkit-transform: rotate(359deg); 32 | transform: rotate(359deg); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /ledfxcontroller/frontend/external/font-awesome/scss/_animated.scss: -------------------------------------------------------------------------------- 1 | // Spinning Icons 2 | // -------------------------- 3 | 4 | .#{$fa-css-prefix}-spin { 5 | -webkit-animation: fa-spin 2s infinite linear; 6 | animation: fa-spin 2s infinite linear; 7 | } 8 | 9 | .#{$fa-css-prefix}-pulse { 10 | -webkit-animation: fa-spin 1s infinite steps(8); 11 | animation: fa-spin 1s infinite steps(8); 12 | } 13 | 14 | @-webkit-keyframes fa-spin { 15 | 0% { 16 | -webkit-transform: rotate(0deg); 17 | transform: rotate(0deg); 18 | } 19 | 100% { 20 | -webkit-transform: rotate(359deg); 21 | transform: rotate(359deg); 22 | } 23 | } 24 | 25 | @keyframes fa-spin { 26 | 0% { 27 | -webkit-transform: rotate(0deg); 28 | transform: rotate(0deg); 29 | } 30 | 100% { 31 | -webkit-transform: rotate(359deg); 32 | transform: rotate(359deg); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from datetime import datetime as dt 4 | from setuptools import setup, find_packages 5 | import ledfxcontroller.consts as const 6 | 7 | PROJECT_PACKAGE_NAME = 'ledfxcontroller' 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 | REQUIRES = [ 15 | 'numpy==1.13.3', 16 | 'scipy>=0.15.1', 17 | 'voluptuous==0.11.1', 18 | 'pyaudio==0.2.11', 19 | 'sacn==1.3', 20 | 'aiohttp==3.3.2', 21 | 'aiohttp_jinja2==1.0.0', 22 | 'pyyaml>=3.11,<4' 23 | ] 24 | 25 | setup( 26 | name=PROJECT_PACKAGE_NAME, 27 | version=PROJECT_VERSION, 28 | license = PROJECT_LICENSE, 29 | author=PROJECT_AUTHOR, 30 | author_email=PROJECT_AUTHOR_EMAIL, 31 | url=PROJECT_URL, 32 | install_requires=REQUIRES, 33 | python_requires=const.REQUIRED_PYTHON_STRING, 34 | include_package_data=True, 35 | entry_points={ 36 | 'console_scripts': [ 37 | 'ledfx = ledfxcontroller.__main__:main' 38 | ] 39 | } 40 | ) 41 | -------------------------------------------------------------------------------- /ledfxcontroller/frontend/js/base.min.js: -------------------------------------------------------------------------------- 1 | !function(e){"use strict";e('.navbar-sidenav [data-toggle="tooltip"]').tooltip({template:''}),e("#sidenavToggler").click(function(o){o.preventDefault(),e("body").toggleClass("sidenav-toggled"),e(".navbar-sidenav .nav-link-collapse").addClass("collapsed"),e(".navbar-sidenav .sidenav-second-level, .navbar-sidenav .sidenav-third-level").removeClass("show")}),e(".navbar-sidenav .nav-link-collapse").click(function(o){o.preventDefault(),e("body").removeClass("sidenav-toggled")}),e("body.fixed-nav .navbar-sidenav, body.fixed-nav .sidenav-toggler, body.fixed-nav .navbar-collapse").on("mousewheel DOMMouseScroll",function(e){var o=e.originalEvent,t=o.wheelDelta||-o.detail;this.scrollTop+=30*(t<0?1:-1),e.preventDefault()}),e(document).scroll(function(){e(this).scrollTop()>100?e(".scroll-to-top").fadeIn():e(".scroll-to-top").fadeOut()}),e('[data-toggle="tooltip"]').tooltip(),e(document).on("click","a.scroll-to-top",function(o){var t=e(this);e("html, body").stop().animate({scrollTop:e(t.attr("href")).offset().top},1e3,"easeInOutExpo"),o.preventDefault()})}(jQuery); -------------------------------------------------------------------------------- /ledfxcontroller/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=0.0, alpha_decay=0.5, alpha_rise=0.5): 24 | """Small rise / decay factors = more smoothing""" 25 | assert 0.0 < alpha_decay < 1.0, 'Invalid decay smoothing factor' 26 | assert 0.0 < alpha_rise < 1.0, 'Invalid rise smoothing factor' 27 | self.alpha_decay = alpha_decay 28 | self.alpha_rise = alpha_rise 29 | self.value = val 30 | 31 | def update(self, value): 32 | if isinstance(self.value, (list, np.ndarray, tuple)): 33 | alpha = value - self.value 34 | alpha[alpha > 0.0] = self.alpha_rise 35 | alpha[alpha <= 0.0] = self.alpha_decay 36 | else: 37 | alpha = self.alpha_rise if value > self.value else self.alpha_decay 38 | self.value = alpha * value + (1.0 - alpha) * self.value 39 | return self.value -------------------------------------------------------------------------------- /ledfxcontroller/effects/spectrum.py: -------------------------------------------------------------------------------- 1 | from ledfxcontroller.effects.audio import AudioReactiveEffect, ExpFilter 2 | import ledfxcontroller.effects.math as math 3 | import voluptuous as vol 4 | import numpy as np 5 | 6 | 7 | class SpectrumAudioEffect(AudioReactiveEffect): 8 | 9 | NAME = "Spectrum" 10 | _prev_y = None 11 | 12 | def audio_data_updated(self, data): 13 | 14 | # Grab the filtered and interpolated melbank data 15 | y = data.interpolated_melbank(self.pixel_count // 2, filtered = False) 16 | filtered_y = data.interpolated_melbank(self.pixel_count // 2, filtered = True) 17 | if self._prev_y is None: 18 | self._prev_y = y 19 | 20 | # Update all the filters 21 | r = data.get_filter( 22 | filter_key = "filtered_difference", 23 | filter_size = self.pixel_count // 2, 24 | alpha_decay = 0.2, 25 | alpha_rise = 0.99).update(y - filtered_y) 26 | g = np.abs(y - self._prev_y) 27 | b = data.get_filter( 28 | filter_key = "filtered_difference", 29 | filter_size = self.pixel_count // 2, 30 | alpha_decay = 0.1, 31 | alpha_rise = 0.5).update(y) 32 | 33 | self._prev_y = y 34 | 35 | # Mirror the color channels for symmetric output 36 | self.r = np.concatenate((r[::-1], r)) 37 | self.g = np.concatenate((g[::-1], g)) 38 | self.b = np.concatenate((b[::-1], b)) 39 | output = np.array([self.r,self.g,self.b]) * 255 40 | 41 | self.pixels = output.T -------------------------------------------------------------------------------- /ledfxcontroller/effects/wavelength.py: -------------------------------------------------------------------------------- 1 | from ledfxcontroller.effects.audio import AudioReactiveEffect 2 | from ledfxcontroller.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', default = 3.0): float 15 | }) 16 | 17 | def audio_data_updated(self, data): 18 | 19 | # Grab the filtered and interpolated melbank data 20 | y = data.interpolated_melbank(self.pixel_count // 2, filtered = False) 21 | filtered_y = data.interpolated_melbank(self.pixel_count // 2, filtered = True) 22 | 23 | # Grab the filtered difference between the filtered melbank and the 24 | # raw melbank. 25 | r = data.get_filter( 26 | filter_key = "filtered_difference", 27 | filter_size = self.pixel_count // 2, 28 | alpha_decay = 0.2, 29 | alpha_rise = 0.99).update(y - filtered_y) 30 | 31 | # Zip the melbank differences with itself. Not really sure why this is 32 | # done instaed of interpolating up to self.pixel_count but not messing 33 | # with effects yet. 34 | r = np.array([j for i in zip(r,r) for j in i]) 35 | 36 | # Apply the melbank data to the gradient curve and update the pixels 37 | self.pixels = self.apply_gradient(r) 38 | -------------------------------------------------------------------------------- /ledfxcontroller/devices/udp_rgb.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from ledfxcontroller.devices import Device 3 | import voluptuous as vol 4 | import numpy as np 5 | import time 6 | import os 7 | import platform 8 | 9 | _LOGGER = logging.getLogger(__name__) 10 | 11 | 12 | class UdpRgbDevice(Device): 13 | """ESP8266 UDP device support""" 14 | 15 | CONFIG_SCHEMA = vol.Schema({ 16 | vol.Required('host'): str, 17 | vol.Required('universe', default=1): int, 18 | vol.Required('universe_size', default=512): int, 19 | vol.Required('channel_offset', default=1): int, 20 | vol.Required(vol.Any('pixel_count', 'channel_count')): vol.Coerce(int) 21 | }) 22 | 23 | def __init__(self, config): 24 | self._config = config 25 | import socket 26 | # we take these from the config.yaml 27 | self._ip = self._config['host'] 28 | self._port = self._config['port'] 29 | self._sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 30 | 31 | @property 32 | def pixel_count(self): 33 | return int(self._config['pixel_count']) 34 | 35 | def activate(self): 36 | 37 | super().activate() 38 | 39 | def deactivate(self): 40 | super().deactivate() 41 | 42 | def flush(self, data): 43 | 44 | p = np.copy(np.clip(data, 0, 255).astype(int)) 45 | 46 | m = [] 47 | m.append(2) 48 | m.append(2) 49 | for i in range(len(p)): 50 | m.append(p[i][0]) # Pixel red value 51 | m.append(p[i][1]) # Pixel green value 52 | m.append(p[i][2]) # Pixel blue value 53 | m = bytes(m) 54 | self._sock.sendto(m, (self._ip, self._port)) 55 | -------------------------------------------------------------------------------- /ledfxcontroller/color.py: -------------------------------------------------------------------------------- 1 | from collections import namedtuple 2 | 3 | RGB = namedtuple('RGB','red, green, blue') 4 | 5 | COLORS = { 6 | 'aqua': RGB(0, 255, 255), 7 | 'blue': RGB(0, 0, 255), 8 | 'gold': RGB(255, 215, 0), 9 | 'green': RGB(0, 128, 0), 10 | 'hotpink': RGB(255, 105, 180), 11 | 'lightblue': RGB(173, 216, 230), 12 | 'lightgreen': RGB(152, 251, 152), 13 | 'lightpink': RGB(255, 182, 193), 14 | 'lightyellow': RGB(255, 255, 224), 15 | 'magenta': RGB(255, 0, 255), 16 | 'maroon': RGB(128, 0, 0), 17 | 'mint': RGB(189, 252, 201), 18 | 'navy': RGB(0, 0, 128), 19 | 'orange': RGB(255, 128, 0), 20 | 'orangered': RGB(255, 69, 0), 21 | 'pink': RGB(255, 192, 203), 22 | 'plum': RGB(221, 160, 221), 23 | 'purple': RGB(128, 0, 128), 24 | 'red': RGB(255, 0, 0), 25 | 'royalblue': RGB(65, 105, 225), 26 | 'sepia': RGB(94, 38, 18), 27 | 'skyblue': RGB(135, 206, 235), 28 | 'springgreen': RGB(0, 255, 127), 29 | 'steelblue': RGB(70, 130, 180), 30 | 'tan': RGB(210, 180, 140), 31 | 'teal': RGB(0, 128, 128), 32 | 'turquoise': RGB(64, 224, 208), 33 | 'turquoiseblue': RGB(0, 199, 140), 34 | 'violet': RGB(238, 130, 238), 35 | 'violetred': RGB(208, 32, 144), 36 | 'white': RGB(255, 255, 255), 37 | 'yellow': RGB(255, 255, 0) 38 | } 39 | 40 | GRADIENTS = { 41 | "spectral" : ["red", "orange", "yellow", "green", "lightblue", "blue", "purple", "pink"], 42 | "dancefloor": ["red", "pink", "purple", "blue"], 43 | "sunset" : ["red", "orange", "yellow"], 44 | "ocean" : ["green", "lightblue", "blue"], 45 | "jungle" : ["green", "red", "orange"], 46 | "sunny" : ["yellow", "lightblue", "orange", "blue"], 47 | "fruity" : ["orange", "blue"], 48 | "peach" : ["orange", "pink"], 49 | "rust" : ["orange", "red"] 50 | } -------------------------------------------------------------------------------- /ledfxcontroller/frontend/external/font-awesome/less/mixins.less: -------------------------------------------------------------------------------- 1 | // Mixins 2 | // -------------------------- 3 | 4 | .fa-icon() { 5 | display: inline-block; 6 | font: normal normal normal @fa-font-size-base/@fa-line-height-base FontAwesome; // shortening font declaration 7 | font-size: inherit; // can't have font-size inherit on line above, so need to override 8 | text-rendering: auto; // optimizelegibility throws things off #1094 9 | -webkit-font-smoothing: antialiased; 10 | -moz-osx-font-smoothing: grayscale; 11 | 12 | } 13 | 14 | .fa-icon-rotate(@degrees, @rotation) { 15 | -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=@{rotation})"; 16 | -webkit-transform: rotate(@degrees); 17 | -ms-transform: rotate(@degrees); 18 | transform: rotate(@degrees); 19 | } 20 | 21 | .fa-icon-flip(@horiz, @vert, @rotation) { 22 | -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=@{rotation}, mirror=1)"; 23 | -webkit-transform: scale(@horiz, @vert); 24 | -ms-transform: scale(@horiz, @vert); 25 | transform: scale(@horiz, @vert); 26 | } 27 | 28 | 29 | // Only display content to screen readers. A la Bootstrap 4. 30 | // 31 | // See: http://a11yproject.com/posts/how-to-hide-content/ 32 | 33 | .sr-only() { 34 | position: absolute; 35 | width: 1px; 36 | height: 1px; 37 | padding: 0; 38 | margin: -1px; 39 | overflow: hidden; 40 | clip: rect(0,0,0,0); 41 | border: 0; 42 | } 43 | 44 | // Use in conjunction with .sr-only to only display content when it's focused. 45 | // 46 | // Useful for "Skip to main content" links; see http://www.w3.org/TR/2013/NOTE-WCAG20-TECHS-20130905/G1 47 | // 48 | // Credit: HTML5 Boilerplate 49 | 50 | .sr-only-focusable() { 51 | &:active, 52 | &:focus { 53 | position: static; 54 | width: auto; 55 | height: auto; 56 | margin: 0; 57 | overflow: visible; 58 | clip: auto; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /ledfxcontroller/frontend/external/font-awesome/scss/_mixins.scss: -------------------------------------------------------------------------------- 1 | // Mixins 2 | // -------------------------- 3 | 4 | @mixin fa-icon() { 5 | display: inline-block; 6 | font: normal normal normal #{$fa-font-size-base}/#{$fa-line-height-base} FontAwesome; // shortening font declaration 7 | font-size: inherit; // can't have font-size inherit on line above, so need to override 8 | text-rendering: auto; // optimizelegibility throws things off #1094 9 | -webkit-font-smoothing: antialiased; 10 | -moz-osx-font-smoothing: grayscale; 11 | 12 | } 13 | 14 | @mixin fa-icon-rotate($degrees, $rotation) { 15 | -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=#{$rotation})"; 16 | -webkit-transform: rotate($degrees); 17 | -ms-transform: rotate($degrees); 18 | transform: rotate($degrees); 19 | } 20 | 21 | @mixin fa-icon-flip($horiz, $vert, $rotation) { 22 | -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=#{$rotation}, mirror=1)"; 23 | -webkit-transform: scale($horiz, $vert); 24 | -ms-transform: scale($horiz, $vert); 25 | transform: scale($horiz, $vert); 26 | } 27 | 28 | 29 | // Only display content to screen readers. A la Bootstrap 4. 30 | // 31 | // See: http://a11yproject.com/posts/how-to-hide-content/ 32 | 33 | @mixin sr-only { 34 | position: absolute; 35 | width: 1px; 36 | height: 1px; 37 | padding: 0; 38 | margin: -1px; 39 | overflow: hidden; 40 | clip: rect(0,0,0,0); 41 | border: 0; 42 | } 43 | 44 | // Use in conjunction with .sr-only to only display content when it's focused. 45 | // 46 | // Useful for "Skip to main content" links; see http://www.w3.org/TR/2013/NOTE-WCAG20-TECHS-20130905/G1 47 | // 48 | // Credit: HTML5 Boilerplate 49 | 50 | @mixin sr-only-focusable { 51 | &:active, 52 | &:focus { 53 | position: static; 54 | width: auto; 55 | height: auto; 56 | margin: 0; 57 | overflow: visible; 58 | clip: auto; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /ledfxcontroller/effects/temporal.py: -------------------------------------------------------------------------------- 1 | import time 2 | import logging 3 | from ledfxcontroller.effects import Effect 4 | from threading import Thread 5 | import voluptuous as vol 6 | 7 | _LOGGER = logging.getLogger(__name__) 8 | DEFAULT_RATE = 1.0 / 60.0 9 | 10 | @Effect.no_registration 11 | class TemporalEffect(Effect): 12 | _thread_active = False 13 | _thread = None 14 | 15 | CONFIG_SCHEMA = vol.Schema({ 16 | vol.Required('speed', default = 1.0): float 17 | }) 18 | 19 | def thread_function(self): 20 | 21 | while self._thread_active: 22 | startTime = time.time() 23 | 24 | # Treat the return value of the effect loop as a speed modifier 25 | # such that effects that are nartually faster or slower can have 26 | # a consistent feel. 27 | sleepInterval = self.effect_loop() 28 | if sleepInterval is None: 29 | sleepInterval = 1.0 30 | sleepInterval = sleepInterval * DEFAULT_RATE 31 | 32 | # Calculate the time to sleep accounting for potential heavy 33 | # frame assembly operations 34 | timeToSleep = (sleepInterval / self._config['speed']) - (time.time() - startTime) 35 | if timeToSleep > 0: 36 | time.sleep(timeToSleep) 37 | 38 | def effect_loop(self): 39 | """ 40 | Triggered periodically based on the effect speed and 41 | any additional effect modifiers 42 | """ 43 | pass 44 | 45 | def activate(self, pixel_count): 46 | super().activate(pixel_count) 47 | 48 | self._thread_active = True 49 | self._thread = Thread(target = self.thread_function) 50 | self._thread.start() 51 | 52 | def deactivate(self): 53 | if self._thread_active: 54 | self._thread_active = False 55 | self._thread.join() 56 | self._thread = None 57 | 58 | super().deactivate() 59 | -------------------------------------------------------------------------------- /ledfxcontroller/frontend/external/jquery-easing/jquery.easing.compatibility.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Easing Compatibility v1 - http://gsgd.co.uk/sandbox/jquery/easing 3 | * 4 | * Adds compatibility for applications that use the pre 1.2 easing names 5 | * 6 | * Copyright (c) 2007 George Smith 7 | * Licensed under the MIT License: 8 | * http://www.opensource.org/licenses/mit-license.php 9 | */ 10 | 11 | (function($){ 12 | $.extend( $.easing, 13 | { 14 | easeIn: function (x, t, b, c, d) { 15 | return $.easing.easeInQuad(x, t, b, c, d); 16 | }, 17 | easeOut: function (x, t, b, c, d) { 18 | return $.easing.easeOutQuad(x, t, b, c, d); 19 | }, 20 | easeInOut: function (x, t, b, c, d) { 21 | return $.easing.easeInOutQuad(x, t, b, c, d); 22 | }, 23 | expoin: function(x, t, b, c, d) { 24 | return $.easing.easeInExpo(x, t, b, c, d); 25 | }, 26 | expoout: function(x, t, b, c, d) { 27 | return $.easing.easeOutExpo(x, t, b, c, d); 28 | }, 29 | expoinout: function(x, t, b, c, d) { 30 | return $.easing.easeInOutExpo(x, t, b, c, d); 31 | }, 32 | bouncein: function(x, t, b, c, d) { 33 | return $.easing.easeInBounce(x, t, b, c, d); 34 | }, 35 | bounceout: function(x, t, b, c, d) { 36 | return $.easing.easeOutBounce(x, t, b, c, d); 37 | }, 38 | bounceinout: function(x, t, b, c, d) { 39 | return $.easing.easeInOutBounce(x, t, b, c, d); 40 | }, 41 | elasin: function(x, t, b, c, d) { 42 | return $.easing.easeInElastic(x, t, b, c, d); 43 | }, 44 | elasout: function(x, t, b, c, d) { 45 | return $.easing.easeOutElastic(x, t, b, c, d); 46 | }, 47 | elasinout: function(x, t, b, c, d) { 48 | return $.easing.easeInOutElastic(x, t, b, c, d); 49 | }, 50 | backin: function(x, t, b, c, d) { 51 | return $.easing.easeInBack(x, t, b, c, d); 52 | }, 53 | backout: function(x, t, b, c, d) { 54 | return $.easing.easeOutBack(x, t, b, c, d); 55 | }, 56 | backinout: function(x, t, b, c, d) { 57 | return $.easing.easeInOutBack(x, t, b, c, d); 58 | } 59 | });})(jQuery); 60 | -------------------------------------------------------------------------------- /ledfxcontroller/frontend/js/device.js: -------------------------------------------------------------------------------- 1 | class DeviceWebsocketConnection { 2 | constructor(deviceId) { 3 | this.deviceId = deviceId 4 | 5 | this.onConnected = function(){}; 6 | this.onDisconnected = function(){}; 7 | this.onPixelUpdate = function(){}; 8 | this.pixelRefreshRate = 30; 9 | } 10 | 11 | connect() { 12 | this.disconnect(); 13 | 14 | var self = this; 15 | var wsUri = (window.location.protocol=='https:'&&'wss://'||'ws://')+ 16 | window.location.host+'/api/websocket'; 17 | this.conn = new WebSocket(wsUri); 18 | 19 | this.conn.onopen = function() { 20 | self.onConnected() 21 | self.getPixelData() 22 | }; 23 | 24 | this.conn.onmessage = function(e) { 25 | var data = JSON.parse(e.data); 26 | switch (data.action) { 27 | case 'update_pixels': 28 | self.onPixelUpdate(data) 29 | break; 30 | } 31 | }; 32 | 33 | this.conn.onclose = function() { 34 | self.conn = null; 35 | self.onDisconnected() 36 | }; 37 | } 38 | 39 | disconnect() { 40 | if (this.conn != null) { 41 | this.conn.close(); 42 | this.conn = null; 43 | } 44 | } 45 | 46 | toggleConnection() { 47 | if (this.isConnected()) { 48 | this.disconnect(); 49 | } else { 50 | this.connect(); 51 | } 52 | } 53 | 54 | isConnected() { 55 | return this.conn != null; 56 | } 57 | 58 | getPixelData() { 59 | if (this.conn != null) { 60 | this.conn.send(JSON.stringify({ 61 | 'id': 0, 62 | 'type': 'get_pixels', 63 | 'device_id': this.deviceId 64 | })); 65 | 66 | var self = this; 67 | setTimeout(function() { 68 | self.getPixelData(); 69 | }, 1000 / self.pixelRefreshRate); 70 | } 71 | } 72 | } -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | LedFxController 2 | =============== 3 | 4 | LedFx is a network based LED effect controller with support for a wide range of effects. Effect range from simple static gradients all the way to audio reactive effects that dance to music! 5 | 6 | To get started with LedFx run the following from the project root: 7 | 8 | .. code:: bash 9 | 10 | python setup.py install 11 | ledfx --open-ui 12 | 13 | Device Support 14 | ============== 15 | 16 | LedFx currently supports E1.31 capable devices, including the `ESPixelStick firmware `__ for any ESP8266 based controller. Upon first launch LedFx will create a default configuration file in the '.ledfx' folder inside the active user profile. The exact path will be printed to the command window. To add a device modify config.yaml as follows: 17 | 18 | .. code-block:: yaml 19 | 20 | devices: 21 | sample_device_1: 22 | type: e131 23 | name: Sample Device 24 | host: 192.168.1.100 25 | universe: 1 26 | channel_offset: 0 27 | channel_count: 300 28 | max_brightness: 1.0 29 | 30 | Optionally, the config can be simplified down to: 31 | 32 | .. code-block:: yaml 33 | 34 | devices: 35 | sample_device_1: 36 | type: e131 37 | name: Sample Device 38 | host: 192.168.1.100 39 | pixel_count: 100 40 | 41 | Also, connecting devices via an UDP packet with 8-bit RGB data is now supported. 42 | Just use the simplified config, but change the device type and specify the network port: 43 | 44 | .. code-block:: yaml 45 | 46 | type: udp_rgb 47 | port: 21324 48 | 49 | Web-Interface 50 | ============= 51 | 52 | LedFx is intended to be ran on a small PC such as a Raspberry Pi, thus all configuration is done through a web-interface. The current UI is very simple and enable control of an individual device's effect, as well as providers a way to visualize the effect. 53 | 54 | |screenshot-webinterface| 55 | 56 | .. |screenshot-webinterface| image:: https://raw.githubusercontent.com/ahodges9/LedFx/master/web_interface.png 57 | -------------------------------------------------------------------------------- /ledfxcontroller/frontend/js/base.js: -------------------------------------------------------------------------------- 1 | (function($) { 2 | "use strict"; // Start of use strict 3 | // Configure tooltips for collapsed side navigation 4 | $('.navbar-sidenav [data-toggle="tooltip"]').tooltip({ 5 | template: '' 6 | }) 7 | // Toggle the side navigation 8 | $("#sidenavToggler").click(function(e) { 9 | e.preventDefault(); 10 | $("body").toggleClass("sidenav-toggled"); 11 | $(".navbar-sidenav .nav-link-collapse").addClass("collapsed"); 12 | $(".navbar-sidenav .sidenav-second-level, .navbar-sidenav .sidenav-third-level").removeClass("show"); 13 | }); 14 | // Force the toggled class to be removed when a collapsible nav link is clicked 15 | $(".navbar-sidenav .nav-link-collapse").click(function(e) { 16 | e.preventDefault(); 17 | $("body").removeClass("sidenav-toggled"); 18 | }); 19 | // Prevent the content wrapper from scrolling when the fixed side navigation hovered over 20 | $('body.fixed-nav .navbar-sidenav, body.fixed-nav .sidenav-toggler, body.fixed-nav .navbar-collapse').on('mousewheel DOMMouseScroll', function(e) { 21 | var e0 = e.originalEvent, 22 | delta = e0.wheelDelta || -e0.detail; 23 | this.scrollTop += (delta < 0 ? 1 : -1) * 30; 24 | e.preventDefault(); 25 | }); 26 | // Scroll to top button appear 27 | $(document).scroll(function() { 28 | var scrollDistance = $(this).scrollTop(); 29 | if (scrollDistance > 100) { 30 | $('.scroll-to-top').fadeIn(); 31 | } else { 32 | $('.scroll-to-top').fadeOut(); 33 | } 34 | }); 35 | // Configure tooltips globally 36 | $('[data-toggle="tooltip"]').tooltip() 37 | // Smooth scrolling using jQuery easing 38 | $(document).on('click', 'a.scroll-to-top', function(event) { 39 | var $anchor = $(this); 40 | $('html, body').stop().animate({ 41 | scrollTop: ($($anchor.attr('href')).offset().top) 42 | }, 1000, 'easeInOutExpo'); 43 | event.preventDefault(); 44 | }); 45 | })(jQuery); // End of use strict 46 | -------------------------------------------------------------------------------- /ledfxcontroller/api/devices.py: -------------------------------------------------------------------------------- 1 | from ledfxcontroller.config import save_config 2 | from ledfxcontroller.api import RestEndpoint 3 | from aiohttp import web 4 | import logging 5 | import json 6 | 7 | _LOGGER = logging.getLogger(__name__) 8 | 9 | class DevicesEndpoint(RestEndpoint): 10 | """REST end-point for querying and managing devices""" 11 | 12 | ENDPOINT_PATH = "/api/devices" 13 | 14 | async def get(self) -> web.Response: 15 | response = { 'status' : 'success' , 'devices' : {}} 16 | for device in self.ledfx.devices.values(): 17 | response['devices'][device.id] = {"name": device.name} 18 | 19 | return web.Response(text=json.dumps(response), status=200) 20 | 21 | async def put(self, request) -> web.Response: 22 | data = await request.json() 23 | 24 | device_config = data.get('config') 25 | if device_config is None: 26 | response = { 'status' : 'failed', 'reason': 'Required attribute "config" was not provided' } 27 | return web.Response(text=json.dumps(response), status=500) 28 | 29 | device_id = data.get('id') 30 | if device_id is None: 31 | response = { 'status' : 'failed', 'reason': 'Required attribute "id" was not provided' } 32 | return web.Response(text=json.dumps(response), status=500) 33 | 34 | # Remove the device it if already exist 35 | try: 36 | self.ledfx.devices.destroy(device_id) 37 | except AttributeError: 38 | pass 39 | 40 | # Create the device 41 | _LOGGER.info("Adding device with config", device_config) 42 | device = self.ledfx.devices.create( 43 | config = device_config, 44 | id = device_id, 45 | name = device_config.get('type')) 46 | 47 | # Update and save the configuration 48 | self.ledfx.config['devices'][device.id] = device_config 49 | save_config( 50 | config = self.ledfx.config, 51 | config_dir = self.ledfx.config_dir) 52 | 53 | response = { 'status' : 'success' } 54 | return web.Response(text=json.dumps(response), status=200) -------------------------------------------------------------------------------- /ledfxcontroller/api/__init__.py: -------------------------------------------------------------------------------- 1 | from ledfxcontroller.utils import BaseRegistry, RegistryLoader 2 | from aiohttp import web 3 | import logging 4 | import inspect 5 | import json 6 | 7 | """ 8 | REST API for LedFx: 9 | 10 | GET /api/devices 11 | PUT /api/devices 12 | GET /api/devices/{{device_id}} 13 | PUT /api/devices/{{device_id}} 14 | DEL /api/devices/{{device_id}} 15 | GET /api/devices/{{device_id}}/effects 16 | PUT /api/devices/{{device_id}}/effects 17 | DEL /api/devices/{{device_id}}/effects 18 | GET /api/devices/{{device_id}}/effects/{{effect_id}} 19 | PUT /api/devices/{{device_id}}/effects/{{effect_id}} 20 | DEL /api/devices/{{device_id}}/effects/{{effect_id}} 21 | 22 | GET /api/effects 23 | 24 | 25 | """ 26 | 27 | @BaseRegistry.no_registration 28 | class RestEndpoint(BaseRegistry): 29 | 30 | def __init__(self, ledfx): 31 | self.ledfx = ledfx 32 | 33 | async def handler(self, request: web.Request): 34 | 35 | method = getattr(self, request.method.lower(), None) 36 | if not method: 37 | raise web.HTTPMethodNotAllowed('') 38 | 39 | wanted_args = list(inspect.signature(method).parameters.keys()) 40 | available_args = request.match_info.copy() 41 | available_args.update({'request': request}) 42 | 43 | unsatisfied_args = set(wanted_args) - set(available_args.keys()) 44 | if unsatisfied_args: 45 | raise web.HttpBadRequest('') 46 | 47 | return await method(**{arg_name: available_args[arg_name] for arg_name in wanted_args}) 48 | 49 | class RestApi(RegistryLoader): 50 | 51 | PACKAGE_NAME = 'ledfxcontroller.api' 52 | 53 | def __init__(self, ledfx): 54 | super().__init__(RestEndpoint, self.PACKAGE_NAME, ledfx) 55 | self.ledfx = ledfx 56 | 57 | def register_routes(self, app): 58 | 59 | # Create the endpoints and register their routes 60 | for endpoint_type in self.types(): 61 | endpoint = self.create(endpoint_type, None, None, self.ledfx) 62 | app.router.add_route('*', endpoint.ENDPOINT_PATH, endpoint.handler, name = "api_{}".format(endpoint_type)) -------------------------------------------------------------------------------- /ledfxcontroller/frontend/external/jquery-easing/jquery.easing.min.js: -------------------------------------------------------------------------------- 1 | (function(factory){if(typeof define==="function"&&define.amd){define(["jquery"],function($){return factory($)})}else if(typeof module==="object"&&typeof module.exports==="object"){exports=factory(require("jquery"))}else{factory(jQuery)}})(function($){$.easing.jswing=$.easing.swing;var pow=Math.pow,sqrt=Math.sqrt,sin=Math.sin,cos=Math.cos,PI=Math.PI,c1=1.70158,c2=c1*1.525,c3=c1+1,c4=2*PI/3,c5=2*PI/4.5;function bounceOut(x){var n1=7.5625,d1=2.75;if(x<1/d1){return n1*x*x}else if(x<2/d1){return n1*(x-=1.5/d1)*x+.75}else if(x<2.5/d1){return n1*(x-=2.25/d1)*x+.9375}else{return n1*(x-=2.625/d1)*x+.984375}}$.extend($.easing,{def:"easeOutQuad",swing:function(x){return $.easing[$.easing.def](x)},easeInQuad:function(x){return x*x},easeOutQuad:function(x){return 1-(1-x)*(1-x)},easeInOutQuad:function(x){return x<.5?2*x*x:1-pow(-2*x+2,2)/2},easeInCubic:function(x){return x*x*x},easeOutCubic:function(x){return 1-pow(1-x,3)},easeInOutCubic:function(x){return x<.5?4*x*x*x:1-pow(-2*x+2,3)/2},easeInQuart:function(x){return x*x*x*x},easeOutQuart:function(x){return 1-pow(1-x,4)},easeInOutQuart:function(x){return x<.5?8*x*x*x*x:1-pow(-2*x+2,4)/2},easeInQuint:function(x){return x*x*x*x*x},easeOutQuint:function(x){return 1-pow(1-x,5)},easeInOutQuint:function(x){return x<.5?16*x*x*x*x*x:1-pow(-2*x+2,5)/2},easeInSine:function(x){return 1-cos(x*PI/2)},easeOutSine:function(x){return sin(x*PI/2)},easeInOutSine:function(x){return-(cos(PI*x)-1)/2},easeInExpo:function(x){return x===0?0:pow(2,10*x-10)},easeOutExpo:function(x){return x===1?1:1-pow(2,-10*x)},easeInOutExpo:function(x){return x===0?0:x===1?1:x<.5?pow(2,20*x-10)/2:(2-pow(2,-20*x+10))/2},easeInCirc:function(x){return 1-sqrt(1-pow(x,2))},easeOutCirc:function(x){return sqrt(1-pow(x-1,2))},easeInOutCirc:function(x){return x<.5?(1-sqrt(1-pow(2*x,2)))/2:(sqrt(1-pow(-2*x+2,2))+1)/2},easeInElastic:function(x){return x===0?0:x===1?1:-pow(2,10*x-10)*sin((x*10-10.75)*c4)},easeOutElastic:function(x){return x===0?0:x===1?1:pow(2,-10*x)*sin((x*10-.75)*c4)+1},easeInOutElastic:function(x){return x===0?0:x===1?1:x<.5?-(pow(2,20*x-10)*sin((20*x-11.125)*c5))/2:pow(2,-20*x+10)*sin((20*x-11.125)*c5)/2+1},easeInBack:function(x){return c3*x*x*x-c1*x*x},easeOutBack:function(x){return 1+c3*pow(x-1,3)+c1*pow(x-1,2)},easeInOutBack:function(x){return x<.5?pow(2*x,2)*((c2+1)*2*x-c2)/2:(pow(2*x-2,2)*((c2+1)*(x*2-2)+c2)+2)/2},easeInBounce:function(x){return 1-bounceOut(1-x)},easeOutBounce:bounceOut,easeInOutBounce:function(x){return x<.5?(1-bounceOut(1-2*x))/2:(1+bounceOut(2*x-1))/2}})}); -------------------------------------------------------------------------------- /ledfxcontroller/api/device_effects.py: -------------------------------------------------------------------------------- 1 | from ledfxcontroller.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/devices/{device_id}/effects" 11 | 12 | async def get(self, device_id) -> web.Response: 13 | device = self.ledfx.devices.get(device_id) 14 | if device is None: 15 | response = { 'not found': 404 } 16 | return web.Response(text=json.dumps(response), status=404) 17 | 18 | # Get the active effect 19 | response = { 'effects' : {}} 20 | if device.active_effect: 21 | effect_response = {} 22 | effect_response['config'] = device.active_effect.config 23 | effect_response['name'] = device.active_effect.name 24 | effect_response['type'] = device.active_effect.type 25 | response = { 'effects' : { device.active_effect.id : effect_response }} 26 | 27 | return web.Response(text=json.dumps(response), status=200) 28 | 29 | async def put(self, device_id, request) -> web.Response: 30 | device = self.ledfx.devices.get(device_id) 31 | if device is None: 32 | response = { 'not found': 404 } 33 | return web.Response(text=json.dumps(response), status=404) 34 | 35 | data = await request.json() 36 | effect_type = data.get('type') 37 | if effect_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 | effect_config = data.get('config') 42 | if effect_config is None: 43 | effect_config = {} 44 | 45 | # Create the effect and add it to the device 46 | effect = self.ledfx.effects.create( 47 | name = effect_type, config = effect_config) 48 | device.set_effect(effect) 49 | 50 | response = { 'status' : 'success' } 51 | return web.Response(text=json.dumps(response), status=200) 52 | 53 | async def delete(self, device_id) -> web.Response: 54 | device = self.ledfx.devices.get(device_id) 55 | if device is None: 56 | response = { 'not found': 404 } 57 | return web.Response(text=json.dumps(response), status=404) 58 | 59 | # Clear the effect 60 | device.clear_effect() 61 | 62 | response = { 'status' : 'success' } 63 | return web.Response(text=json.dumps(response), status=200) -------------------------------------------------------------------------------- /ledfxcontroller/frontend/dev_tools.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% set title = "Developer Tools" %} 4 | 5 | {% block javascript %} 6 | 28 | {% endblock %} 29 | 30 | {% block content %} 31 | 32 |
33 |
API Commands
34 |
35 | 36 |
37 |

38 | 39 | 45 |

46 |

47 | 48 | 49 |

50 |

51 | 52 | 53 |

54 | 55 |
56 | 57 |
58 |
59 | 60 |
61 |
Result
62 |
63 | 64 | 65 |
66 |
67 | 68 | {% endblock %} -------------------------------------------------------------------------------- /ledfxcontroller/api/device.py: -------------------------------------------------------------------------------- 1 | from ledfxcontroller.config import save_config 2 | from ledfxcontroller.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 | device = self.ledfx.devices.create( 41 | config = device_config, 42 | id = device_id, 43 | name = device_config.get('type')) 44 | 45 | # Update and save the configuration 46 | self.ledfx.config['devices'][device_id] = device_config 47 | save_config( 48 | config = self.ledfx.config, 49 | config_dir = self.ledfx.config_dir) 50 | 51 | response = { 'status' : 'success' } 52 | return web.Response(text=json.dumps(response), status=500) 53 | 54 | async def delete(self, device_id) -> web.Response: 55 | device = self.ledfx.devices.get(device_id) 56 | if device is None: 57 | response = { 'not found': 404 } 58 | return web.Response(text=json.dumps(response), status=404) 59 | 60 | self.ledfx.devices.destroy(device_id) 61 | 62 | # Update and save the configuration 63 | del self.ledfx.config['devices'][device_id] 64 | save_config( 65 | config = self.ledfx.config, 66 | config_dir = self.ledfx.config_dir) 67 | 68 | response = { 'status' : 'success' } 69 | return web.Response(text=json.dumps(response), status=200) -------------------------------------------------------------------------------- /ledfxcontroller/__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 ledfxcontroller.consts import ( 24 | REQUIRED_PYTHON_VERSION, REQUIRED_PYTHON_STRING, 25 | PROJECT_VERSION) 26 | from ledfxcontroller.core import LedFxController 27 | import ledfxcontroller.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='LedFxController {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 = LedFxController(config_dir = args.config) 89 | ledfx.start(open_ui = args.open_ui) 90 | 91 | if __name__ == "__main__": 92 | sys.exit(main()) -------------------------------------------------------------------------------- /ledfxcontroller/http.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import jinja2 3 | import aiohttp_jinja2 4 | from aiohttp import web 5 | import aiohttp 6 | from ledfxcontroller.consts import PROJECT_ROOT 7 | from ledfxcontroller.api import RestApi 8 | import numpy as np 9 | import json 10 | 11 | _LOGGER = logging.getLogger(__name__) 12 | 13 | class LedFxControllerHTTP(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, loader=jinja2.PackageLoader('ledfxcontroller', 'frontend')) 21 | self.register_routes() 22 | 23 | self.ledfx = ledfx 24 | self.host = host 25 | self.port = port 26 | 27 | @aiohttp_jinja2.template('index.html') 28 | async def index(self, request): 29 | return { 30 | 'devices': self.ledfx.devices.values() 31 | } 32 | 33 | @aiohttp_jinja2.template('dev_tools.html') 34 | async def dev_tools(self, request): 35 | return { 36 | 'devices': self.ledfx.devices.values() 37 | } 38 | 39 | @aiohttp_jinja2.template('device.html') 40 | async def device(self, request): 41 | device_id = request.match_info['device_id'] 42 | device = self.ledfx.devices.get_device(device_id) 43 | 44 | if device is None: 45 | return web.json_response({'error_message': 'Invalid device id'}) 46 | 47 | return { 48 | 'devices': self.ledfx.devices.values(), 49 | 'effects': self.ledfx.effects.classes(), 50 | 'device': device 51 | } 52 | 53 | def register_routes(self): 54 | self.app.router.add_get('/', self.index, name='index') 55 | self.app.router.add_get('/device/{device_id}', self.device, name='device') 56 | self.app.router.add_get('/dev_tools', self.dev_tools, name='dev_tools') 57 | 58 | self.app.router.add_static('/static/', 59 | path=PROJECT_ROOT / 'frontend', 60 | name='static') 61 | 62 | self.api.register_routes(self.app) 63 | 64 | async def start(self): 65 | self.handler = self.app.make_handler(loop=self.ledfx.loop) 66 | 67 | try: 68 | self.server = await self.ledfx.loop.create_server( 69 | self.handler, self.host, self.port) 70 | except OSError as error: 71 | _LOGGER.error("Failed to create HTTP server at port %d: %s", 72 | self.port, error) 73 | 74 | self.base_url = ('http://{}:{}').format(self.host, self.port) 75 | print(('Started webinterface at {}').format(self.base_url)) 76 | 77 | async def stop(self): 78 | if self.server: 79 | self.server.close() 80 | await self.server.wait_closed() 81 | await self.app.shutdown() 82 | if self.handler: 83 | await self.handler.shutdown(10) 84 | await self.app.cleanup() -------------------------------------------------------------------------------- /ledfxcontroller/frontend/js/graph.js: -------------------------------------------------------------------------------- 1 | Chart.defaults.global.defaultFontFamily = '-apple-system,system-ui,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif'; 2 | Chart.defaults.global.defaultFontColor = '#292b2c'; 3 | 4 | // Create a RGB visualization graph on the given canvas 5 | 6 | class RGBVisualizationGraph { 7 | constructor(canvasId) { 8 | var ctx = document.getElementById(canvasId); 9 | this.graph = new Chart(ctx, { 10 | type: 'line', 11 | data: { 12 | labels: [], 13 | datasets: [ 14 | { 15 | label: "Red", 16 | lineTension: 0.1, 17 | backgroundColor: "rgba(255,0,0,0.1)", 18 | borderColor: "rgba(255,0,0,1)", 19 | pointRadius: 0, 20 | data: [], 21 | }, 22 | { 23 | label: "Green", 24 | lineTension: 0.1, 25 | backgroundColor: "rgba(0,255,0,0.1)", 26 | borderColor: "rgba(0,255,0,1)", 27 | pointRadius: 0, 28 | data: [], 29 | }, 30 | { 31 | label: "Blue", 32 | lineTension: 0.1, 33 | backgroundColor: "rgba(0,0,255,0.1)", 34 | borderColor: "rgba(0,0,255,1)", 35 | pointRadius: 0, 36 | data: [], 37 | }], 38 | }, 39 | options: { 40 | responsive: true, 41 | maintainAspectRatio: false, 42 | tooltips: {enabled: false}, 43 | hover: {mode: null}, 44 | animation: { 45 | duration: 0, 46 | }, 47 | hover: { 48 | animationDuration: 0, 49 | }, 50 | responsiveAnimationDuration: 0, 51 | scales: { 52 | xAxes: [{ 53 | gridLines: { 54 | display: false 55 | }, 56 | ticks: { 57 | maxTicksLimit: 7 58 | } 59 | }], 60 | yAxes: [{ 61 | ticks: { 62 | min: 0, 63 | max: 256, 64 | stepSize: 64 65 | }, 66 | gridLines: { 67 | color: "rgba(0, 0, 0, .125)", 68 | } 69 | }], 70 | }, 71 | legend: { 72 | display: false 73 | } 74 | } 75 | }); 76 | } 77 | 78 | update(data) { 79 | this.graph.data.labels = data.rgb_x 80 | this.graph.data.datasets[0].data = data.r 81 | this.graph.data.datasets[1].data = data.g 82 | this.graph.data.datasets[2].data = data.b 83 | this.graph.update(0) 84 | } 85 | 86 | setPixelMax(pixelMax) { 87 | this.graph.config.options.scales.yAxes[0].ticks.max = pixelMax 88 | } 89 | } -------------------------------------------------------------------------------- /ledfxcontroller/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 ledfxcontroller.utils import async_fire_and_forget 11 | from ledfxcontroller.http import LedFxControllerHTTP 12 | from ledfxcontroller.devices import Devices 13 | from ledfxcontroller.effects import Effects 14 | from ledfxcontroller.config import load_config, save_config 15 | 16 | _LOGGER = logging.getLogger(__name__) 17 | 18 | class LedFxController(object): 19 | 20 | def __init__(self, config_dir): 21 | self.config_dir = config_dir 22 | self.config = load_config(config_dir) 23 | 24 | if sys.platform == 'win32': 25 | self.loop = asyncio.ProactorEventLoop() 26 | else: 27 | self.loop = asyncio.get_event_loop() 28 | executor_opts = {'max_workers': self.config.get('max_workers')} 29 | 30 | self.executor = ThreadPoolExecutor(**executor_opts) 31 | self.loop.set_default_executor(self.executor) 32 | self.loop.set_exception_handler(self.loop_exception_handler) 33 | 34 | self.http = LedFxControllerHTTP(ledfx = self, 35 | host = self.config['host'], 36 | port = self.config['port']) 37 | self.exit_code = None 38 | 39 | 40 | def loop_exception_handler(self, loop, context): 41 | kwargs = {} 42 | exception = context.get('exception') 43 | if exception: 44 | kwargs['exc_info'] = (type(exception), exception, 45 | exception.__traceback__) 46 | 47 | _LOGGER.error('Exception in core event loop: {}'.format( 48 | context['message']), **kwargs) 49 | 50 | async def flush_loop(self): 51 | await asyncio.sleep(0, loop=self.loop) 52 | 53 | def start(self, open_ui = False): 54 | async_fire_and_forget( 55 | self.async_start(open_ui = open_ui), self.loop) 56 | 57 | try: 58 | self.loop.run_forever() 59 | except KeyboardInterrupt: 60 | self.loop.call_soon_threadsafe( 61 | self.loop.create_task, self.async_stop()) 62 | self.loop.run_forever() 63 | finally: 64 | self.loop.close() 65 | return self.exit_code 66 | 67 | async def async_start(self, open_ui = False): 68 | _LOGGER.info("Starting LedFxController") 69 | await self.http.start() 70 | 71 | self.devices = Devices(self) 72 | self.devices.create_from_config(self.config['devices']) 73 | self.effects = Effects(self) 74 | 75 | if open_ui: 76 | import webbrowser 77 | webbrowser.open(self.http.base_url) 78 | 79 | await self.flush_loop() 80 | 81 | def stop(self, exit_code=0): 82 | async_fire_and_forget(self.async_stop(exit_code), self.loop) 83 | 84 | async def async_stop(self, exit_code=0): 85 | print('Stopping LedFxController.') 86 | await self.http.stop() 87 | self.devices.clear_all_effects() 88 | 89 | # Save the configuration before shutting down 90 | save_config(config = self.config, config_dir = self.config_dir) 91 | 92 | await self.flush_loop() 93 | self.executor.shutdown() 94 | self.exit_code = exit_code 95 | self.loop.stop() -------------------------------------------------------------------------------- /ledfxcontroller/effects/gradient.py: -------------------------------------------------------------------------------- 1 | from ledfxcontroller.effects.temporal import TemporalEffect 2 | from ledfxcontroller.color import COLORS, GRADIENTS 3 | from ledfxcontroller.effects import Effect 4 | from scipy.misc import comb 5 | import voluptuous as vol 6 | import numpy as np 7 | import logging 8 | 9 | _LOGGER = logging.getLogger(__name__) 10 | 11 | @Effect.no_registration 12 | class GradientEffect(Effect): 13 | """ 14 | Simple effect base class that supplies gradient functionality. This 15 | is intended for effect which instead of outputing exact colors output 16 | colors based upon some configured color pallet. 17 | """ 18 | 19 | CONFIG_SCHEMA = vol.Schema({ 20 | vol.Required('gradient', default = 'Spectral'): vol.Any(str, list), 21 | vol.Required('gradient_flip', default = False): bool, 22 | vol.Required('gradient_roll', default = 0): int, 23 | }) 24 | 25 | _gradient_curve = None 26 | 27 | def _bernstein_poly(self, i, n, t): 28 | """The Bernstein polynomial of n, i as a function of t""" 29 | return comb(n, i) * ( t**(n-i) ) * (1 - t)**i 30 | 31 | def _generate_bezier_curve(self, gradient_colors, gradient_length): 32 | 33 | # Check to see if we have a custom gradient, or a predefined one and 34 | # load the colors accordingly 35 | if isinstance(gradient_colors, str): 36 | gradient_colors = GRADIENTS[gradient_colors.lower()] 37 | 38 | rgb_list = np.array([COLORS[color.lower()] for color in gradient_colors]).T 39 | n_colors = len(rgb_list[0]) 40 | 41 | t = np.linspace(0.0, 1.0, gradient_length) 42 | polynomial_array = np.array([self._bernstein_poly(i, n_colors-1, t) for i in range(0, n_colors)]) 43 | gradient = np.array([np.dot(rgb_list[0], polynomial_array), 44 | np.dot(rgb_list[1], polynomial_array), 45 | np.dot(rgb_list[2], polynomial_array)]) 46 | 47 | _LOGGER.info(('Generating new gradient curve for {}'.format(gradient_colors))) 48 | self._gradient_curve = gradient 49 | 50 | def _gradient_valid(self): 51 | if self._gradient_curve is None: 52 | return False # Uninitialized gradient 53 | if len(self._gradient_curve[0]) != self.pixel_count: 54 | return False # Incorrect size 55 | return True 56 | 57 | def _validate_gradient(self): 58 | if not self._gradient_valid(): 59 | self._generate_bezier_curve(self._config['gradient'], self.pixel_count) 60 | 61 | def _roll_gradient(self): 62 | if self._config['gradient_roll'] == 0: 63 | return 64 | 65 | self._gradient_curve = np.roll( 66 | self._gradient_curve, 67 | self._config['gradient_roll'], 68 | axis=1) 69 | 70 | def config_updated(self, config): 71 | """Invalidate the gradient""" 72 | self._gradient_curve = None 73 | 74 | def apply_gradient(self, y): 75 | self._validate_gradient() 76 | 77 | # Apply and roll the gradient if necessary 78 | flip_index = -1 if self._config['gradient_flip'] else 1 79 | output = (self._gradient_curve[:][::flip_index]*y).T 80 | self._roll_gradient() 81 | 82 | return output 83 | 84 | 85 | class TemporalGradientEffect(TemporalEffect, GradientEffect): 86 | """ 87 | A simple effect that just applies a gradient to the channel. This 88 | is essentually just the temporal exposure of gradients. 89 | """ 90 | 91 | NAME = "Gradient" 92 | 93 | def effect_loop(self): 94 | # TODO: Could add some cool effects like twinkle or sin modulation 95 | # of the gradient. 96 | self.pixels = self.apply_gradient(1) -------------------------------------------------------------------------------- /ledfxcontroller/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 | DEFAULT_CONFIG = """ 13 | # Webserver setup 14 | host: 127.0.0.1 15 | port: 8888 16 | 17 | # Device setup 18 | # devices: 19 | # test_device_1: 20 | # type: e131 21 | # name: Test Device 22 | # host: 192.168.1.100 23 | # pixel_count: 100 24 | """ 25 | 26 | CORE_CONFIG_SCHEMA = vol.Schema({ 27 | vol.Required('host'): str, 28 | vol.Required('port'): int, 29 | vol.Optional('max_workers'): int, 30 | vol.Optional('devices', default = {}): dict 31 | }) 32 | 33 | def get_default_config_directory() -> str: 34 | """Get the default configuration directory""" 35 | 36 | base_dir = os.getenv('APPDATA') if os.name == "nt" \ 37 | else os.path.expanduser('~') 38 | return os.path.join(base_dir, CONFIG_DIRECTORY) 39 | 40 | def get_config_file(config_dir: str) -> str: 41 | """Finds a supported configuration fill in the provided directory""" 42 | 43 | config_path = os.path.join(config_dir, CONFIG_FILE_NAME) 44 | return config_path if os.path.isfile(config_path) else None 45 | 46 | def create_default_config(config_dir: str) -> str: 47 | """Creates a default configuration in the provided directory""" 48 | 49 | config_path = os.path.join(config_dir, CONFIG_FILE_NAME) 50 | try: 51 | with open(config_path, 'wt') as config_file: 52 | config_file.write(DEFAULT_CONFIG) 53 | return config_path 54 | 55 | except IOError: 56 | print(('Unable to create default configuration file {}').format(config_path)) 57 | return None 58 | 59 | def ensure_config_file(config_dir: str) -> str: 60 | """Checks if a config file exsit, and otherwise creates one""" 61 | 62 | ensure_config_directory(config_dir) 63 | config_path = get_config_file(config_dir) 64 | if config_path is None: 65 | config_path = create_default_config(config_dir) 66 | print(('Failed to find configuration file. Creating default configuration ' 67 | 'file in {}').format(config_path)) 68 | 69 | return config_path 70 | 71 | def ensure_config_directory(config_dir: str) -> None: 72 | """Validate that the config directory is valid.""" 73 | 74 | # If an explict path is provided simply check if it exist and failfast 75 | # if it doesn't. Otherwise, if we have the default directory attempt to 76 | # create the file 77 | if not os.path.isdir(config_dir): 78 | if config_dir != get_default_config_directory(): 79 | print(('Error: Invalid configuration directory {}').format(config_dir)) 80 | sys.exit(1) 81 | 82 | try: 83 | os.mkdir(config_dir) 84 | except OSError: 85 | print(('Error: Unable to create configuration directory {}').format(config_dir)) 86 | sys.exit(1) 87 | 88 | def load_config(config_dir: str) -> dict: 89 | """Validates and loads the configuration file in the provided directory""" 90 | 91 | config_file = ensure_config_file(config_dir) 92 | print(('Loading configuration file from {}').format(config_dir)) 93 | with open(config_file, 'rt') as file: 94 | return CORE_CONFIG_SCHEMA(yaml.load(file)) 95 | 96 | def save_config(config: dict, config_dir: str) -> None: 97 | """Saves the configuration to the provided directory""" 98 | 99 | config_file = ensure_config_file(config_dir) 100 | _LOGGER.info(('Saving configuration file to {}').format(config_dir)) 101 | with open(config_file, 'w') as file: 102 | yaml.dump(config, file, default_flow_style=False) 103 | -------------------------------------------------------------------------------- /ledfxcontroller/frontend/external/bootstrap/css/bootstrap-reboot.min.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap Reboot v4.0.0-beta.2 (https://getbootstrap.com) 3 | * Copyright 2011-2017 The Bootstrap Authors 4 | * Copyright 2011-2017 Twitter, Inc. 5 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) 6 | * Forked from Normalize.css, licensed MIT (https://github.com/necolas/normalize.css/blob/master/LICENSE.md) 7 | */*,::after,::before{box-sizing:border-box}html{font-family:sans-serif;line-height:1.15;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%;-ms-overflow-style:scrollbar;-webkit-tap-highlight-color:transparent}@-ms-viewport{width:device-width}article,aside,dialog,figcaption,figure,footer,header,hgroup,main,nav,section{display:block}body{margin:0;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol";font-size:1rem;font-weight:400;line-height:1.5;color:#212529;text-align:left;background-color:#fff}[tabindex="-1"]:focus{outline:0!important}hr{box-sizing:content-box;height:0;overflow:visible}h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:.5rem}p{margin-top:0;margin-bottom:1rem}abbr[data-original-title],abbr[title]{text-decoration:underline;-webkit-text-decoration:underline dotted;text-decoration:underline dotted;cursor:help;border-bottom:0}address{margin-bottom:1rem;font-style:normal;line-height:inherit}dl,ol,ul{margin-top:0;margin-bottom:1rem}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}dt{font-weight:700}dd{margin-bottom:.5rem;margin-left:0}blockquote{margin:0 0 1rem}dfn{font-style:italic}b,strong{font-weight:bolder}small{font-size:80%}sub,sup{position:relative;font-size:75%;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}a{color:#007bff;text-decoration:none;background-color:transparent;-webkit-text-decoration-skip:objects}a:hover{color:#0056b3;text-decoration:underline}a:not([href]):not([tabindex]){color:inherit;text-decoration:none}a:not([href]):not([tabindex]):focus,a:not([href]):not([tabindex]):hover{color:inherit;text-decoration:none}a:not([href]):not([tabindex]):focus{outline:0}code,kbd,pre,samp{font-family:monospace,monospace;font-size:1em}pre{margin-top:0;margin-bottom:1rem;overflow:auto;-ms-overflow-style:scrollbar}figure{margin:0 0 1rem}img{vertical-align:middle;border-style:none}svg:not(:root){overflow:hidden}[role=button],a,area,button,input:not([type=range]),label,select,summary,textarea{-ms-touch-action:manipulation;touch-action:manipulation}table{border-collapse:collapse}caption{padding-top:.75rem;padding-bottom:.75rem;color:#868e96;text-align:left;caption-side:bottom}th{text-align:inherit}label{display:inline-block;margin-bottom:.5rem}button{border-radius:0}button:focus{outline:1px dotted;outline:5px auto -webkit-focus-ring-color}button,input,optgroup,select,textarea{margin:0;font-family:inherit;font-size:inherit;line-height:inherit}button,input{overflow:visible}button,select{text-transform:none}[type=reset],[type=submit],button,html [type=button]{-webkit-appearance:button}[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner,button::-moz-focus-inner{padding:0;border-style:none}input[type=checkbox],input[type=radio]{box-sizing:border-box;padding:0}input[type=date],input[type=datetime-local],input[type=month],input[type=time]{-webkit-appearance:listbox}textarea{overflow:auto;resize:vertical}fieldset{min-width:0;padding:0;margin:0;border:0}legend{display:block;width:100%;max-width:100%;padding:0;margin-bottom:.5rem;font-size:1.5rem;line-height:inherit;color:inherit;white-space:normal}progress{vertical-align:baseline}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{outline-offset:-2px;-webkit-appearance:none}[type=search]::-webkit-search-cancel-button,[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}output{display:inline-block}summary{display:list-item}template{display:none}[hidden]{display:none!important} 8 | /*# sourceMappingURL=bootstrap-reboot.min.css.map */ -------------------------------------------------------------------------------- /tests/test_ledfx.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | import time 5 | import pytest 6 | import numpy as np 7 | from ledfxcontroller.devices import DeviceManager 8 | from ledfxcontroller.effects.rainbow import RainbowEffect 9 | from ledfxcontroller.effects.spectrum import SpectrumAudioEffect 10 | from ledfxcontroller.effects.wavelength import WavelengthAudioEffect 11 | from ledfxcontroller.effects.gradient import TemporalGradientEffect 12 | from ledfxcontroller.effects import Effect, EffectManager 13 | 14 | # TODO: Cleanup test as they are not 100% functional yet 15 | 16 | # BASIC_E131_CONFIG = { 17 | # "name": "Test E131 Device", 18 | # "e131": 19 | # { 20 | # "host": "192.168.1.185", 21 | # "channel_count": 96 22 | # } 23 | # } 24 | 25 | BASIC_E131_CONFIG = { 26 | "name": "Test E131 Device", 27 | "e131": 28 | { 29 | "host": "192.168.1.183", 30 | "channel_count": 900 31 | } 32 | } 33 | 34 | # def test_device_creation(): 35 | # deviceManager = DeviceManager() 36 | 37 | # device = deviceManager.createDevice(BASIC_E131_CONFIG) 38 | # assert device is not None 39 | 40 | # def test_device_channel(): 41 | # deviceManager = DeviceManager() 42 | # device = deviceManager.createDevice(BASIC_E131_CONFIG) 43 | 44 | # assert device.outputChannels[0].pixelCount == 32 45 | # assert len(device.outputChannels[0].pixels) == 32 46 | 47 | # # Validate setting the pixels as a single tuple 48 | # device.outputChannels[0].pixels = (255, 0, 0) 49 | # for pixel in range(0, device.outputChannels[0].pixelCount): 50 | # assert (device.outputChannels[0].pixels[pixel] == (255, 0, 0)).all() 51 | 52 | # # Validate the output channel gets assembled into the frame 53 | # frame = device.assembleFrame() 54 | # for pixel in range(0, device.outputChannels[0].pixelCount): 55 | # assert (frame[pixel] == (255, 0, 0)).all() 56 | 57 | # # Validate setting the pixels as a numpy array of equal size 58 | # device.outputChannels[0].pixels = np.zeros((device.outputChannels[0].pixelCount, 3)) 59 | # for pixel in range(0, device.outputChannels[0].pixelCount): 60 | # assert (device.outputChannels[0].pixels[pixel] == (0, 0, 0)).all() 61 | 62 | # # Validate the output channel gets assembled into the frame 63 | # frame = device.assembleFrame() 64 | # for pixel in range(0, device.outputChannels[0].pixelCount): 65 | # assert (frame[pixel] == (0, 0, 0)).all() 66 | 67 | # def test_effect_rainbow(): 68 | # deviceManager = DeviceManager() 69 | # device = deviceManager.createDevice(BASIC_E131_CONFIG) 70 | # assert device is not None 71 | 72 | # effectManager = EffectManager() 73 | # effect = effectManager.createEffect("Rainbow") 74 | # assert effect is not None 75 | 76 | # device.activate() 77 | # effect.activate(device.outputChannels[0]) 78 | 79 | # time.sleep(5) # Default 80 | # effect.updateConfig({'speed': 3.0}) 81 | # time.sleep(5) # Default w/ 3x speed 82 | # effect.updateConfig({'frequency': 3.0}) 83 | # time.sleep(5) # Default w/ 3x frequency 84 | 85 | # effect.deactivate() 86 | # device.deactivate() 87 | 88 | # def test_effect_gradient_shift(): 89 | # deviceManager = DeviceManager() 90 | # device = deviceManager.createDevice(BASIC_E131_CONFIG) 91 | # assert device is not None 92 | 93 | # effectManager = EffectManager() 94 | # effect = effectManager.createEffect("Gradient", { "gradient": "Dancefloor"}) 95 | # assert effect is not None 96 | 97 | # device.activate() 98 | # effect.activate(device.outputChannels[0]) 99 | 100 | # time.sleep(5) 101 | 102 | # effect.deactivate() 103 | # device.deactivate() 104 | 105 | # assert False 106 | 107 | # def test_effect_spectrum(): 108 | # deviceManager = DeviceManager() 109 | # device = deviceManager.createDevice(BASIC_E131_CONFIG) 110 | # assert device is not None 111 | 112 | # effectManager = EffectManager() 113 | # effect = effectManager.createEffect("Spectrum") 114 | # assert effect is not None 115 | 116 | # device.activate() 117 | # effect.activate(device.outputChannels[0]) 118 | 119 | # time.sleep(20) 120 | 121 | # effect.deactivate() 122 | # device.deactivate() 123 | 124 | # assert False 125 | -------------------------------------------------------------------------------- /ledfxcontroller/devices/e131.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from ledfxcontroller.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 | class E131Device(Device): 11 | """E1.31 device support""" 12 | 13 | CONFIG_SCHEMA = vol.Schema({ 14 | vol.Required('host'): str, 15 | vol.Required('universe', default=1): int, 16 | vol.Required('universe_size', default=512): int, 17 | vol.Required('channel_offset', default=1): int, 18 | vol.Required(vol.Any('pixel_count', 'channel_count')): vol.Coerce(int) 19 | }) 20 | 21 | def __init__(self, config): 22 | self._config = config 23 | 24 | # Allow for configuring in terms of "pixels" or "channels" 25 | if 'pixel_count' in self._config: 26 | self._config['channel_count'] = self._config['pixel_count'] * 3 27 | else: 28 | self._config['pixel_count'] = self._config['channel_count'] // 3 29 | 30 | span = self._config['channel_offset'] + self._config['channel_count'] - 1 31 | self._config['universe_end'] = self._config['universe'] + int(span / self._config['universe_size']) 32 | if span % self._config['universe_size'] == 0: 33 | self._config['universe_end'] -= 1 34 | 35 | self._sacn = None 36 | 37 | @property 38 | def pixel_count(self): 39 | return int(self._config['pixel_count']) 40 | 41 | def activate(self): 42 | if self._sacn: 43 | raise Exception('sACN sender already started.') 44 | 45 | # Configure sACN and start the dedicated thread to flush the buffer 46 | self._sacn = sacn.sACNsender() 47 | for universe in range(self._config['universe'], self._config['universe_end'] + 1): 48 | _LOGGER.info("sACN activating universe {}".format(universe)) 49 | self._sacn.activate_output(universe) 50 | if (self._config['host'] == None): 51 | self._sacn[universe].multicast = True 52 | else: 53 | self._sacn[universe].destination = self._config['host'] 54 | self._sacn[universe].multicast = False 55 | #self._sacn.fps = 60 56 | self._sacn.start() 57 | 58 | _LOGGER.info("sACN sender started.") 59 | super().activate() 60 | 61 | def deactivate(self): 62 | super().deactivate() 63 | 64 | if not self._sacn: 65 | raise Exception('sACN sender not started.') 66 | 67 | # Turn off all the LEDs when deactivating. With how the sender 68 | # works currently we need to sleep to ensure the pixels actually 69 | # get updated. Need to replace the sACN sender such that flush 70 | # directly writes the pixels. 71 | self.flush(np.zeros(self._config['channel_count'])) 72 | time.sleep(1.5) 73 | 74 | self._sacn.stop() 75 | self._sacn = None 76 | _LOGGER.info("sACN sender stopped.") 77 | 78 | 79 | def flush(self, data): 80 | """Flush the data to all the E1.31 channels account for spanning universes""" 81 | 82 | if not self._sacn: 83 | raise Exception('sACN sender not started.') 84 | if data.size != self._config['channel_count']: 85 | raise Exception('Invalid buffer size.') 86 | 87 | data = data.flatten() 88 | current_index = 0 89 | for universe in range(self._config['universe'], self._config['universe_end'] + 1): 90 | # Calculate offset into the provide input buffer for the channel. There are some 91 | # cleaner ways this can be done... This is just the quick and dirty 92 | universe_start = (universe - self._config['universe']) * self._config['universe_size'] 93 | universe_end = (universe - self._config['universe'] + 1) * self._config['universe_size'] 94 | 95 | dmx_start = max(universe_start, self._config['channel_offset']) % self._config['universe_size'] 96 | dmx_end = min(universe_end, self._config['channel_offset'] + self._config['channel_count']) % self._config['universe_size'] 97 | if dmx_end == 0: 98 | dmx_end = self._config['universe_size'] 99 | 100 | input_start = current_index 101 | input_end = current_index + dmx_end - dmx_start 102 | current_index = input_end 103 | 104 | dmx_data = np.array(self._sacn[universe].dmx_data) 105 | dmx_data[dmx_start:dmx_end] = data[input_start:input_end] 106 | self._sacn[universe].dmx_data = dmx_data -------------------------------------------------------------------------------- /ledfxcontroller/frontend/base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | LedFx 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 62 |
63 |
64 | 65 | 73 |
74 |
75 |

{{title}}

76 | {% block content %} 77 | 80 | {% endblock %} 81 |
82 |
83 |
84 | 85 |
86 |
87 |
88 | Copyright © Austin Hodges 2018 89 |
90 |
91 |
92 | 93 | 94 | 95 | 96 | 97 | {% block javascript %} 98 | {% endblock %} 99 |
100 | 101 | 102 | 103 | -------------------------------------------------------------------------------- /ledfxcontroller/frontend/external/jquery-easing/jquery.easing.js: -------------------------------------------------------------------------------- 1 | /* 2 | * jQuery Easing v1.4.1 - http://gsgd.co.uk/sandbox/jquery/easing/ 3 | * Open source under the BSD License. 4 | * Copyright © 2008 George McGinley Smith 5 | * All rights reserved. 6 | * https://raw.github.com/gdsmith/jquery-easing/master/LICENSE 7 | */ 8 | 9 | (function (factory) { 10 | if (typeof define === "function" && define.amd) { 11 | define(['jquery'], function ($) { 12 | return factory($); 13 | }); 14 | } else if (typeof module === "object" && typeof module.exports === "object") { 15 | exports = factory(require('jquery')); 16 | } else { 17 | factory(jQuery); 18 | } 19 | })(function($){ 20 | 21 | // Preserve the original jQuery "swing" easing as "jswing" 22 | $.easing.jswing = $.easing.swing; 23 | 24 | var pow = Math.pow, 25 | sqrt = Math.sqrt, 26 | sin = Math.sin, 27 | cos = Math.cos, 28 | PI = Math.PI, 29 | c1 = 1.70158, 30 | c2 = c1 * 1.525, 31 | c3 = c1 + 1, 32 | c4 = ( 2 * PI ) / 3, 33 | c5 = ( 2 * PI ) / 4.5; 34 | 35 | // x is the fraction of animation progress, in the range 0..1 36 | function bounceOut(x) { 37 | var n1 = 7.5625, 38 | d1 = 2.75; 39 | if ( x < 1/d1 ) { 40 | return n1*x*x; 41 | } else if ( x < 2/d1 ) { 42 | return n1*(x-=(1.5/d1))*x + 0.75; 43 | } else if ( x < 2.5/d1 ) { 44 | return n1*(x-=(2.25/d1))*x + 0.9375; 45 | } else { 46 | return n1*(x-=(2.625/d1))*x + 0.984375; 47 | } 48 | } 49 | 50 | $.extend( $.easing, 51 | { 52 | def: 'easeOutQuad', 53 | swing: function (x) { 54 | return $.easing[$.easing.def](x); 55 | }, 56 | easeInQuad: function (x) { 57 | return x * x; 58 | }, 59 | easeOutQuad: function (x) { 60 | return 1 - ( 1 - x ) * ( 1 - x ); 61 | }, 62 | easeInOutQuad: function (x) { 63 | return x < 0.5 ? 64 | 2 * x * x : 65 | 1 - pow( -2 * x + 2, 2 ) / 2; 66 | }, 67 | easeInCubic: function (x) { 68 | return x * x * x; 69 | }, 70 | easeOutCubic: function (x) { 71 | return 1 - pow( 1 - x, 3 ); 72 | }, 73 | easeInOutCubic: function (x) { 74 | return x < 0.5 ? 75 | 4 * x * x * x : 76 | 1 - pow( -2 * x + 2, 3 ) / 2; 77 | }, 78 | easeInQuart: function (x) { 79 | return x * x * x * x; 80 | }, 81 | easeOutQuart: function (x) { 82 | return 1 - pow( 1 - x, 4 ); 83 | }, 84 | easeInOutQuart: function (x) { 85 | return x < 0.5 ? 86 | 8 * x * x * x * x : 87 | 1 - pow( -2 * x + 2, 4 ) / 2; 88 | }, 89 | easeInQuint: function (x) { 90 | return x * x * x * x * x; 91 | }, 92 | easeOutQuint: function (x) { 93 | return 1 - pow( 1 - x, 5 ); 94 | }, 95 | easeInOutQuint: function (x) { 96 | return x < 0.5 ? 97 | 16 * x * x * x * x * x : 98 | 1 - pow( -2 * x + 2, 5 ) / 2; 99 | }, 100 | easeInSine: function (x) { 101 | return 1 - cos( x * PI/2 ); 102 | }, 103 | easeOutSine: function (x) { 104 | return sin( x * PI/2 ); 105 | }, 106 | easeInOutSine: function (x) { 107 | return -( cos( PI * x ) - 1 ) / 2; 108 | }, 109 | easeInExpo: function (x) { 110 | return x === 0 ? 0 : pow( 2, 10 * x - 10 ); 111 | }, 112 | easeOutExpo: function (x) { 113 | return x === 1 ? 1 : 1 - pow( 2, -10 * x ); 114 | }, 115 | easeInOutExpo: function (x) { 116 | return x === 0 ? 0 : x === 1 ? 1 : x < 0.5 ? 117 | pow( 2, 20 * x - 10 ) / 2 : 118 | ( 2 - pow( 2, -20 * x + 10 ) ) / 2; 119 | }, 120 | easeInCirc: function (x) { 121 | return 1 - sqrt( 1 - pow( x, 2 ) ); 122 | }, 123 | easeOutCirc: function (x) { 124 | return sqrt( 1 - pow( x - 1, 2 ) ); 125 | }, 126 | easeInOutCirc: function (x) { 127 | return x < 0.5 ? 128 | ( 1 - sqrt( 1 - pow( 2 * x, 2 ) ) ) / 2 : 129 | ( sqrt( 1 - pow( -2 * x + 2, 2 ) ) + 1 ) / 2; 130 | }, 131 | easeInElastic: function (x) { 132 | return x === 0 ? 0 : x === 1 ? 1 : 133 | -pow( 2, 10 * x - 10 ) * sin( ( x * 10 - 10.75 ) * c4 ); 134 | }, 135 | easeOutElastic: function (x) { 136 | return x === 0 ? 0 : x === 1 ? 1 : 137 | pow( 2, -10 * x ) * sin( ( x * 10 - 0.75 ) * c4 ) + 1; 138 | }, 139 | easeInOutElastic: function (x) { 140 | return x === 0 ? 0 : x === 1 ? 1 : x < 0.5 ? 141 | -( pow( 2, 20 * x - 10 ) * sin( ( 20 * x - 11.125 ) * c5 )) / 2 : 142 | pow( 2, -20 * x + 10 ) * sin( ( 20 * x - 11.125 ) * c5 ) / 2 + 1; 143 | }, 144 | easeInBack: function (x) { 145 | return c3 * x * x * x - c1 * x * x; 146 | }, 147 | easeOutBack: function (x) { 148 | return 1 + c3 * pow( x - 1, 3 ) + c1 * pow( x - 1, 2 ); 149 | }, 150 | easeInOutBack: function (x) { 151 | return x < 0.5 ? 152 | ( pow( 2 * x, 2 ) * ( ( c2 + 1 ) * 2 * x - c2 ) ) / 2 : 153 | ( pow( 2 * x - 2, 2 ) *( ( c2 + 1 ) * ( x * 2 - 2 ) + c2 ) + 2 ) / 2; 154 | }, 155 | easeInBounce: function (x) { 156 | return 1 - bounceOut( 1 - x ); 157 | }, 158 | easeOutBounce: bounceOut, 159 | easeInOutBounce: function (x) { 160 | return x < 0.5 ? 161 | ( 1 - bounceOut( 1 - 2 * x ) ) / 2 : 162 | ( 1 + bounceOut( 2 * x - 1 ) ) / 2; 163 | } 164 | }); 165 | 166 | }); 167 | -------------------------------------------------------------------------------- /ledfxcontroller/frontend/device.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% set title = device.name %} 4 | {% block javascript %} 5 | 6 | 7 | 8 | 9 | 66 | {% endblock %} 67 | 68 | {% block breadcrumbs %} 69 | 70 | {% endblock %} 71 | 72 | {% block content %} 73 | {% if error_message %}

{{ error_message }}

{% endif %} 74 | 75 | 76 |
77 |
78 | Effect Control
79 |
80 | 81 |
82 |

83 | 84 | 90 |

91 |

92 | 93 | 94 |

95 | 96 |
97 | 98 |
99 |
100 | 101 | 102 |
103 |
104 | 105 | RGB Values 106 | 107 | 108 | 109 | 110 |
111 |
112 | 113 |
114 | 115 |
116 | 117 | 118 | {% endblock %} -------------------------------------------------------------------------------- /ledfxcontroller/effects/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | from ledfxcontroller.utils import BaseRegistry, RegistryLoader 3 | from scipy.ndimage.filters import gaussian_filter1d 4 | import voluptuous as vol 5 | import numpy as np 6 | import importlib 7 | import colorsys 8 | import pkgutil 9 | import logging 10 | import sys 11 | import os 12 | 13 | _LOGGER = logging.getLogger(__name__) 14 | 15 | def fill_solid(pixels, color): 16 | pixels[:,] = color 17 | 18 | def fill_rainbow(pixels, initial_hue, delta_hue): 19 | hue = initial_hue 20 | sat = 0.95 21 | val = 1.0 22 | for i in range(0,len(pixels)): 23 | pixels[i,:] = tuple(int(i * 255) for i in colorsys.hsv_to_rgb(hue, sat, val)) 24 | hue = hue + delta_hue 25 | return pixels 26 | 27 | def mirror_pixels(pixels): 28 | return np.concatenate((pixels[::-2,:], pixels[::2,:]), axis=0) 29 | 30 | def flip_pixels(pixels): 31 | return np.flipud(pixels) 32 | 33 | def blur_pixels(pixels, sigma): 34 | return gaussian_filter1d(pixels, axis=0, sigma=sigma) 35 | 36 | @BaseRegistry.no_registration 37 | class Effect(BaseRegistry): 38 | """ 39 | Manages an effect 40 | """ 41 | NAME = "" 42 | _pixels = None 43 | _dirty = False 44 | _config = None 45 | _active = False 46 | 47 | # Basic effect properties that can be applied to all effects 48 | CONFIG_SCHEMA = vol.Schema({ 49 | vol.Optional('blur', default = 0.0): vol.Coerce(float), 50 | vol.Optional('flip', default = False): bool, 51 | vol.Optional('mirror', default = False): bool, 52 | }) 53 | 54 | def __init__(self, config): 55 | self.update_config(config) 56 | 57 | def __del__(self): 58 | if self._active: 59 | self.deactivate() 60 | 61 | def activate(self, pixel_count): 62 | """Attaches an output channel to the effect""" 63 | self._pixels = np.zeros((pixel_count, 3)) 64 | self._active = True 65 | 66 | _LOGGER.info("Effect {} activated.".format(self.NAME)) 67 | 68 | def deactivate(self): 69 | """Detaches an output channel from the effect""" 70 | self._pixels = None 71 | self._active = False 72 | 73 | _LOGGER.info("Effect {} deactivated.".format(self.NAME)) 74 | 75 | def update_config(self, config): 76 | # TODO: Sync locks to ensure everything is thread safe 77 | validated_config = type(self).schema()(config) 78 | self._config = validated_config 79 | 80 | def inherited(cls, method): 81 | if hasattr(cls, method) and hasattr(super(cls, cls), method): 82 | return cls.foo == super(cls).foo 83 | return False 84 | 85 | # Iterate all the base classes and check to see if there is a custom 86 | # implementation of config updates. If to notify the base class. 87 | valid_classes = list(type(self).__bases__) 88 | valid_classes.append(type(self)) 89 | for base in valid_classes: 90 | if base.config_updated != super(base, base).config_updated: 91 | base.config_updated(self, self._config) 92 | 93 | _LOGGER.info("Effect {} config updated to {}.".format( 94 | self.NAME, validated_config)) 95 | 96 | def config_updated(self, config): 97 | """ 98 | Optional event for when an effect's config is updated. This 99 | shold be used by the subclass only if they need to build up 100 | complex properties off the configuration, otherwise the config 101 | should just be referenced in the effect's loop directly 102 | """ 103 | pass 104 | 105 | @property 106 | def is_active(self): 107 | """Return if the effect is currently active""" 108 | return self._active 109 | 110 | @property 111 | def pixels(self): 112 | """Returns the pixels for the channel""" 113 | if not self._active: 114 | raise Exception('Attempting to access pixels before effect is active') 115 | 116 | return self._pixels 117 | 118 | @pixels.setter 119 | def pixels(self, pixels): 120 | """Sets the pixels for the channel""" 121 | if not self._active: 122 | raise Exception('Attempting to set pixels before effect is active') 123 | 124 | if isinstance(pixels, tuple): 125 | self._pixels = pixels 126 | elif isinstance(pixels, np.ndarray): 127 | 128 | # Apply some of the base output filters if necessary 129 | if self._config['blur'] != 0.0: 130 | pixels = blur_pixels(pixels=pixels, sigma=self._config['blur']) 131 | if self._config['flip']: 132 | pixels = flip_pixels(pixels) 133 | if self._config['mirror']: 134 | pixels = mirror_pixels(pixels) 135 | self._pixels = pixels 136 | else: 137 | raise TypeError() 138 | 139 | self._dirty = True 140 | 141 | @property 142 | def pixel_count(self): 143 | """Returns the number of pixels for the channel""" 144 | return len(self.pixels) 145 | 146 | @property 147 | def name(self): 148 | return self.NAME 149 | 150 | class Effects(RegistryLoader): 151 | """Thin wrapper around the effect registry that manages effects""" 152 | 153 | PACKAGE_NAME = 'ledfxcontroller.effects' 154 | 155 | def __init__(self, ledfx): 156 | super().__init__(Effect, self.PACKAGE_NAME, ledfx) -------------------------------------------------------------------------------- /ledfxcontroller/frontend/external/datatables/dataTables.bootstrap4.js: -------------------------------------------------------------------------------- 1 | /*! DataTables Bootstrap 3 integration 2 | * ©2011-2015 SpryMedia Ltd - datatables.net/license 3 | */ 4 | 5 | /** 6 | * DataTables integration for Bootstrap 3. This requires Bootstrap 3 and 7 | * DataTables 1.10 or newer. 8 | * 9 | * This file sets the defaults and adds options to DataTables to style its 10 | * controls using Bootstrap. See http://datatables.net/manual/styling/bootstrap 11 | * for further information. 12 | */ 13 | (function( factory ){ 14 | if ( typeof define === 'function' && define.amd ) { 15 | // AMD 16 | define( ['jquery', 'datatables.net'], function ( $ ) { 17 | return factory( $, window, document ); 18 | } ); 19 | } 20 | else if ( typeof exports === 'object' ) { 21 | // CommonJS 22 | module.exports = function (root, $) { 23 | if ( ! root ) { 24 | root = window; 25 | } 26 | 27 | if ( ! $ || ! $.fn.dataTable ) { 28 | // Require DataTables, which attaches to jQuery, including 29 | // jQuery if needed and have a $ property so we can access the 30 | // jQuery object that is used 31 | $ = require('datatables.net')(root, $).$; 32 | } 33 | 34 | return factory( $, root, root.document ); 35 | }; 36 | } 37 | else { 38 | // Browser 39 | factory( jQuery, window, document ); 40 | } 41 | }(function( $, window, document, undefined ) { 42 | 'use strict'; 43 | var DataTable = $.fn.dataTable; 44 | 45 | 46 | /* Set the defaults for DataTables initialisation */ 47 | $.extend( true, DataTable.defaults, { 48 | dom: 49 | "<'row'<'col-sm-12 col-md-6'l><'col-sm-12 col-md-6'f>>" + 50 | "<'row'<'col-sm-12'tr>>" + 51 | "<'row'<'col-sm-12 col-md-5'i><'col-sm-12 col-md-7'p>>", 52 | renderer: 'bootstrap' 53 | } ); 54 | 55 | 56 | /* Default class modification */ 57 | $.extend( DataTable.ext.classes, { 58 | sWrapper: "dataTables_wrapper container-fluid dt-bootstrap4", 59 | sFilterInput: "form-control form-control-sm", 60 | sLengthSelect: "form-control form-control-sm", 61 | sProcessing: "dataTables_processing card", 62 | sPageButton: "paginate_button page-item" 63 | } ); 64 | 65 | 66 | /* Bootstrap paging button renderer */ 67 | DataTable.ext.renderer.pageButton.bootstrap = function ( settings, host, idx, buttons, page, pages ) { 68 | var api = new DataTable.Api( settings ); 69 | var classes = settings.oClasses; 70 | var lang = settings.oLanguage.oPaginate; 71 | var aria = settings.oLanguage.oAria.paginate || {}; 72 | var btnDisplay, btnClass, counter=0; 73 | 74 | var attach = function( container, buttons ) { 75 | var i, ien, node, button; 76 | var clickHandler = function ( e ) { 77 | e.preventDefault(); 78 | if ( !$(e.currentTarget).hasClass('disabled') && api.page() != e.data.action ) { 79 | api.page( e.data.action ).draw( 'page' ); 80 | } 81 | }; 82 | 83 | for ( i=0, ien=buttons.length ; i 0 ? 102 | '' : ' disabled'); 103 | break; 104 | 105 | case 'previous': 106 | btnDisplay = lang.sPrevious; 107 | btnClass = button + (page > 0 ? 108 | '' : ' disabled'); 109 | break; 110 | 111 | case 'next': 112 | btnDisplay = lang.sNext; 113 | btnClass = button + (page < pages-1 ? 114 | '' : ' disabled'); 115 | break; 116 | 117 | case 'last': 118 | btnDisplay = lang.sLast; 119 | btnClass = button + (page < pages-1 ? 120 | '' : ' disabled'); 121 | break; 122 | 123 | default: 124 | btnDisplay = button + 1; 125 | btnClass = page === button ? 126 | 'active' : ''; 127 | break; 128 | } 129 | 130 | if ( btnDisplay ) { 131 | node = $('
  • ', { 132 | 'class': classes.sPageButton+' '+btnClass, 133 | 'id': idx === 0 && typeof button === 'string' ? 134 | settings.sTableId +'_'+ button : 135 | null 136 | } ) 137 | .append( $('', { 138 | 'href': '#', 139 | 'aria-controls': settings.sTableId, 140 | 'aria-label': aria[ button ], 141 | 'data-dt-idx': counter, 142 | 'tabindex': settings.iTabIndex, 143 | 'class': 'page-link' 144 | } ) 145 | .html( btnDisplay ) 146 | ) 147 | .appendTo( container ); 148 | 149 | settings.oApi._fnBindAction( 150 | node, {action: button}, clickHandler 151 | ); 152 | 153 | counter++; 154 | } 155 | } 156 | } 157 | }; 158 | 159 | // IE9 throws an 'unknown error' if document.activeElement is used 160 | // inside an iframe or frame. 161 | var activeEl; 162 | 163 | try { 164 | // Because this approach is destroying and recreating the paging 165 | // elements, focus is lost on the select button which is bad for 166 | // accessibility. So we want to restore focus once the draw has 167 | // completed 168 | activeEl = $(host).find(document.activeElement).data('dt-idx'); 169 | } 170 | catch (e) {} 171 | 172 | attach( 173 | $(host).empty().html('
      ').children('ul'), 174 | buttons 175 | ); 176 | 177 | if ( activeEl !== undefined ) { 178 | $(host).find( '[data-dt-idx='+activeEl+']' ).focus(); 179 | } 180 | }; 181 | 182 | 183 | return DataTable; 184 | })); 185 | -------------------------------------------------------------------------------- /ledfxcontroller/devices/__init__.py: -------------------------------------------------------------------------------- 1 | from ledfxcontroller.utils import BaseRegistry, RegistryLoader 2 | from abc import abstractmethod 3 | from threading import Thread 4 | import voluptuous as vol 5 | import numpy as np 6 | import importlib 7 | import pkgutil 8 | import logging 9 | import time 10 | import os 11 | import re 12 | 13 | _LOGGER = logging.getLogger(__name__) 14 | 15 | @BaseRegistry.no_registration 16 | class Device(BaseRegistry): 17 | 18 | CONFIG_SCHEMA = vol.Schema({ 19 | vol.Required('name'): str, 20 | vol.Required('type'): str, 21 | vol.Optional('max_brightness', default=1.0): vol.All(vol.Coerce(float), vol.Range(min=0, max=1)), 22 | vol.Optional('refresh_rate', default=60): int, 23 | vol.Optional('force_refresh', default=False): bool, 24 | vol.Optional('preview_only', default=False): bool 25 | }) 26 | 27 | _active = False 28 | _output_thread = None 29 | _active_effect = None 30 | _latest_frame = None 31 | 32 | def __init__(self, config): 33 | self._config = config 34 | 35 | def __del__(self): 36 | if self._active: 37 | self.deactivate() 38 | 39 | @property 40 | def pixel_count(self): 41 | pass 42 | 43 | def set_effect(self, effect, start_pixel = None, end_pixel = None): 44 | if self._active_effect != None: 45 | self._active_effect.deactivate() 46 | 47 | self._active_effect = effect 48 | self._active_effect.activate(self.pixel_count) 49 | if not self._active: 50 | self.activate() 51 | 52 | def clear_effect(self): 53 | if self._active_effect != None: 54 | self._active_effect.deactivate() 55 | self._active_effect = None 56 | 57 | if self._active: 58 | # Clear all the pixel data before deactiving the device 59 | self._latest_frame = np.zeros((self.pixel_count, 3)) 60 | self.flush(self._latest_frame) 61 | 62 | self.deactivate() 63 | 64 | @property 65 | def active_effect(self): 66 | return self._active_effect 67 | 68 | def thread_function(self): 69 | # TODO: Evaluate switching over to asyncio with UV loop optimization 70 | # instead of spinning a seperate thread. 71 | 72 | sleep_interval = 1 / self._config['refresh_rate'] 73 | 74 | while self._active: 75 | start_time = time.time() 76 | 77 | # Assemble the frame if necessary, if nothing changed just sleep 78 | assembled_frame = self.assemble_frame() 79 | if assembled_frame is not None and not self._config['preview_only']: 80 | self.flush(assembled_frame) 81 | 82 | # Calculate the time to sleep accounting for potential heavy 83 | # frame assembly operations 84 | time_to_sleep = sleep_interval - (time.time() - start_time) 85 | if time_to_sleep > 0: 86 | time.sleep(time_to_sleep) 87 | _LOGGER.info("Output device thread terminated.") 88 | 89 | def assemble_frame(self): 90 | """ 91 | Assembles the frame to be flushed. Currently this will just return 92 | the active channels pixels, but will eventaully handle things like 93 | merging multiple segments segments and alpha blending channels 94 | """ 95 | frame = None 96 | if self._active_effect._dirty: 97 | frame = np.clip(self._active_effect.pixels * self._config['max_brightness'], 0, 255) 98 | self._active_effect._dirty = self._config['force_refresh'] 99 | self._latest_frame = frame 100 | 101 | return frame 102 | 103 | def activate(self): 104 | self._active = True 105 | self._device_thread = Thread(target = self.thread_function) 106 | self._device_thread.start() 107 | 108 | def deactivate(self): 109 | self._active = False 110 | if self._device_thread: 111 | self._device_thread.join() 112 | self._device_thread = None 113 | 114 | @abstractmethod 115 | def flush(self, data): 116 | """ 117 | Flushes the provided data to the device. This abstract medthod must be 118 | overwritten by the device implementation. 119 | """ 120 | 121 | @property 122 | def name(self): 123 | return self._config['name'] 124 | 125 | @property 126 | def max_brightness(self): 127 | return self._config['max_brightness'] * 256 128 | 129 | @property 130 | def refresh_rate(self): 131 | return self._config['refresh_rate'] 132 | 133 | @property 134 | def latest_frame(self): 135 | return self._latest_frame 136 | 137 | 138 | class Devices(RegistryLoader): 139 | """Thin wrapper around the device registry that manages devices""" 140 | 141 | PACKAGE_NAME = 'ledfxcontroller.devices' 142 | 143 | def __init__(self, ledfx): 144 | super().__init__(Device, self.PACKAGE_NAME, ledfx) 145 | 146 | def create_from_config(self, config): 147 | for device_id, device_config in config.items(): 148 | self.create( 149 | config = device_config, 150 | id = device_id, 151 | name = device_config.get('type')) 152 | 153 | def clear_all_effects(self): 154 | for device in self.values(): 155 | device.clear_effect() 156 | 157 | def get_device(self, device_id): 158 | for device in self.values(): 159 | if device_id == device.id: 160 | return device 161 | return None 162 | 163 | 164 | -------------------------------------------------------------------------------- /ledfxcontroller/effects/mel.py: -------------------------------------------------------------------------------- 1 | """This module implements a Mel Filter Bank. 2 | In other words it is a filter bank with triangular shaped bands 3 | arnged on the mel frequency scale. 4 | An example ist shown in the following figure: 5 | .. plot:: 6 | from pylab import plt 7 | import melbank 8 | f1, f2 = 1000, 8000 9 | melmat, (melfreq, fftfreq) = melbank.compute_melmat(6, f1, f2, num_fft_bands=4097) 10 | fig, ax = plt.subplots(figsize=(8, 3)) 11 | ax.plot(fftfreq, melmat.T) 12 | ax.grid(True) 13 | ax.set_ylabel('Weight') 14 | ax.set_xlabel('Frequency / Hz') 15 | ax.set_xlim((f1, f2)) 16 | ax2 = ax.twiny() 17 | ax2.xaxis.set_ticks_position('top') 18 | ax2.set_xlim((f1, f2)) 19 | ax2.xaxis.set_ticks(melbank.mel_to_hertz(melfreq)) 20 | ax2.xaxis.set_ticklabels(['{:.0f}'.format(mf) for mf in melfreq]) 21 | ax2.set_xlabel('Frequency / mel') 22 | plt.tight_layout() 23 | fig, ax = plt.subplots() 24 | ax.matshow(melmat) 25 | plt.axis('equal') 26 | plt.axis('tight') 27 | plt.title('Mel Matrix') 28 | plt.tight_layout() 29 | Functions 30 | --------- 31 | """ 32 | 33 | from numpy import abs, append, arange, insert, linspace, log10, round, zeros 34 | from math import log 35 | 36 | 37 | def hertz_to_mel(freq): 38 | """Returns mel-frequency from linear frequency input. 39 | Parameter 40 | --------- 41 | freq : scalar or ndarray 42 | Frequency value or array in Hz. 43 | Returns 44 | ------- 45 | mel : scalar or ndarray 46 | Mel-frequency value or ndarray in Mel 47 | """ 48 | #return 2595.0 * log10(1 + (freq / 700.0)) 49 | return 3340.0 * log(1 + (freq / 250.0), 9) 50 | 51 | 52 | def mel_to_hertz(mel): 53 | """Returns frequency from mel-frequency input. 54 | Parameter 55 | --------- 56 | mel : scalar or ndarray 57 | Mel-frequency value or ndarray in Mel 58 | Returns 59 | ------- 60 | freq : scalar or ndarray 61 | Frequency value or array in Hz. 62 | """ 63 | #return 700.0 * (10**(mel / 2595.0)) - 700.0 64 | return 250.0 * (9**(mel / 3340.0)) - 250.0 65 | 66 | 67 | def melfrequencies_mel_filterbank(num_bands, freq_min, freq_max, num_fft_bands): 68 | """Returns centerfrequencies and band edges for a mel filter bank 69 | Parameters 70 | ---------- 71 | num_bands : int 72 | Number of mel bands. 73 | freq_min : scalar 74 | Minimum frequency for the first band. 75 | freq_max : scalar 76 | Maximum frequency for the last band. 77 | num_fft_bands : int 78 | Number of fft bands. 79 | Returns 80 | ------- 81 | center_frequencies_mel : ndarray 82 | lower_edges_mel : ndarray 83 | upper_edges_mel : ndarray 84 | """ 85 | 86 | mel_max = hertz_to_mel(freq_max) 87 | mel_min = hertz_to_mel(freq_min) 88 | delta_mel = abs(mel_max - mel_min) / (num_bands + 1.0) 89 | frequencies_mel = mel_min + delta_mel * arange(0, num_bands + 2) 90 | lower_edges_mel = frequencies_mel[:-2] 91 | upper_edges_mel = frequencies_mel[2:] 92 | center_frequencies_mel = frequencies_mel[1:-1] 93 | return center_frequencies_mel, lower_edges_mel, upper_edges_mel 94 | 95 | 96 | def compute_melmat(num_mel_bands=12, freq_min=64, freq_max=8000, 97 | num_fft_bands=513, sample_rate=16000): 98 | """Returns tranformation matrix for mel spectrum. 99 | Parameters 100 | ---------- 101 | num_mel_bands : int 102 | Number of mel bands. Number of rows in melmat. 103 | Default: 24 104 | freq_min : scalar 105 | Minimum frequency for the first band. 106 | Default: 64 107 | freq_max : scalar 108 | Maximum frequency for the last band. 109 | Default: 8000 110 | num_fft_bands : int 111 | Number of fft-frequenc bands. This ist NFFT/2+1 ! 112 | number of columns in melmat. 113 | Default: 513 (this means NFFT=1024) 114 | sample_rate : scalar 115 | Sample rate for the signals that will be used. 116 | Default: 44100 117 | Returns 118 | ------- 119 | melmat : ndarray 120 | Transformation matrix for the mel spectrum. 121 | Use this with fft spectra of num_fft_bands_bands length 122 | and multiply the spectrum with the melmat 123 | this will tranform your fft-spectrum 124 | to a mel-spectrum. 125 | frequencies : tuple (ndarray , ndarray ) 126 | Center frequencies of the mel bands, center frequencies of fft spectrum. 127 | """ 128 | center_frequencies_mel, lower_edges_mel, upper_edges_mel = \ 129 | melfrequencies_mel_filterbank( 130 | num_mel_bands, 131 | freq_min, 132 | freq_max, 133 | num_fft_bands 134 | ) 135 | 136 | center_frequencies_hz = mel_to_hertz(center_frequencies_mel) 137 | lower_edges_hz = mel_to_hertz(lower_edges_mel) 138 | upper_edges_hz = mel_to_hertz(upper_edges_mel) 139 | freqs = linspace(0.0, sample_rate / 2.0, num_fft_bands) 140 | melmat = zeros((num_mel_bands, num_fft_bands)) 141 | 142 | for imelband, (center, lower, upper) in enumerate(zip( 143 | center_frequencies_hz, lower_edges_hz, upper_edges_hz)): 144 | 145 | left_slope = (freqs >= lower) == (freqs <= center) 146 | melmat[imelband, left_slope] = ( 147 | (freqs[left_slope] - lower) / (center - lower) 148 | ) 149 | 150 | right_slope = (freqs >= center) == (freqs <= upper) 151 | melmat[imelband, right_slope] = ( 152 | (upper - freqs[right_slope]) / (upper - center) 153 | ) 154 | return melmat, (center_frequencies_mel, freqs) 155 | -------------------------------------------------------------------------------- /ledfxcontroller/api/websocket.py: -------------------------------------------------------------------------------- 1 | import json 2 | import logging 3 | import asyncio 4 | from aiohttp import web 5 | import voluptuous as vol 6 | from concurrent import futures 7 | from ledfxcontroller.api import RestEndpoint 8 | 9 | _LOGGER = logging.getLogger(__name__) 10 | MAX_PENDING_MESSAGES = 256 11 | 12 | BASE_MESSAGE_SCHEMA = vol.Schema({ 13 | vol.Required('id'): vol.Coerce(int), 14 | vol.Required('type'): str, 15 | }, extra=vol.ALLOW_EXTRA) 16 | 17 | # TODO: Have a more well defined registration and a more componetized solution. 18 | # Could do something like have Device actually provide the handler for Device 19 | # related functionality. This would allow easy access to internal workings and 20 | # events. 21 | websocket_handlers = {} 22 | def websocket_handler(type): 23 | def function(func): 24 | websocket_handlers[type] = func 25 | return func 26 | return function 27 | 28 | class WebsocketEndpoint(RestEndpoint): 29 | 30 | ENDPOINT_PATH = "/api/websocket" 31 | 32 | async def get(self, request) -> web.Response: 33 | return await WebsocketConnection(self.ledfx).handle(request) 34 | 35 | class WebsocketConnection: 36 | 37 | def __init__(self, ledfx): 38 | self.ledfx = ledfx 39 | self.socket = None 40 | 41 | self.receiver_task = None 42 | self.sender_task = None 43 | self.sender_queue = asyncio.Queue(maxsize=MAX_PENDING_MESSAGES, loop=ledfx.loop) 44 | 45 | def close(self): 46 | """Closes the websocket connection""" 47 | 48 | if self.receiver_task: 49 | self.receiver_task.cancel() 50 | if self.sender_task: 51 | self.sender_task.cancel() 52 | 53 | def send(self, message): 54 | """Sends a message to the websocket connection""" 55 | 56 | try: 57 | self.sender_queue.put_nowait(message) 58 | except asyncio.QueueFull: 59 | _LOGGER.error('Client sender queue size exceeded {}'.format( 60 | MAX_PENDING_MESSAGES)) 61 | self.close() 62 | 63 | def send_error(self, id, message): 64 | """Sends an error string to the websocket connection""" 65 | 66 | return self.send({ 67 | 'id': id, 68 | 'success': False, 69 | 'error': { 70 | 'message': message 71 | } 72 | }) 73 | 74 | async def _sender(self): 75 | """Async write loop to pull from the queue and send""" 76 | 77 | _LOGGER.info("Starting sender") 78 | while not self.socket.closed: 79 | message = await self.sender_queue.get() 80 | if message is None: 81 | break 82 | 83 | try: 84 | _LOGGER.info("Sending websocket message") 85 | await self.socket.send_json(message, dumps=json.dumps) 86 | except TypeError as err: 87 | _LOGGER.error('Unable to serialize to JSON: %s\n%s', 88 | err, message) 89 | 90 | _LOGGER.info("Stopping sender") 91 | 92 | async def handle(self, request): 93 | """Handle the websocket connection""" 94 | 95 | socket = self.socket = web.WebSocketResponse() 96 | await socket.prepare(request) 97 | _LOGGER.info("Websocket connected.") 98 | 99 | self.receiver_task = asyncio.Task.current_task(loop=self.ledfx.loop) 100 | self.sender_task = self.ledfx.loop.create_task(self._sender()) 101 | 102 | try: 103 | message = await socket.receive_json() 104 | while message: 105 | message = BASE_MESSAGE_SCHEMA(message) 106 | 107 | if message['type'] in websocket_handlers: 108 | websocket_handlers[message['type']](self, message) 109 | else: 110 | _LOGGER.error(('Received unknown command {}').format(message['type'])) 111 | self.send_error(message['id'], 'Unknown command type.') 112 | 113 | message = await socket.receive_json() 114 | 115 | except (vol.Invalid, ValueError) as e: 116 | self.send_error(message['id'], 'Invalid message format.') 117 | 118 | except TypeError as e: 119 | if socket.closed: 120 | _LOGGER.info('Connection closed by client.') 121 | else: 122 | _LOGGER.exception('Unexpected TypeError: {}'.format(e)) 123 | 124 | except (asyncio.CancelledError, futures.CancelledError) as e: 125 | _LOGGER.info("Connection cancelled") 126 | 127 | except Exception as err: 128 | _LOGGER.exception("Unexpected Exception: %s", err) 129 | 130 | finally: 131 | 132 | # Gracefully stop the sender ensuring all messages get flushed 133 | self.send(None) 134 | await self.sender_task 135 | 136 | # Close the connection 137 | await socket.close() 138 | _LOGGER.info("Closed connection") 139 | 140 | return socket 141 | 142 | import numpy as np 143 | 144 | @websocket_handler('get_pixels') 145 | def get_pixels_handler(conn, message): 146 | device = conn.ledfx.devices.get(message.get('device_id')) 147 | if device is None: 148 | conn.send_error(message['id'], 'Device not found.') 149 | 150 | rgb_x = np.arange(0, device.pixel_count).tolist() 151 | if device.latest_frame is not None: 152 | pixels = np.copy(device.latest_frame).T 153 | conn.send({"action": "update_pixels", "rgb_x": rgb_x, "r": pixels[0].tolist(), "g": pixels[1].tolist(), "b": pixels[2].tolist()}) 154 | else: 155 | pixels = np.zeros((device.pixel_count, 3)) 156 | conn.send({"action": "update_pixels", "rgb_x": rgb_x, "r": pixels[0].tolist(), "g": pixels[1].tolist(), "b": pixels[2].tolist()}) -------------------------------------------------------------------------------- /ledfxcontroller/frontend/external/datatables/dataTables.bootstrap4.css: -------------------------------------------------------------------------------- 1 | table.dataTable { 2 | clear: both; 3 | margin-top: 6px !important; 4 | margin-bottom: 6px !important; 5 | max-width: none !important; 6 | border-collapse: separate !important; 7 | } 8 | table.dataTable td, 9 | table.dataTable th { 10 | -webkit-box-sizing: content-box; 11 | box-sizing: content-box; 12 | } 13 | table.dataTable td.dataTables_empty, 14 | table.dataTable th.dataTables_empty { 15 | text-align: center; 16 | } 17 | table.dataTable.nowrap th, 18 | table.dataTable.nowrap td { 19 | white-space: nowrap; 20 | } 21 | 22 | div.dataTables_wrapper div.dataTables_length label { 23 | font-weight: normal; 24 | text-align: left; 25 | white-space: nowrap; 26 | } 27 | div.dataTables_wrapper div.dataTables_length select { 28 | width: 75px; 29 | display: inline-block; 30 | } 31 | div.dataTables_wrapper div.dataTables_filter { 32 | text-align: right; 33 | } 34 | div.dataTables_wrapper div.dataTables_filter label { 35 | font-weight: normal; 36 | white-space: nowrap; 37 | text-align: left; 38 | } 39 | div.dataTables_wrapper div.dataTables_filter input { 40 | margin-left: 0.5em; 41 | display: inline-block; 42 | width: auto; 43 | } 44 | div.dataTables_wrapper div.dataTables_info { 45 | padding-top: 0.85em; 46 | white-space: nowrap; 47 | } 48 | div.dataTables_wrapper div.dataTables_paginate { 49 | margin: 0; 50 | white-space: nowrap; 51 | text-align: right; 52 | } 53 | div.dataTables_wrapper div.dataTables_paginate ul.pagination { 54 | margin: 2px 0; 55 | white-space: nowrap; 56 | justify-content: flex-end; 57 | } 58 | div.dataTables_wrapper div.dataTables_processing { 59 | position: absolute; 60 | top: 50%; 61 | left: 50%; 62 | width: 200px; 63 | margin-left: -100px; 64 | margin-top: -26px; 65 | text-align: center; 66 | padding: 1em 0; 67 | } 68 | 69 | table.dataTable thead > tr > th.sorting_asc, table.dataTable thead > tr > th.sorting_desc, table.dataTable thead > tr > th.sorting, 70 | table.dataTable thead > tr > td.sorting_asc, 71 | table.dataTable thead > tr > td.sorting_desc, 72 | table.dataTable thead > tr > td.sorting { 73 | padding-right: 30px; 74 | } 75 | table.dataTable thead > tr > th:active, 76 | table.dataTable thead > tr > td:active { 77 | outline: none; 78 | } 79 | table.dataTable thead .sorting, 80 | table.dataTable thead .sorting_asc, 81 | table.dataTable thead .sorting_desc, 82 | table.dataTable thead .sorting_asc_disabled, 83 | table.dataTable thead .sorting_desc_disabled { 84 | cursor: pointer; 85 | position: relative; 86 | } 87 | table.dataTable thead .sorting:before, table.dataTable thead .sorting:after, 88 | table.dataTable thead .sorting_asc:before, 89 | table.dataTable thead .sorting_asc:after, 90 | table.dataTable thead .sorting_desc:before, 91 | table.dataTable thead .sorting_desc:after, 92 | table.dataTable thead .sorting_asc_disabled:before, 93 | table.dataTable thead .sorting_asc_disabled:after, 94 | table.dataTable thead .sorting_desc_disabled:before, 95 | table.dataTable thead .sorting_desc_disabled:after { 96 | position: absolute; 97 | bottom: 0.9em; 98 | display: block; 99 | opacity: 0.3; 100 | } 101 | table.dataTable thead .sorting:before, 102 | table.dataTable thead .sorting_asc:before, 103 | table.dataTable thead .sorting_desc:before, 104 | table.dataTable thead .sorting_asc_disabled:before, 105 | table.dataTable thead .sorting_desc_disabled:before { 106 | right: 1em; 107 | content: "\2191"; 108 | } 109 | table.dataTable thead .sorting:after, 110 | table.dataTable thead .sorting_asc:after, 111 | table.dataTable thead .sorting_desc:after, 112 | table.dataTable thead .sorting_asc_disabled:after, 113 | table.dataTable thead .sorting_desc_disabled:after { 114 | right: 0.5em; 115 | content: "\2193"; 116 | } 117 | table.dataTable thead .sorting_asc:before, 118 | table.dataTable thead .sorting_desc:after { 119 | opacity: 1; 120 | } 121 | table.dataTable thead .sorting_asc_disabled:before, 122 | table.dataTable thead .sorting_desc_disabled:after { 123 | opacity: 0; 124 | } 125 | 126 | div.dataTables_scrollHead table.dataTable { 127 | margin-bottom: 0 !important; 128 | } 129 | 130 | div.dataTables_scrollBody table { 131 | border-top: none; 132 | margin-top: 0 !important; 133 | margin-bottom: 0 !important; 134 | } 135 | div.dataTables_scrollBody table thead .sorting:after, 136 | div.dataTables_scrollBody table thead .sorting_asc:after, 137 | div.dataTables_scrollBody table thead .sorting_desc:after { 138 | display: none; 139 | } 140 | div.dataTables_scrollBody table tbody tr:first-child th, 141 | div.dataTables_scrollBody table tbody tr:first-child td { 142 | border-top: none; 143 | } 144 | 145 | div.dataTables_scrollFoot > .dataTables_scrollFootInner { 146 | box-sizing: content-box; 147 | } 148 | div.dataTables_scrollFoot > .dataTables_scrollFootInner > table { 149 | margin-top: 0 !important; 150 | border-top: none; 151 | } 152 | 153 | @media screen and (max-width: 767px) { 154 | div.dataTables_wrapper div.dataTables_length, 155 | div.dataTables_wrapper div.dataTables_filter, 156 | div.dataTables_wrapper div.dataTables_info, 157 | div.dataTables_wrapper div.dataTables_paginate { 158 | text-align: center; 159 | } 160 | } 161 | table.dataTable.table-sm > thead > tr > th { 162 | padding-right: 20px; 163 | } 164 | table.dataTable.table-sm .sorting:before, 165 | table.dataTable.table-sm .sorting_asc:before, 166 | table.dataTable.table-sm .sorting_desc:before { 167 | top: 5px; 168 | right: 0.85em; 169 | } 170 | table.dataTable.table-sm .sorting:after, 171 | table.dataTable.table-sm .sorting_asc:after, 172 | table.dataTable.table-sm .sorting_desc:after { 173 | top: 5px; 174 | } 175 | 176 | table.table-bordered.dataTable th, 177 | table.table-bordered.dataTable td { 178 | border-left-width: 0; 179 | } 180 | table.table-bordered.dataTable th:last-child, table.table-bordered.dataTable th:last-child, 181 | table.table-bordered.dataTable td:last-child, 182 | table.table-bordered.dataTable td:last-child { 183 | border-right-width: 0; 184 | } 185 | table.table-bordered.dataTable tbody th, 186 | table.table-bordered.dataTable tbody td { 187 | border-bottom-width: 0; 188 | } 189 | 190 | div.dataTables_scrollHead table.table-bordered { 191 | border-bottom-width: 0; 192 | } 193 | 194 | div.table-responsive > div.dataTables_wrapper > div.row { 195 | margin: 0; 196 | } 197 | div.table-responsive > div.dataTables_wrapper > div.row > div[class^="col-"]:first-child { 198 | padding-left: 0; 199 | } 200 | div.table-responsive > div.dataTables_wrapper > div.row > div[class^="col-"]:last-child { 201 | padding-right: 0; 202 | } 203 | -------------------------------------------------------------------------------- /ledfxcontroller/frontend/external/bootstrap/css/bootstrap-reboot.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap Reboot v4.0.0-beta.2 (https://getbootstrap.com) 3 | * Copyright 2011-2017 The Bootstrap Authors 4 | * Copyright 2011-2017 Twitter, Inc. 5 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) 6 | * Forked from Normalize.css, licensed MIT (https://github.com/necolas/normalize.css/blob/master/LICENSE.md) 7 | */ 8 | *, 9 | *::before, 10 | *::after { 11 | box-sizing: border-box; 12 | } 13 | 14 | html { 15 | font-family: sans-serif; 16 | line-height: 1.15; 17 | -webkit-text-size-adjust: 100%; 18 | -ms-text-size-adjust: 100%; 19 | -ms-overflow-style: scrollbar; 20 | -webkit-tap-highlight-color: transparent; 21 | } 22 | 23 | @-ms-viewport { 24 | width: device-width; 25 | } 26 | 27 | article, aside, dialog, figcaption, figure, footer, header, hgroup, main, nav, section { 28 | display: block; 29 | } 30 | 31 | body { 32 | margin: 0; 33 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; 34 | font-size: 1rem; 35 | font-weight: 400; 36 | line-height: 1.5; 37 | color: #212529; 38 | text-align: left; 39 | background-color: #fff; 40 | } 41 | 42 | [tabindex="-1"]:focus { 43 | outline: none !important; 44 | } 45 | 46 | hr { 47 | box-sizing: content-box; 48 | height: 0; 49 | overflow: visible; 50 | } 51 | 52 | h1, h2, h3, h4, h5, h6 { 53 | margin-top: 0; 54 | margin-bottom: 0.5rem; 55 | } 56 | 57 | p { 58 | margin-top: 0; 59 | margin-bottom: 1rem; 60 | } 61 | 62 | abbr[title], 63 | abbr[data-original-title] { 64 | text-decoration: underline; 65 | -webkit-text-decoration: underline dotted; 66 | text-decoration: underline dotted; 67 | cursor: help; 68 | border-bottom: 0; 69 | } 70 | 71 | address { 72 | margin-bottom: 1rem; 73 | font-style: normal; 74 | line-height: inherit; 75 | } 76 | 77 | ol, 78 | ul, 79 | dl { 80 | margin-top: 0; 81 | margin-bottom: 1rem; 82 | } 83 | 84 | ol ol, 85 | ul ul, 86 | ol ul, 87 | ul ol { 88 | margin-bottom: 0; 89 | } 90 | 91 | dt { 92 | font-weight: 700; 93 | } 94 | 95 | dd { 96 | margin-bottom: .5rem; 97 | margin-left: 0; 98 | } 99 | 100 | blockquote { 101 | margin: 0 0 1rem; 102 | } 103 | 104 | dfn { 105 | font-style: italic; 106 | } 107 | 108 | b, 109 | strong { 110 | font-weight: bolder; 111 | } 112 | 113 | small { 114 | font-size: 80%; 115 | } 116 | 117 | sub, 118 | sup { 119 | position: relative; 120 | font-size: 75%; 121 | line-height: 0; 122 | vertical-align: baseline; 123 | } 124 | 125 | sub { 126 | bottom: -.25em; 127 | } 128 | 129 | sup { 130 | top: -.5em; 131 | } 132 | 133 | a { 134 | color: #007bff; 135 | text-decoration: none; 136 | background-color: transparent; 137 | -webkit-text-decoration-skip: objects; 138 | } 139 | 140 | a:hover { 141 | color: #0056b3; 142 | text-decoration: underline; 143 | } 144 | 145 | a:not([href]):not([tabindex]) { 146 | color: inherit; 147 | text-decoration: none; 148 | } 149 | 150 | a:not([href]):not([tabindex]):focus, a:not([href]):not([tabindex]):hover { 151 | color: inherit; 152 | text-decoration: none; 153 | } 154 | 155 | a:not([href]):not([tabindex]):focus { 156 | outline: 0; 157 | } 158 | 159 | pre, 160 | code, 161 | kbd, 162 | samp { 163 | font-family: monospace, monospace; 164 | font-size: 1em; 165 | } 166 | 167 | pre { 168 | margin-top: 0; 169 | margin-bottom: 1rem; 170 | overflow: auto; 171 | -ms-overflow-style: scrollbar; 172 | } 173 | 174 | figure { 175 | margin: 0 0 1rem; 176 | } 177 | 178 | img { 179 | vertical-align: middle; 180 | border-style: none; 181 | } 182 | 183 | svg:not(:root) { 184 | overflow: hidden; 185 | } 186 | 187 | a, 188 | area, 189 | button, 190 | [role="button"], 191 | input:not([type="range"]), 192 | label, 193 | select, 194 | summary, 195 | textarea { 196 | -ms-touch-action: manipulation; 197 | touch-action: manipulation; 198 | } 199 | 200 | table { 201 | border-collapse: collapse; 202 | } 203 | 204 | caption { 205 | padding-top: 0.75rem; 206 | padding-bottom: 0.75rem; 207 | color: #868e96; 208 | text-align: left; 209 | caption-side: bottom; 210 | } 211 | 212 | th { 213 | text-align: inherit; 214 | } 215 | 216 | label { 217 | display: inline-block; 218 | margin-bottom: .5rem; 219 | } 220 | 221 | button { 222 | border-radius: 0; 223 | } 224 | 225 | button:focus { 226 | outline: 1px dotted; 227 | outline: 5px auto -webkit-focus-ring-color; 228 | } 229 | 230 | input, 231 | button, 232 | select, 233 | optgroup, 234 | textarea { 235 | margin: 0; 236 | font-family: inherit; 237 | font-size: inherit; 238 | line-height: inherit; 239 | } 240 | 241 | button, 242 | input { 243 | overflow: visible; 244 | } 245 | 246 | button, 247 | select { 248 | text-transform: none; 249 | } 250 | 251 | button, 252 | html [type="button"], 253 | [type="reset"], 254 | [type="submit"] { 255 | -webkit-appearance: button; 256 | } 257 | 258 | button::-moz-focus-inner, 259 | [type="button"]::-moz-focus-inner, 260 | [type="reset"]::-moz-focus-inner, 261 | [type="submit"]::-moz-focus-inner { 262 | padding: 0; 263 | border-style: none; 264 | } 265 | 266 | input[type="radio"], 267 | input[type="checkbox"] { 268 | box-sizing: border-box; 269 | padding: 0; 270 | } 271 | 272 | input[type="date"], 273 | input[type="time"], 274 | input[type="datetime-local"], 275 | input[type="month"] { 276 | -webkit-appearance: listbox; 277 | } 278 | 279 | textarea { 280 | overflow: auto; 281 | resize: vertical; 282 | } 283 | 284 | fieldset { 285 | min-width: 0; 286 | padding: 0; 287 | margin: 0; 288 | border: 0; 289 | } 290 | 291 | legend { 292 | display: block; 293 | width: 100%; 294 | max-width: 100%; 295 | padding: 0; 296 | margin-bottom: .5rem; 297 | font-size: 1.5rem; 298 | line-height: inherit; 299 | color: inherit; 300 | white-space: normal; 301 | } 302 | 303 | progress { 304 | vertical-align: baseline; 305 | } 306 | 307 | [type="number"]::-webkit-inner-spin-button, 308 | [type="number"]::-webkit-outer-spin-button { 309 | height: auto; 310 | } 311 | 312 | [type="search"] { 313 | outline-offset: -2px; 314 | -webkit-appearance: none; 315 | } 316 | 317 | [type="search"]::-webkit-search-cancel-button, 318 | [type="search"]::-webkit-search-decoration { 319 | -webkit-appearance: none; 320 | } 321 | 322 | ::-webkit-file-upload-button { 323 | font: inherit; 324 | -webkit-appearance: button; 325 | } 326 | 327 | output { 328 | display: inline-block; 329 | } 330 | 331 | summary { 332 | display: list-item; 333 | } 334 | 335 | template { 336 | display: none; 337 | } 338 | 339 | [hidden] { 340 | display: none !important; 341 | } 342 | /*# sourceMappingURL=bootstrap-reboot.css.map */ -------------------------------------------------------------------------------- /ledfxcontroller/utils.py: -------------------------------------------------------------------------------- 1 | from asyncio import coroutines, ensure_future 2 | import concurrent.futures 3 | import voluptuous as vol 4 | from abc import ABC 5 | import threading 6 | import logging 7 | import inspect 8 | import importlib 9 | import pkgutil 10 | 11 | _LOGGER = logging.getLogger(__name__) 12 | 13 | def async_fire_and_forget(coro, loop): 14 | """Run some code in the core event loop without a result""" 15 | 16 | if not coroutines.iscoroutine(coro): 17 | raise TypeError(('A coroutine object is required: {}').format(coro)) 18 | 19 | def callback(): 20 | """Handle the firing of a coroutine.""" 21 | ensure_future(coro, loop=loop) 22 | 23 | loop.call_soon_threadsafe(callback) 24 | return 25 | 26 | def async_callback(loop, callback, *args): 27 | """Run a callback in the event loop with access to the result""" 28 | 29 | future = concurrent.futures.Future() 30 | def run_callback(): 31 | try: 32 | future.set_result(callback(*args)) 33 | # pylint: disable=broad-except 34 | except Exception as e: 35 | if future.set_running_or_notify_cancel(): 36 | future.set_exception(e) 37 | else: 38 | _LOGGER.warning("Exception on lost future: ", exc_info=True) 39 | 40 | loop.call_soon_threadsafe(run_callback) 41 | return future 42 | 43 | def hasattr_explicit(cls, attr): 44 | """Returns if the given object has explicitly declared an attribute""" 45 | try: 46 | return getattr(cls, attr) != getattr(super(cls, cls), attr, None) 47 | except AttributeError: 48 | return False 49 | 50 | def getattr_explicit(cls, attr, *default): 51 | """Gets an explicit attribute from an object""" 52 | 53 | if len(default) > 1: 54 | raise TypeError("getattr_explicit expected at most 3 arguments, got {}".format( 55 | len(default) + 2)) 56 | 57 | if hasattr_explicit(cls, attr): 58 | return getattr(cls, attr, default) 59 | if default: 60 | return default[0] 61 | 62 | raise AttributeError("type object '{}' has no attribute '{}'.".format( 63 | cls.__name__, attr)) 64 | 65 | class BaseRegistry(ABC): 66 | """ 67 | Base registry class used for effects and devices. This maintains a 68 | list of automatically registered base classes and assembles schema 69 | information 70 | 71 | The prevent registration for classes that are intended to serve as 72 | base classes (i.e. GradientEffect) add the following declarator: 73 | @Effect.no_registration 74 | """ 75 | _schema_attr = 'CONFIG_SCHEMA' 76 | 77 | def __init_subclass__(cls, **kwargs): 78 | """Automatically register the class""" 79 | super().__init_subclass__(**kwargs) 80 | 81 | if not hasattr(cls, '_registry'): 82 | cls._registry = {} 83 | 84 | name = cls.__module__.split('.')[-1] 85 | cls._registry[name] = cls 86 | 87 | @classmethod 88 | def no_registration(self, cls): 89 | """Clear registration entiry based on special declarator""" 90 | 91 | name = cls.__module__.split('.')[-1] 92 | del cls._registry[name] 93 | return cls 94 | 95 | @classmethod 96 | def schema(self, extended=True, extra=vol.ALLOW_EXTRA): 97 | """Returns the extended schema of the class""" 98 | 99 | if extended is False: 100 | return getattr_explicit(type(self), self._schema_attr, vol.Schema({})) 101 | 102 | schema = vol.Schema({}, extra=extra) 103 | classes = inspect.getmro(self)[::-1] 104 | for c in classes: 105 | c_schema = getattr_explicit(c, self._schema_attr, None) 106 | if c_schema is not None: 107 | schema = schema.extend(c_schema.schema) 108 | 109 | return schema 110 | 111 | @classmethod 112 | def registry(self): 113 | """Returns all the subclasses in the registry""" 114 | 115 | return self._registry 116 | 117 | @property 118 | def id(self): 119 | """Returns the id for the object""" 120 | return getattr(self, '_id', None) 121 | 122 | @property 123 | def type(self): 124 | """Returns the id for the object""" 125 | return getattr(self, '_type', None) 126 | 127 | @property 128 | def config(self): 129 | """Returns the config for the object""" 130 | return getattr(self, '_config', None) 131 | 132 | class RegistryLoader(object): 133 | """Manages loading of compoents for a given registry""" 134 | 135 | def __init__(self, cls, package, ledfx): 136 | self._package = package 137 | self._ledfx = ledfx 138 | self._cls = cls 139 | self._objects = {} 140 | self._object_id = 1 141 | 142 | self.import_registry(package) 143 | 144 | def import_registry(self, package): 145 | """ 146 | Imports all the modules in the package thus hydrating 147 | the registry for the class 148 | """ 149 | 150 | found = self.discover_modules(package) 151 | _LOGGER.info("Importing {} from {}".format(found, package)) 152 | for name in found: 153 | importlib.import_module(name) 154 | 155 | def discover_modules(self, package): 156 | """Discovers all modules in the package""" 157 | module = importlib.import_module(package) 158 | 159 | found = [] 160 | for _, name, _ in pkgutil.iter_modules(module.__path__, package + '.'): 161 | found.append(name) 162 | 163 | return found 164 | 165 | def __iter__(self): 166 | return iter(self._objects) 167 | 168 | def types(self): 169 | """Returns all the type strings in the registry""" 170 | return list(self._cls.registry().keys()) 171 | 172 | def classes(self): 173 | """Returns all the classes in the regsitry""" 174 | return self._cls.registry() 175 | 176 | def get_class(self, type): 177 | return self._cls.registry()[type] 178 | 179 | def values(self): 180 | """Returns all the created objects""" 181 | return self._objects.values() 182 | 183 | def reload(self, force = False): 184 | """Reloads the registry""" 185 | 186 | # TODO: Deteremine exactly how to reload. This seems to work sometimes 187 | # depending on the current state. Probably need to invalidate the 188 | # system cash to ensure everything gets reloaded 189 | self.import_registry(self._package) 190 | 191 | def create(self, name, config = {}, id = None, *args): 192 | """Loads and creates a object from the registry by name""" 193 | 194 | if name not in self._cls.registry(): 195 | raise AttributeError(("Couldn't find '{}' in the {} registry").format( 196 | name, self._cls.__name__.lower())) 197 | if id is None: 198 | id = self._object_id 199 | self._object_id = self._object_id + 1 200 | if id in self._objects: 201 | raise AttributeError(("Object with id '{}' already created").format(id)) 202 | 203 | # Create the new object based on the registry entires and 204 | # validate the schema. 205 | _cls = self._cls.registry().get(name) 206 | if config is not None: 207 | config = _cls.schema()(config) 208 | obj = _cls(config, *args) 209 | else: 210 | obj = _cls(*args) 211 | 212 | # Attach some common properties 213 | setattr(obj, '_id', id) 214 | setattr(obj, '_type', name) 215 | 216 | # Store the object into the internal list and return it 217 | self._objects[id] = obj 218 | return obj 219 | 220 | def destroy(self, id): 221 | 222 | if id not in self._objects: 223 | raise AttributeError(("Object with id '{}' does not exist.").format(id)) 224 | del self._objects[id] 225 | 226 | def get(self, id): 227 | return self._objects.get(id) -------------------------------------------------------------------------------- /ledfxcontroller/effects/audio.py: -------------------------------------------------------------------------------- 1 | import time 2 | import logging 3 | import pyaudio 4 | from ledfxcontroller.effects import Effect 5 | import voluptuous as vol 6 | from scipy.ndimage.filters import gaussian_filter1d 7 | import ledfxcontroller.effects.mel as mel 8 | from ledfxcontroller.effects.math import ExpFilter 9 | import ledfxcontroller.effects.math as math 10 | from functools import lru_cache 11 | import numpy as np 12 | 13 | _LOGGER = logging.getLogger(__name__) 14 | 15 | class AudioInputSource(object): 16 | 17 | _audio = None 18 | _stream = None 19 | _callbacks = [] 20 | 21 | def __init__(self, mic_rate, sample_rate): 22 | self._mic_rate = mic_rate 23 | self._sample_rate = sample_rate 24 | self._frames_per_buffer = int(self._mic_rate / self._sample_rate) 25 | 26 | def activate(self): 27 | 28 | if self._audio is None: 29 | self._audio = pyaudio.PyAudio() 30 | 31 | self._stream = self._audio.open( 32 | format=pyaudio.paInt16, 33 | channels=1, 34 | rate=self._mic_rate, 35 | input=True, 36 | frames_per_buffer = self._frames_per_buffer, 37 | stream_callback = self._audio_sample_callback) 38 | self._stream.start_stream() 39 | 40 | _LOGGER.info("Audio source opened.") 41 | 42 | def deactivate(self): 43 | self._stream.stop_stream() 44 | self._stream.close() 45 | self._stream = None 46 | _LOGGER.info("Audio source closed.") 47 | 48 | def subscribe(self, callback): 49 | """Registers a callback with the input source""" 50 | self._callbacks.append(callback) 51 | 52 | if len(self._callbacks) == 1: 53 | self.activate() 54 | 55 | def unsubscribe(self, callback): 56 | """Unregisters a callback with the input srouce""" 57 | self._callbacks.remove(callback) 58 | 59 | if len(self._callbacks) == 0: 60 | self.deactivate() 61 | 62 | def _audio_sample_callback(self, in_data, frame_count, time_info, status): 63 | """Callback for when a new audio sample is acquired""" 64 | self._audio_data = np.fromstring(in_data, dtype=np.int16) 65 | self._invalidate_caches() 66 | self._invoke_callbacks() 67 | 68 | return (self._audio_data, pyaudio.paContinue) 69 | 70 | def _invoke_callbacks(self): 71 | """Notifies all clients of the new data""" 72 | for callback in self._callbacks: 73 | callback() 74 | 75 | def _invalidate_caches(self): 76 | """Invalidates the necessary cache""" 77 | self.volume.cache_clear() 78 | 79 | def audio_sample(self): 80 | """Returns the raw audio sample""" 81 | return self._audio_data 82 | 83 | @lru_cache(maxsize=32) 84 | def volume(self): 85 | return np.max(np.abs(self.audio_sample())) 86 | 87 | class MelbankInputSource(AudioInputSource): 88 | 89 | CONFIG_SCHEMA = vol.Schema({ 90 | vol.Optional('sample_rate', default = 60): int, 91 | vol.Optional('mic_rate', default = 44100): int, 92 | vol.Optional('window_size', default = 4): int, 93 | vol.Optional('samples', default = 24): int, 94 | vol.Optional('min_frequency', default = 20): int, 95 | vol.Optional('max_frequency', default = 18000): int, 96 | }) 97 | 98 | def __init__(self, config): 99 | self._config = self.CONFIG_SCHEMA(config) 100 | super().__init__( 101 | sample_rate = self._config['sample_rate'], 102 | mic_rate = self._config['mic_rate']) 103 | 104 | self._initialize_melbank() 105 | 106 | def _invalidate_caches(self): 107 | """Invalidates the cache for all melbank related data""" 108 | super()._invalidate_caches() 109 | self.melbank.cache_clear() 110 | self.sample_melbank.cache_clear() 111 | self.interpolated_melbank.cache_clear() 112 | 113 | def _initialize_melbank(self): 114 | """Initialize all the melbank related variables""" 115 | 116 | self._init = True 117 | self._y_roll = np.random.rand(self._config['window_size'], int(self._config['mic_rate'] / self._config['sample_rate'])) / 1e16 118 | 119 | self._fft_window = np.hamming(int((self._config['window_size'] * self._config['mic_rate']) / self._config['sample_rate'])) 120 | 121 | samples = int(self._config['mic_rate'] * self._config['window_size'] / (2.0 * self._config['sample_rate'])) 122 | self.mel_y, (_, self.mel_x) = mel.compute_melmat(num_mel_bands=self._config['samples'], 123 | freq_min=self._config['min_frequency'], 124 | freq_max=self._config['max_frequency'], 125 | num_fft_bands=samples, 126 | sample_rate=self._config['mic_rate']) 127 | self.fft_plot_filter = ExpFilter(np.tile(1e-1, self._config['samples']), alpha_decay=0.99, alpha_rise=0.99) 128 | self.mel_gain = ExpFilter(np.tile(1e-1, self._config['samples']), alpha_decay=0.01, alpha_rise=0.99) 129 | self.mel_smoothing = ExpFilter(np.tile(1e-1, self._config['samples']), alpha_decay=0.99, alpha_rise=0.99) 130 | 131 | @lru_cache(maxsize=32) 132 | def melbank(self): 133 | """Returns the raw melbank curve""" 134 | # TODO: This code was pretty much taken as is without any 135 | # changes other than some minor cleanup. Need to eventually 136 | # work out the math and fix this up and ideally use some 137 | # seconday library for the heavy lifting. 138 | 139 | # Update the rolling window and sum up the samples 140 | self._y_roll[:-1] = self._y_roll[1:] 141 | self._y_roll[-1, :] = np.copy(self.audio_sample()) 142 | y_data = np.concatenate(self._y_roll, axis=0).astype(np.float32) 143 | 144 | # Transform audio input into the frequency domain 145 | y_data *= self._fft_window 146 | 147 | # Pad with zeros until the next power of two 148 | N = len(y_data) 149 | N_zeros = 2**int(np.ceil(np.log2(N))) - N 150 | y_padded = np.pad(y_data, (0, N_zeros), mode='constant') 151 | 152 | # Construct a Mel filterbank from the FFT data 153 | YS = np.abs(np.fft.rfft(y_padded)[:N // 2]) 154 | mel = np.atleast_2d(YS).T * self.mel_y.T 155 | 156 | # Scale data to values more suitable for visualization 157 | mel = np.sum(mel, axis=0) 158 | mel = mel**1.6 159 | 160 | # Gain normalization 161 | self.mel_gain.update(np.max(gaussian_filter1d(mel, sigma=0.1))) 162 | mel /= self.mel_gain.value 163 | self.mel = self.mel_smoothing.update(mel) 164 | 165 | return self.mel 166 | 167 | @lru_cache(maxsize=32) 168 | def sample_melbank(self, hz): 169 | """Samples the melbank curve at a given frequency""" 170 | pass 171 | 172 | @lru_cache(maxsize=32) 173 | def interpolated_melbank(self, size, filtered = True): 174 | """Returns a melbank curve interpolated up to a given size""" 175 | if filtered is True: 176 | interpolated_y = self.interpolated_melbank(size, filtered=False) 177 | return self.get_filter( 178 | filter_key = "interpolated_melbank", 179 | filter_size = size, 180 | alpha_decay = 0.99, 181 | alpha_rise = 0.01).update(interpolated_y) 182 | return math.interpolate(self.melbank(), size) 183 | 184 | @lru_cache(maxsize=None) 185 | def get_filter(self, filter_key, filter_size, alpha_decay, alpha_rise): 186 | """ 187 | Gets a filter given a specific size and identifier. This 188 | is implemented as a simply param cache and does not get reset 189 | when new audio data arrives. 190 | """ 191 | _LOGGER.info("Initializing new melbank filter {}".format(filter_key)) 192 | return ExpFilter(np.tile(0.01, filter_size), alpha_decay=alpha_decay, alpha_rise=alpha_rise) 193 | 194 | 195 | # TODO: Rationalize 196 | _melbank_source = None 197 | def get_melbank_input_source(): 198 | global _melbank_source 199 | if _melbank_source is None: 200 | _melbank_source = MelbankInputSource({}) 201 | return _melbank_source 202 | 203 | @Effect.no_registration 204 | class AudioReactiveEffect(Effect): 205 | """ 206 | Base for audio reactive effects. This really just subscribes 207 | to the melbank input source and forwards input along to the 208 | subclasses. This can be expaneded to do the common r/g/b filters. 209 | """ 210 | 211 | def activate(self, channel): 212 | super().activate(channel) 213 | get_melbank_input_source().subscribe( 214 | self._audio_data_updated) 215 | 216 | def deactivate(self): 217 | get_melbank_input_source().unsubscribe( 218 | self._audio_data_updated) 219 | super().deactivate() 220 | 221 | def _audio_data_updated(self): 222 | self.audio_data_updated(get_melbank_input_source()) 223 | 224 | def audio_data_updated(self, data): 225 | """ 226 | Callback for when the audio data is updatead. Should 227 | be implemented by subclasses 228 | """ 229 | pass 230 | -------------------------------------------------------------------------------- /ledfxcontroller/frontend/css/style.min.css: -------------------------------------------------------------------------------- 1 | html{position:relative;min-height:100%}body{overflow-x:hidden}body.sticky-footer{margin-bottom:56px}body.sticky-footer .content-wrapper{min-height:calc(100vh - 56px - 56px)}body.fixed-nav{padding-top:56px}.content-wrapper{min-height:calc(100vh - 56px);padding-top:1rem}.scroll-to-top{position:fixed;right:15px;bottom:3px;display:none;width:50px;height:50px;text-align:center;color:#fff;background:rgba(52,58,64,.5);line-height:45px}.scroll-to-top:focus,.scroll-to-top:hover{color:#fff}.scroll-to-top:hover{background:#343a40}.scroll-to-top i{font-weight:800}.smaller{font-size:.7rem}.o-hidden{overflow:hidden!important}.z-0{z-index:0}.z-1{z-index:1}#mainNav .navbar-collapse{overflow:auto;max-height:75vh}#mainNav .navbar-collapse .navbar-nav .nav-item .nav-link{cursor:pointer}#mainNav .navbar-collapse .navbar-sidenav .nav-link-collapse:after{float:right;content:'\f107';font-family:FontAwesome}#mainNav .navbar-collapse .navbar-sidenav .nav-link-collapse.collapsed:after{content:'\f105'}#mainNav .navbar-collapse .navbar-sidenav .sidenav-second-level,#mainNav .navbar-collapse .navbar-sidenav .sidenav-third-level{padding-left:0}#mainNav .navbar-collapse .navbar-sidenav .sidenav-second-level>li>a,#mainNav .navbar-collapse .navbar-sidenav .sidenav-third-level>li>a{display:block;padding:.5em 0}#mainNav .navbar-collapse .navbar-sidenav .sidenav-second-level>li>a:focus,#mainNav .navbar-collapse .navbar-sidenav .sidenav-second-level>li>a:hover,#mainNav .navbar-collapse .navbar-sidenav .sidenav-third-level>li>a:focus,#mainNav .navbar-collapse .navbar-sidenav .sidenav-third-level>li>a:hover{text-decoration:none}#mainNav .navbar-collapse .navbar-sidenav .sidenav-second-level>li>a{padding-left:1em}#mainNav .navbar-collapse .navbar-sidenav .sidenav-third-level>li>a{padding-left:2em}#mainNav .navbar-collapse .sidenav-toggler{display:none}#mainNav .navbar-collapse .navbar-nav>.nav-item.dropdown>.nav-link{position:relative;min-width:45px}#mainNav .navbar-collapse .navbar-nav>.nav-item.dropdown>.nav-link:after{float:right;width:auto;content:'\f105';border:none;font-family:FontAwesome}#mainNav .navbar-collapse .navbar-nav>.nav-item.dropdown>.nav-link .indicator{position:absolute;top:5px;left:21px;font-size:10px}#mainNav .navbar-collapse .navbar-nav>.nav-item.dropdown.show>.nav-link:after{content:'\f107'}#mainNav .navbar-collapse .navbar-nav>.nav-item.dropdown .dropdown-menu>.dropdown-item>.dropdown-message{overflow:hidden;max-width:none;text-overflow:ellipsis}@media (min-width:992px){#mainNav .navbar-brand{width:250px}#mainNav .navbar-collapse{overflow:visible;max-height:none}#mainNav .navbar-collapse .navbar-sidenav{position:absolute;top:0;left:0;-webkit-flex-direction:column;-ms-flex-direction:column;flex-direction:column;margin-top:56px}#mainNav .navbar-collapse .navbar-sidenav>.nav-item{width:250px;padding:0}#mainNav .navbar-collapse .navbar-sidenav>.nav-item>.nav-link{padding:1em}#mainNav .navbar-collapse .navbar-sidenav>.nav-item .sidenav-second-level,#mainNav .navbar-collapse .navbar-sidenav>.nav-item .sidenav-third-level{padding-left:0;list-style:none}#mainNav .navbar-collapse .navbar-sidenav>.nav-item .sidenav-second-level>li,#mainNav .navbar-collapse .navbar-sidenav>.nav-item .sidenav-third-level>li{width:250px}#mainNav .navbar-collapse .navbar-sidenav>.nav-item .sidenav-second-level>li>a,#mainNav .navbar-collapse .navbar-sidenav>.nav-item .sidenav-third-level>li>a{padding:1em}#mainNav .navbar-collapse .navbar-sidenav>.nav-item .sidenav-second-level>li>a{padding-left:2.75em}#mainNav .navbar-collapse .navbar-sidenav>.nav-item .sidenav-third-level>li>a{padding-left:3.75em}#mainNav .navbar-collapse .navbar-nav>.nav-item.dropdown>.nav-link{min-width:0}#mainNav .navbar-collapse .navbar-nav>.nav-item.dropdown>.nav-link:after{width:24px;text-align:center}#mainNav .navbar-collapse .navbar-nav>.nav-item.dropdown .dropdown-menu>.dropdown-item>.dropdown-message{max-width:300px}}#mainNav.fixed-top .sidenav-toggler{display:none}@media (min-width:992px){#mainNav.fixed-top .navbar-sidenav{height:calc(100vh - 112px)}#mainNav.fixed-top .sidenav-toggler{position:absolute;top:0;left:0;display:flex;-webkit-flex-direction:column;-ms-flex-direction:column;flex-direction:column;margin-top:calc(100vh - 56px)}#mainNav.fixed-top .sidenav-toggler>.nav-item{width:250px;padding:0}#mainNav.fixed-top .sidenav-toggler>.nav-item>.nav-link{padding:1em}}#mainNav.fixed-top.navbar-dark .sidenav-toggler{background-color:#212529}#mainNav.fixed-top.navbar-dark .sidenav-toggler a i{color:#adb5bd}#mainNav.fixed-top.navbar-light .sidenav-toggler{background-color:#dee2e6}#mainNav.fixed-top.navbar-light .sidenav-toggler a i{color:rgba(0,0,0,.5)}body.sidenav-toggled #mainNav.fixed-top .sidenav-toggler{overflow-x:hidden;width:55px}body.sidenav-toggled #mainNav.fixed-top .sidenav-toggler .nav-item,body.sidenav-toggled #mainNav.fixed-top .sidenav-toggler .nav-link{width:55px!important}body.sidenav-toggled #mainNav.fixed-top #sidenavToggler i{-webkit-transform:scaleX(-1);-moz-transform:scaleX(-1);-o-transform:scaleX(-1);transform:scaleX(-1);filter:FlipH;-ms-filter:FlipH}#mainNav.static-top .sidenav-toggler{display:none}@media (min-width:992px){#mainNav.static-top .sidenav-toggler{display:flex}}body.sidenav-toggled #mainNav.static-top #sidenavToggler i{-webkit-transform:scaleX(-1);-moz-transform:scaleX(-1);-o-transform:scaleX(-1);transform:scaleX(-1);filter:FlipH;-ms-filter:FlipH}.content-wrapper{overflow-x:hidden;background:#fff}@media (min-width:992px){.content-wrapper{margin-left:250px}}#sidenavToggler i{font-weight:800}.navbar-sidenav-tooltip.show{display:none}@media (min-width:992px){body.sidenav-toggled .content-wrapper{margin-left:55px}}body.sidenav-toggled .navbar-sidenav{width:55px}body.sidenav-toggled .navbar-sidenav .nav-link-text{display:none}body.sidenav-toggled .navbar-sidenav .nav-item,body.sidenav-toggled .navbar-sidenav .nav-link{width:55px!important}body.sidenav-toggled .navbar-sidenav .nav-item:after,body.sidenav-toggled .navbar-sidenav .nav-link:after{display:none}body.sidenav-toggled .navbar-sidenav .nav-item{white-space:nowrap}body.sidenav-toggled .navbar-sidenav-tooltip.show{display:flex}#mainNav.navbar-dark .navbar-collapse .navbar-sidenav .nav-link-collapse:after{color:#868e96}#mainNav.navbar-dark .navbar-collapse .navbar-sidenav>.nav-item>.nav-link{color:#868e96}#mainNav.navbar-dark .navbar-collapse .navbar-sidenav>.nav-item>.nav-link:hover{color:#adb5bd}#mainNav.navbar-dark .navbar-collapse .navbar-sidenav>.nav-item .sidenav-second-level>li>a,#mainNav.navbar-dark .navbar-collapse .navbar-sidenav>.nav-item .sidenav-third-level>li>a{color:#868e96}#mainNav.navbar-dark .navbar-collapse .navbar-sidenav>.nav-item .sidenav-second-level>li>a:focus,#mainNav.navbar-dark .navbar-collapse .navbar-sidenav>.nav-item .sidenav-second-level>li>a:hover,#mainNav.navbar-dark .navbar-collapse .navbar-sidenav>.nav-item .sidenav-third-level>li>a:focus,#mainNav.navbar-dark .navbar-collapse .navbar-sidenav>.nav-item .sidenav-third-level>li>a:hover{color:#adb5bd}#mainNav.navbar-dark .navbar-collapse .navbar-nav>.nav-item.dropdown>.nav-link:after{color:#adb5bd}@media (min-width:992px){#mainNav.navbar-dark .navbar-collapse .navbar-sidenav{background:#343a40}#mainNav.navbar-dark .navbar-collapse .navbar-sidenav li.active a{color:#fff!important;background-color:#495057}#mainNav.navbar-dark .navbar-collapse .navbar-sidenav li.active a:focus,#mainNav.navbar-dark .navbar-collapse .navbar-sidenav li.active a:hover{color:#fff}#mainNav.navbar-dark .navbar-collapse .navbar-sidenav>.nav-item .sidenav-second-level,#mainNav.navbar-dark .navbar-collapse .navbar-sidenav>.nav-item .sidenav-third-level{background:#343a40}}#mainNav.navbar-light .navbar-collapse .navbar-sidenav .nav-link-collapse:after{color:rgba(0,0,0,.5)}#mainNav.navbar-light .navbar-collapse .navbar-sidenav>.nav-item>.nav-link{color:rgba(0,0,0,.5)}#mainNav.navbar-light .navbar-collapse .navbar-sidenav>.nav-item>.nav-link:hover{color:rgba(0,0,0,.7)}#mainNav.navbar-light .navbar-collapse .navbar-sidenav>.nav-item .sidenav-second-level>li>a,#mainNav.navbar-light .navbar-collapse .navbar-sidenav>.nav-item .sidenav-third-level>li>a{color:rgba(0,0,0,.5)}#mainNav.navbar-light .navbar-collapse .navbar-sidenav>.nav-item .sidenav-second-level>li>a:focus,#mainNav.navbar-light .navbar-collapse .navbar-sidenav>.nav-item .sidenav-second-level>li>a:hover,#mainNav.navbar-light .navbar-collapse .navbar-sidenav>.nav-item .sidenav-third-level>li>a:focus,#mainNav.navbar-light .navbar-collapse .navbar-sidenav>.nav-item .sidenav-third-level>li>a:hover{color:rgba(0,0,0,.7)}#mainNav.navbar-light .navbar-collapse .navbar-nav>.nav-item.dropdown>.nav-link:after{color:rgba(0,0,0,.5)}@media (min-width:992px){#mainNav.navbar-light .navbar-collapse .navbar-sidenav{background:#f8f9fa}#mainNav.navbar-light .navbar-collapse .navbar-sidenav li.active a{color:#000!important;background-color:#e9ecef}#mainNav.navbar-light .navbar-collapse .navbar-sidenav li.active a:focus,#mainNav.navbar-light .navbar-collapse .navbar-sidenav li.active a:hover{color:#000}#mainNav.navbar-light .navbar-collapse .navbar-sidenav>.nav-item .sidenav-second-level,#mainNav.navbar-light .navbar-collapse .navbar-sidenav>.nav-item .sidenav-third-level{background:#f8f9fa}}.card-body-icon{position:absolute;z-index:0;top:-25px;right:-25px;font-size:5rem;-webkit-transform:rotate(15deg);-ms-transform:rotate(15deg);transform:rotate(15deg)}@media (min-width:576px){.card-columns{column-count:1}}@media (min-width:768px){.card-columns{column-count:2}}@media (min-width:1200px){.card-columns{column-count:2}}.card-login{max-width:25rem}.card-register{max-width:40rem}footer.sticky-footer{position:absolute;right:0;bottom:0;width:100%;height:56px;background-color:#e9ecef;line-height:55px}@media (min-width:992px){footer.sticky-footer{width:calc(100% - 250px)}}@media (min-width:992px){body.sidenav-toggled footer.sticky-footer{width:calc(100% - 55px)}} -------------------------------------------------------------------------------- /ledfxcontroller/frontend/css/style.css: -------------------------------------------------------------------------------- 1 | html { 2 | position: relative; 3 | min-height: 100%; 4 | } 5 | 6 | body { 7 | overflow-x: hidden; 8 | } 9 | 10 | body.sticky-footer { 11 | margin-bottom: 56px; 12 | } 13 | 14 | body.sticky-footer .content-wrapper { 15 | min-height: calc(100vh - 56px - 56px); 16 | } 17 | 18 | body.fixed-nav { 19 | padding-top: 56px; 20 | } 21 | 22 | .content-wrapper { 23 | min-height: calc(100vh - 56px); 24 | padding-top: 1rem; 25 | } 26 | 27 | .scroll-to-top { 28 | position: fixed; 29 | right: 15px; 30 | bottom: 3px; 31 | display: none; 32 | width: 50px; 33 | height: 50px; 34 | text-align: center; 35 | color: white; 36 | background: rgba(52, 58, 64, 0.5); 37 | line-height: 45px; 38 | } 39 | 40 | .scroll-to-top:focus, .scroll-to-top:hover { 41 | color: white; 42 | } 43 | 44 | .scroll-to-top:hover { 45 | background: #343a40; 46 | } 47 | 48 | .scroll-to-top i { 49 | font-weight: 800; 50 | } 51 | 52 | .smaller { 53 | font-size: 0.7rem; 54 | } 55 | 56 | .o-hidden { 57 | overflow: hidden !important; 58 | } 59 | 60 | .z-0 { 61 | z-index: 0; 62 | } 63 | 64 | .z-1 { 65 | z-index: 1; 66 | } 67 | 68 | #mainNav .navbar-collapse { 69 | overflow: auto; 70 | max-height: 75vh; 71 | } 72 | 73 | #mainNav .navbar-collapse .navbar-nav .nav-item .nav-link { 74 | cursor: pointer; 75 | } 76 | 77 | #mainNav .navbar-collapse .navbar-sidenav .nav-link-collapse:after { 78 | float: right; 79 | content: '\f107'; 80 | font-family: 'FontAwesome'; 81 | } 82 | 83 | #mainNav .navbar-collapse .navbar-sidenav .nav-link-collapse.collapsed:after { 84 | content: '\f105'; 85 | } 86 | 87 | #mainNav .navbar-collapse .navbar-sidenav .sidenav-second-level, 88 | #mainNav .navbar-collapse .navbar-sidenav .sidenav-third-level { 89 | padding-left: 0; 90 | } 91 | 92 | #mainNav .navbar-collapse .navbar-sidenav .sidenav-second-level > li > a, 93 | #mainNav .navbar-collapse .navbar-sidenav .sidenav-third-level > li > a { 94 | display: block; 95 | padding: 0.5em 0; 96 | } 97 | 98 | #mainNav .navbar-collapse .navbar-sidenav .sidenav-second-level > li > a:focus, #mainNav .navbar-collapse .navbar-sidenav .sidenav-second-level > li > a:hover, 99 | #mainNav .navbar-collapse .navbar-sidenav .sidenav-third-level > li > a:focus, 100 | #mainNav .navbar-collapse .navbar-sidenav .sidenav-third-level > li > a:hover { 101 | text-decoration: none; 102 | } 103 | 104 | #mainNav .navbar-collapse .navbar-sidenav .sidenav-second-level > li > a { 105 | padding-left: 1em; 106 | } 107 | 108 | #mainNav .navbar-collapse .navbar-sidenav .sidenav-third-level > li > a { 109 | padding-left: 2em; 110 | } 111 | 112 | #mainNav .navbar-collapse .sidenav-toggler { 113 | display: none; 114 | } 115 | 116 | #mainNav .navbar-collapse .navbar-nav > .nav-item.dropdown > .nav-link { 117 | position: relative; 118 | min-width: 45px; 119 | } 120 | 121 | #mainNav .navbar-collapse .navbar-nav > .nav-item.dropdown > .nav-link:after { 122 | float: right; 123 | width: auto; 124 | content: '\f105'; 125 | border: none; 126 | font-family: 'FontAwesome'; 127 | } 128 | 129 | #mainNav .navbar-collapse .navbar-nav > .nav-item.dropdown > .nav-link .indicator { 130 | position: absolute; 131 | top: 5px; 132 | left: 21px; 133 | font-size: 10px; 134 | } 135 | 136 | #mainNav .navbar-collapse .navbar-nav > .nav-item.dropdown.show > .nav-link:after { 137 | content: '\f107'; 138 | } 139 | 140 | #mainNav .navbar-collapse .navbar-nav > .nav-item.dropdown .dropdown-menu > .dropdown-item > .dropdown-message { 141 | overflow: hidden; 142 | max-width: none; 143 | text-overflow: ellipsis; 144 | } 145 | 146 | @media (min-width: 992px) { 147 | #mainNav .navbar-brand { 148 | width: 250px; 149 | } 150 | #mainNav .navbar-collapse { 151 | overflow: visible; 152 | max-height: none; 153 | } 154 | #mainNav .navbar-collapse .navbar-sidenav { 155 | position: absolute; 156 | top: 0; 157 | left: 0; 158 | -webkit-flex-direction: column; 159 | -ms-flex-direction: column; 160 | flex-direction: column; 161 | margin-top: 56px; 162 | } 163 | #mainNav .navbar-collapse .navbar-sidenav > .nav-item { 164 | width: 250px; 165 | padding: 0; 166 | } 167 | #mainNav .navbar-collapse .navbar-sidenav > .nav-item > .nav-link { 168 | padding: 1em; 169 | } 170 | #mainNav .navbar-collapse .navbar-sidenav > .nav-item .sidenav-second-level, 171 | #mainNav .navbar-collapse .navbar-sidenav > .nav-item .sidenav-third-level { 172 | padding-left: 0; 173 | list-style: none; 174 | } 175 | #mainNav .navbar-collapse .navbar-sidenav > .nav-item .sidenav-second-level > li, 176 | #mainNav .navbar-collapse .navbar-sidenav > .nav-item .sidenav-third-level > li { 177 | width: 250px; 178 | } 179 | #mainNav .navbar-collapse .navbar-sidenav > .nav-item .sidenav-second-level > li > a, 180 | #mainNav .navbar-collapse .navbar-sidenav > .nav-item .sidenav-third-level > li > a { 181 | padding: 1em; 182 | } 183 | #mainNav .navbar-collapse .navbar-sidenav > .nav-item .sidenav-second-level > li > a { 184 | padding-left: 2.75em; 185 | } 186 | #mainNav .navbar-collapse .navbar-sidenav > .nav-item .sidenav-third-level > li > a { 187 | padding-left: 3.75em; 188 | } 189 | #mainNav .navbar-collapse .navbar-nav > .nav-item.dropdown > .nav-link { 190 | min-width: 0; 191 | } 192 | #mainNav .navbar-collapse .navbar-nav > .nav-item.dropdown > .nav-link:after { 193 | width: 24px; 194 | text-align: center; 195 | } 196 | #mainNav .navbar-collapse .navbar-nav > .nav-item.dropdown .dropdown-menu > .dropdown-item > .dropdown-message { 197 | max-width: 300px; 198 | } 199 | } 200 | 201 | #mainNav.fixed-top .sidenav-toggler { 202 | display: none; 203 | } 204 | 205 | @media (min-width: 992px) { 206 | #mainNav.fixed-top .navbar-sidenav { 207 | height: calc(100vh - 112px); 208 | } 209 | #mainNav.fixed-top .sidenav-toggler { 210 | position: absolute; 211 | top: 0; 212 | left: 0; 213 | display: flex; 214 | -webkit-flex-direction: column; 215 | -ms-flex-direction: column; 216 | flex-direction: column; 217 | margin-top: calc(100vh - 56px); 218 | } 219 | #mainNav.fixed-top .sidenav-toggler > .nav-item { 220 | width: 250px; 221 | padding: 0; 222 | } 223 | #mainNav.fixed-top .sidenav-toggler > .nav-item > .nav-link { 224 | padding: 1em; 225 | } 226 | } 227 | 228 | #mainNav.fixed-top.navbar-dark .sidenav-toggler { 229 | background-color: #212529; 230 | } 231 | 232 | #mainNav.fixed-top.navbar-dark .sidenav-toggler a i { 233 | color: #adb5bd; 234 | } 235 | 236 | #mainNav.fixed-top.navbar-light .sidenav-toggler { 237 | background-color: #dee2e6; 238 | } 239 | 240 | #mainNav.fixed-top.navbar-light .sidenav-toggler a i { 241 | color: rgba(0, 0, 0, 0.5); 242 | } 243 | 244 | body.sidenav-toggled #mainNav.fixed-top .sidenav-toggler { 245 | overflow-x: hidden; 246 | width: 55px; 247 | } 248 | 249 | body.sidenav-toggled #mainNav.fixed-top .sidenav-toggler .nav-item, 250 | body.sidenav-toggled #mainNav.fixed-top .sidenav-toggler .nav-link { 251 | width: 55px !important; 252 | } 253 | 254 | body.sidenav-toggled #mainNav.fixed-top #sidenavToggler i { 255 | -webkit-transform: scaleX(-1); 256 | -moz-transform: scaleX(-1); 257 | -o-transform: scaleX(-1); 258 | transform: scaleX(-1); 259 | filter: FlipH; 260 | -ms-filter: 'FlipH'; 261 | } 262 | 263 | #mainNav.static-top .sidenav-toggler { 264 | display: none; 265 | } 266 | 267 | @media (min-width: 992px) { 268 | #mainNav.static-top .sidenav-toggler { 269 | display: flex; 270 | } 271 | } 272 | 273 | body.sidenav-toggled #mainNav.static-top #sidenavToggler i { 274 | -webkit-transform: scaleX(-1); 275 | -moz-transform: scaleX(-1); 276 | -o-transform: scaleX(-1); 277 | transform: scaleX(-1); 278 | filter: FlipH; 279 | -ms-filter: 'FlipH'; 280 | } 281 | 282 | .content-wrapper { 283 | overflow-x: hidden; 284 | background: white; 285 | } 286 | 287 | @media (min-width: 992px) { 288 | .content-wrapper { 289 | margin-left: 250px; 290 | } 291 | } 292 | 293 | #sidenavToggler i { 294 | font-weight: 800; 295 | } 296 | 297 | .navbar-sidenav-tooltip.show { 298 | display: none; 299 | } 300 | 301 | @media (min-width: 992px) { 302 | body.sidenav-toggled .content-wrapper { 303 | margin-left: 55px; 304 | } 305 | } 306 | 307 | body.sidenav-toggled .navbar-sidenav { 308 | width: 55px; 309 | } 310 | 311 | body.sidenav-toggled .navbar-sidenav .nav-link-text { 312 | display: none; 313 | } 314 | 315 | body.sidenav-toggled .navbar-sidenav .nav-item, 316 | body.sidenav-toggled .navbar-sidenav .nav-link { 317 | width: 55px !important; 318 | } 319 | 320 | body.sidenav-toggled .navbar-sidenav .nav-item:after, 321 | body.sidenav-toggled .navbar-sidenav .nav-link:after { 322 | display: none; 323 | } 324 | 325 | body.sidenav-toggled .navbar-sidenav .nav-item { 326 | white-space: nowrap; 327 | } 328 | 329 | body.sidenav-toggled .navbar-sidenav-tooltip.show { 330 | display: flex; 331 | } 332 | 333 | #mainNav.navbar-dark .navbar-collapse .navbar-sidenav .nav-link-collapse:after { 334 | color: #868e96; 335 | } 336 | 337 | #mainNav.navbar-dark .navbar-collapse .navbar-sidenav > .nav-item > .nav-link { 338 | color: #868e96; 339 | } 340 | 341 | #mainNav.navbar-dark .navbar-collapse .navbar-sidenav > .nav-item > .nav-link:hover { 342 | color: #adb5bd; 343 | } 344 | 345 | #mainNav.navbar-dark .navbar-collapse .navbar-sidenav > .nav-item .sidenav-second-level > li > a, 346 | #mainNav.navbar-dark .navbar-collapse .navbar-sidenav > .nav-item .sidenav-third-level > li > a { 347 | color: #868e96; 348 | } 349 | 350 | #mainNav.navbar-dark .navbar-collapse .navbar-sidenav > .nav-item .sidenav-second-level > li > a:focus, #mainNav.navbar-dark .navbar-collapse .navbar-sidenav > .nav-item .sidenav-second-level > li > a:hover, 351 | #mainNav.navbar-dark .navbar-collapse .navbar-sidenav > .nav-item .sidenav-third-level > li > a:focus, 352 | #mainNav.navbar-dark .navbar-collapse .navbar-sidenav > .nav-item .sidenav-third-level > li > a:hover { 353 | color: #adb5bd; 354 | } 355 | 356 | #mainNav.navbar-dark .navbar-collapse .navbar-nav > .nav-item.dropdown > .nav-link:after { 357 | color: #adb5bd; 358 | } 359 | 360 | @media (min-width: 992px) { 361 | #mainNav.navbar-dark .navbar-collapse .navbar-sidenav { 362 | background: #343a40; 363 | } 364 | #mainNav.navbar-dark .navbar-collapse .navbar-sidenav li.active a { 365 | color: white !important; 366 | background-color: #495057; 367 | } 368 | #mainNav.navbar-dark .navbar-collapse .navbar-sidenav li.active a:focus, #mainNav.navbar-dark .navbar-collapse .navbar-sidenav li.active a:hover { 369 | color: white; 370 | } 371 | #mainNav.navbar-dark .navbar-collapse .navbar-sidenav > .nav-item .sidenav-second-level, 372 | #mainNav.navbar-dark .navbar-collapse .navbar-sidenav > .nav-item .sidenav-third-level { 373 | background: #343a40; 374 | } 375 | } 376 | 377 | #mainNav.navbar-light .navbar-collapse .navbar-sidenav .nav-link-collapse:after { 378 | color: rgba(0, 0, 0, 0.5); 379 | } 380 | 381 | #mainNav.navbar-light .navbar-collapse .navbar-sidenav > .nav-item > .nav-link { 382 | color: rgba(0, 0, 0, 0.5); 383 | } 384 | 385 | #mainNav.navbar-light .navbar-collapse .navbar-sidenav > .nav-item > .nav-link:hover { 386 | color: rgba(0, 0, 0, 0.7); 387 | } 388 | 389 | #mainNav.navbar-light .navbar-collapse .navbar-sidenav > .nav-item .sidenav-second-level > li > a, 390 | #mainNav.navbar-light .navbar-collapse .navbar-sidenav > .nav-item .sidenav-third-level > li > a { 391 | color: rgba(0, 0, 0, 0.5); 392 | } 393 | 394 | #mainNav.navbar-light .navbar-collapse .navbar-sidenav > .nav-item .sidenav-second-level > li > a:focus, #mainNav.navbar-light .navbar-collapse .navbar-sidenav > .nav-item .sidenav-second-level > li > a:hover, 395 | #mainNav.navbar-light .navbar-collapse .navbar-sidenav > .nav-item .sidenav-third-level > li > a:focus, 396 | #mainNav.navbar-light .navbar-collapse .navbar-sidenav > .nav-item .sidenav-third-level > li > a:hover { 397 | color: rgba(0, 0, 0, 0.7); 398 | } 399 | 400 | #mainNav.navbar-light .navbar-collapse .navbar-nav > .nav-item.dropdown > .nav-link:after { 401 | color: rgba(0, 0, 0, 0.5); 402 | } 403 | 404 | @media (min-width: 992px) { 405 | #mainNav.navbar-light .navbar-collapse .navbar-sidenav { 406 | background: #f8f9fa; 407 | } 408 | #mainNav.navbar-light .navbar-collapse .navbar-sidenav li.active a { 409 | color: #000 !important; 410 | background-color: #e9ecef; 411 | } 412 | #mainNav.navbar-light .navbar-collapse .navbar-sidenav li.active a:focus, #mainNav.navbar-light .navbar-collapse .navbar-sidenav li.active a:hover { 413 | color: #000; 414 | } 415 | #mainNav.navbar-light .navbar-collapse .navbar-sidenav > .nav-item .sidenav-second-level, 416 | #mainNav.navbar-light .navbar-collapse .navbar-sidenav > .nav-item .sidenav-third-level { 417 | background: #f8f9fa; 418 | } 419 | } 420 | 421 | .card-body-icon { 422 | position: absolute; 423 | z-index: 0; 424 | top: -25px; 425 | right: -25px; 426 | font-size: 5rem; 427 | -webkit-transform: rotate(15deg); 428 | -ms-transform: rotate(15deg); 429 | transform: rotate(15deg); 430 | } 431 | 432 | @media (min-width: 576px) { 433 | .card-columns { 434 | column-count: 1; 435 | } 436 | } 437 | 438 | @media (min-width: 768px) { 439 | .card-columns { 440 | column-count: 2; 441 | } 442 | } 443 | 444 | @media (min-width: 1200px) { 445 | .card-columns { 446 | column-count: 2; 447 | } 448 | } 449 | 450 | .card-login { 451 | max-width: 25rem; 452 | } 453 | 454 | .card-register { 455 | max-width: 40rem; 456 | } 457 | 458 | footer.sticky-footer { 459 | position: absolute; 460 | right: 0; 461 | bottom: 0; 462 | width: 100%; 463 | height: 56px; 464 | background-color: #e9ecef; 465 | line-height: 55px; 466 | } 467 | 468 | @media (min-width: 992px) { 469 | footer.sticky-footer { 470 | width: calc(100% - 250px); 471 | } 472 | } 473 | 474 | @media (min-width: 992px) { 475 | body.sidenav-toggled footer.sticky-footer { 476 | width: calc(100% - 55px); 477 | } 478 | } 479 | -------------------------------------------------------------------------------- /ledfxcontroller/frontend/external/font-awesome/css/font-awesome.css.map: -------------------------------------------------------------------------------- 1 | { 2 | "version": 3, 3 | "mappings": "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", 4 | "sources": ["../scss/_path.scss","../scss/_core.scss","../scss/_larger.scss","../scss/_fixed-width.scss","../scss/_list.scss","../scss/_variables.scss","../scss/_bordered-pulled.scss","../scss/_animated.scss","../scss/_rotated-flipped.scss","../scss/_mixins.scss","../scss/_stacked.scss","../scss/_icons.scss"], 5 | "names": [], 6 | "file": "font-awesome.css" 7 | } 8 | --------------------------------------------------------------------------------