Compare commits

...

16 Commits

  1. 17
      README.md
  2. 17
      pyleds/.env.defaults
  3. 2
      pyleds/.env.example
  4. 1
      pyleds/.gitignore
  5. 38
      pyleds/lib/Config.py
  6. 61
      pyleds/lib/Litsimaja.py
  7. 1
      pyleds/lib/ProgramLoading.py
  8. 38
      pyleds/lib/Regions.py
  9. 45
      pyleds/lib/Tempo.py
  10. 3
      pyleds/program/peter/DiskoPidu.py
  11. 35
      pyleds/program/siinus/Gaynbow.py
  12. 22
      pyleds/program/siinus/HzTick.py
  13. 61
      pyleds/run.py
  14. 41
      pyleds/templates/custom.css
  15. 105
      pyleds/templates/index.html

@ -8,20 +8,9 @@ Projekt Litsimaja - Lapikute tagatoa seintele programmeeritavad ARGB ribad
#### Running with emulation #### Running with emulation
This is mainly for testing, development. This is mainly for testing, development.
In ``lib/Litsimaja.py`` change the following: Create .env file:
``` ```cp .env.example .env```
# from lib.strip.TkinterStrip import TkinterStrip
def __init__(self): and see: ```USE_EMULATOR```
self._strip = PixelStrip(290, 18, 800000, 10, False, 255, 0, 4104)
```
to
```
from lib.strip.TkinterStrip import TkinterStrip
def __init__(self):
self._strip = TkinterStrip(290, 18, 800000, 10, False, 255, 0, 4104)
```
Now when you run the program, you will see a Tkinter window pop up with a rectangle simulating the LED strip. Now when you run the program, you will see a Tkinter window pop up with a rectangle simulating the LED strip.
Don't commit this change.

@ -0,0 +1,17 @@
ENVIRONMENT=prod
USE_EMULATOR=false
BIND_ADDR=127.0.0.1
BIND_PORT=8080
# Strip configuration
STRIP_PIXELS=290
STRIP_GPIO_PIN=18
STRIP_HZ=800000
STRIP_DMA=10
STRIP_INVERT=false
STRIP_BRIGHTNESS=255
STRIP_CHANNEL=0
STRIP_TYPE=4104
REGIONS=46,96,191,241
BPM_DEFAULT=60

@ -0,0 +1,2 @@
ENVIRONMENT=dev
USE_EMULATOR=true

1
pyleds/.gitignore vendored

@ -1 +1,2 @@
.env
litsimaja.log litsimaja.log

@ -0,0 +1,38 @@
from dotenv import dotenv_values
import pathlib
def populate_values(load: {}):
conf = load
conf['IS_DEV']: bool = str.lower(load['ENVIRONMENT']) == 'dev'
conf['USE_EMULATOR']: bool = str.lower(load['USE_EMULATOR']) == 'true'
conf['BIND_PORT']: int = int(load['BIND_PORT'])
conf['STRIP_PIXELS']: int = int(load['STRIP_PIXELS'])
conf['STRIP_GPIO_PIN']: int = int(load['STRIP_GPIO_PIN'])
conf['STRIP_HZ']: int = int(load['STRIP_HZ'])
conf['STRIP_DMA']: int = int(load['STRIP_DMA'])
conf['STRIP_INVERT']: bool = str.lower(load['STRIP_INVERT']) == 'true'
conf['STRIP_BRIGHTNESS']: int = int(load['STRIP_BRIGHTNESS'])
conf['STRIP_CHANNEL']: int = int(load['STRIP_CHANNEL'])
conf['STRIP_TYPE']: int = int(load['STRIP_TYPE'])
conf['REGIONS']: [] = [int(x) for x in load['REGIONS'].split(',')]
conf['BPM_DEFAULT']: int = int(load['BPM_DEFAULT'])
return conf
class Config(object):
_config: {} = {}
def __init__(self):
rp = str(pathlib.Path(__file__).parent.parent.absolute())
load_conf = {
**dotenv_values(rp + "/.env.defaults"),
**dotenv_values(rp + "/.env"),
}
self._config = populate_values(load_conf)
def get(self, key: str):
return self._config[key]

@ -1,16 +1,30 @@
from rpi_ws281x import PixelStrip from lib.Config import Config
# from lib.strip.WindowStrip import WindowStrip
# from lib.strip.TkinterStrip import TkinterStrip
from lib.LoopSwitch import LoopSwitch from lib.LoopSwitch import LoopSwitch
from lib.Regions import Regions
from lib.Tempo import Tempo
class Litsimaja(object): class Litsimaja(object):
_loops: []
def __init__(self): def __init__(self):
self._strip = PixelStrip(290, 18, 800000, 10, False, 255, 0, 4104) self._config = Config()
if self.conf('USE_EMULATOR'):
module = __import__('lib.strip.TkinterStrip', None, None, ['TkinterStrip'])
class_name = 'TkinterStrip'
else:
module = __import__('rpi_ws281x', None, None, ['PixelStrip'])
class_name = 'PixelStrip'
loaded_class = getattr(module, class_name)
self._strip = loaded_class(
self.conf('STRIP_PIXELS'), self.conf('STRIP_GPIO_PIN'), self.conf('STRIP_HZ'), self.conf('STRIP_DMA'),
self.conf('STRIP_INVERT'), self.conf('STRIP_BRIGHTNESS'), self.conf('STRIP_CHANNEL'),
self.conf('STRIP_TYPE')
)
self._loops = [] self._loops = []
self._strip.begin() self._strip.begin()
self._regions: Regions = Regions(self.count_pixels(), self.conf('REGIONS'))
self._tempo: Tempo = Tempo(self.conf('BPM_DEFAULT'))
self._selected_program = None
def count_pixels(self) -> int: def count_pixels(self) -> int:
return self._strip.numPixels() return self._strip.numPixels()
@ -19,7 +33,10 @@ class Litsimaja(object):
return self._strip return self._strip
def set_pixel_color(self, n: int, color: int) -> None: def set_pixel_color(self, n: int, color: int) -> None:
self._strip.setPixelColor(n, color) if self._regions.is_pixel_enabled(n):
self._strip.setPixelColor(n, color)
else:
self._strip.setPixelColor(n, 0)
def show(self) -> None: def show(self) -> None:
self._strip.show() self._strip.show()
@ -32,3 +49,33 @@ class Litsimaja(object):
for loop in self._loops: for loop in self._loops:
loop.stop() loop.stop()
self._loops.clear() self._loops.clear()
def switch_region(self, region_id: int):
self._regions.switch_region(region_id)
def get_region_ids(self):
return self._regions.list_region_ids()
def build_status_array(self):
data = {'success': True}
features = {
'program': self._selected_program,
'tempo': {
'bpm': self.get_tempo().get_bpm()
},
}
regions = []
for region_id in self._regions.list_region_ids():
regions.append(self._regions.is_region_enabled(region_id))
features['region'] = regions
data['features'] = features
return data
def get_tempo(self):
return self._tempo
def set_selected_program(self, program_name: str):
self._selected_program = program_name
def conf(self, key: str):
return self._config.get(key)

@ -15,6 +15,7 @@ def run(namespace: str, class_name: str, lm: Litsimaja, logger, args: [] = None)
program = loaded_class(lm) program = loaded_class(lm)
logger.info('Loaded "' + module.name() + '" from ' + namespace + '.' + class_name + ' with args: ' + repr(args)) logger.info('Loaded "' + module.name() + '" from ' + namespace + '.' + class_name + ' with args: ' + repr(args))
lm.add_loop(program.get_loop()) lm.add_loop(program.get_loop())
lm.set_selected_program(namespace + '.' + class_name)
program.run(args) program.run(args)

@ -0,0 +1,38 @@
class Regions(object):
_pixelsEnabled: [] = []
_regions: [] = []
def __init__(self, strip_length: int, splitters: []):
self._length: int = strip_length
start_pixel = 0
for i in splitters:
if i > self._length:
raise ValueError('splitter out of bounds, you idiot')
self._regions.append([start_pixel, i + 1, True])
start_pixel = i + 1
self._regions.append([start_pixel, self._length, True])
for i in range(self._length):
self._pixelsEnabled.append(True)
def region_switch(self, region_id: int, enabled: bool):
pixel_region = self._regions[region_id]
for i in range(pixel_region[0], pixel_region[1]):
self._pixelsEnabled[i] = enabled
self._regions[region_id][2] = enabled
def is_pixel_enabled(self, pixel_id: int) -> bool:
return self._pixelsEnabled[pixel_id]
def is_region_enabled(self, region_id: int) -> bool:
return self._regions[region_id][2]
def switch_region(self, region_id: int):
status: bool = self._regions[region_id][2]
self.region_switch(region_id, not status)
def list_region_ids(self):
result = []
for i in range(len(self._regions)):
result.append(i)
return result

@ -0,0 +1,45 @@
import time
from typing import Optional
class Tempo(object):
_last_beat: float
_override_beat: Optional[float] = None
def __init__(self, bpm: float = 60):
self._bpm: float = bpm
self.update_last_beat()
def set_bpm(self, bpm: float) -> None:
self._bpm = bpm
def get_bpm(self) -> float:
return self._bpm
def update_last_beat(self) -> None:
self._last_beat = time.time()
def get_beat_time(self) -> float:
return 1000 / (self._bpm / 60)
def get_beat_delay(self) -> Optional[float]:
tick = self._override_beat
if tick is None:
return None
self._override_beat = None
return tick - self._last_beat
def sync_beat(self):
self._override_beat = time.time()
def wait(self, beat_divider: int = 1):
now = time.time()
next_beat = self._last_beat + (self.get_beat_time() / beat_divider / 1000)
wait_time = next_beat - now
beat_delay = self.get_beat_delay()
if beat_delay is not None:
wait_time += beat_delay
if wait_time > 0:
time.sleep(wait_time)
self.update_last_beat()
return True

@ -30,7 +30,8 @@ class DiskoPidu(Program):
loop = args['loop'] loop = args['loop']
while self.get_loop().status(): while self.get_loop().status():
self.disco(10, 500) self._lm.get_tempo().wait()
self.disco(10, 0)
if not loop: if not loop:
break break

@ -0,0 +1,35 @@
from lib.Program import Program
from rpi_ws281x import Color
import time
def name():
return 'RGBT Gaynbow'
def wheel(pos):
"""Generate rainbow colors across 0-255 positions."""
if pos < 85:
return Color(pos * 3, 255 - pos * 3, 0)
elif pos < 170:
pos -= 85
return Color(255 - pos * 3, 0, pos * 3)
else:
pos -= 170
return Color(0, pos * 3, 255 - pos * 3)
class Gaynbow(Program):
def run(self, args: [] = None):
wait_ms = 20
iterations = 5
while self.get_loop().status():
"""Draw rainbow that uniformly distributes itself across all pixels."""
for j in range(256 * iterations):
if not self.get_loop().status():
break
for i in range(self._lm.count_pixels()):
self._lm.set_pixel_color(i, wheel(
(int(i * 256 / self._lm.count_pixels()) + j) & 255))
self._lm.show()
time.sleep(wait_ms / 1000.0)

@ -0,0 +1,22 @@
from lib.Program import Program
import time
def name():
return '1Hz tick'
# Tick seconds like analog clock
class HzTick(Program):
def run(self, args: [] = None) -> None:
tempo = self._lm.get_tempo()
while self.get_loop().status():
second = time.localtime().tm_sec
second_pixel = int(self._lm.count_pixels() / 60 * second)
color_arr = args['color']
for i in range(self._lm.count_pixels()):
self._lm.set_pixel_color(i, (color_arr[0] << 16) | (color_arr[1] << 8) | color_arr[2])
self._lm.set_pixel_color(second_pixel, 255)
self._lm.show()
tempo.wait()

@ -1,42 +1,54 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
import logging import logging
import pathlib
import sys import sys
import lib.ProgramLoading as Pl import lib.ProgramLoading as Pl
from lib.Litsimaja import Litsimaja from lib.Litsimaja import Litsimaja
from flask import Flask, request, Response, render_template from flask import Flask, request, Response, render_template, json
root_path = pathlib.Path(__file__).parent.absolute()
# start litsimaja
lm = Litsimaja()
# logging # logging
logger = logging.getLogger('litsimaja') logger = logging.getLogger('litsimaja')
logger.setLevel(logging.DEBUG) logger.setLevel(logging.DEBUG if lm.conf('IS_DEV') else logging.WARN)
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
file_handler = logging.FileHandler('litsimaja.log') file_handler = logging.FileHandler(str(root_path) + '/litsimaja.log')
file_handler.setFormatter(formatter) file_handler.setFormatter(formatter)
logger.addHandler(file_handler) logger.addHandler(file_handler)
stdout_handler = logging.StreamHandler(sys.stdout) stdout_handler = logging.StreamHandler(sys.stdout)
stdout_handler.setFormatter(formatter) stdout_handler.setFormatter(formatter)
logger.addHandler(stdout_handler) logger.addHandler(stdout_handler)
# start litsimaja
lm = Litsimaja()
app = Flask(__name__, static_url_path='', static_folder='templates') app = Flask(__name__, static_url_path='', static_folder='templates')
Pl.run('siinus', 'Wipes', lm, logger, {'color': [0, 0, 0]})
Pl.run('peter', 'DiskoPidu', lm, logger, {})
def lm_standard_xhr_response() -> Response:
return Response(response=json.dumps(lm.build_status_array()), status=200, mimetype='application/json')
@app.route('/', methods=['GET']) @app.route('/', methods=['GET'])
def respondroot(): def respond_root():
return render_template('index.html', programs=Pl.list_all(True)) return render_template(
'index.html',
programs=Pl.list_all(True),
regions=lm.get_region_ids(),
status=lm.build_status_array()
)
@app.route('/run') @app.route('/status', methods=['GET'])
def run(): def respond_status():
Pl.run('siinus', 'MyProgram', lm, logger, {'color': [50, 50, 50]}) return lm_standard_xhr_response()
return Response(status=200)
@app.route('/crash', methods=['GET']) @app.route('/crash', methods=['GET'])
def crash(): def crash():
lm.clear_loops() lm.clear_loops()
return Response(status=200) return lm_standard_xhr_response()
@app.route('/program/<program>', methods=['POST']) @app.route('/program/<program>', methods=['POST'])
@ -44,7 +56,26 @@ def run_program(program):
args = request.get_json(force=True) args = request.get_json(force=True)
prg = program.split('.') prg = program.split('.')
Pl.run(prg[0], prg[1], lm, logger, args) Pl.run(prg[0], prg[1], lm, logger, args)
return Response(status=200) return lm_standard_xhr_response()
@app.route('/region/<region>', methods=['GET'])
def switch_region(region):
lm.switch_region(int(region))
return lm_standard_xhr_response()
@app.route('/tempo/set/<float:bpm>', methods=['GET'])
def set_tempo(bpm: float):
tempo = lm.get_tempo()
tempo.set_bpm(bpm)
return lm_standard_xhr_response()
@app.route('/tempo/sync', methods=['GET'])
def sync_beat():
lm.get_tempo().sync_beat()
return lm_standard_xhr_response()
app.run('0.0.0.0', 8080) app.run(lm.conf('BIND_ADDR'), lm.conf('BIND_PORT'))

@ -1,13 +1,3 @@
.section-rgb {
height: 20rem;
text-align: center;
}
select {
width: 100%;
height: calc(100% - 1.5rem);
}
.colorpicker { .colorpicker {
display: inline-block; display: inline-block;
height: 38px; height: 38px;
@ -27,4 +17,33 @@ select {
.spacer-row { .spacer-row {
height: 2rem; height: 2rem;
} }
button.region {
padding: 0 15;
}
button.region_off {
background-color: gray !important
}
div.program_select {
height: 300px;
overflow: auto;
border: 1px solid gray;
text-align: left;
}
div.program_select ul {
list-style: none;
border-bottom: 1px solid silver;
margin-bottom: 0;
}
div.program_select ul li {
margin: auto;
}
div.program_select ul li label {
margin: 3px auto;
}

@ -7,11 +7,16 @@
<link rel="stylesheet" href="custom.css"> <link rel="stylesheet" href="custom.css">
<script type="text/javascript"> <script type="text/javascript">
const hexToRgb = hex => 'use strict';
hex.replace(/^#?([a-f\d])([a-f\d])([a-f\d])$/i function hexToRgb(value) {
,(m, r, g, b) => '#' + r + r + g + g + b + b) var aRgbHex = value.substring(1).match(/.{1,2}/g);
.substring(1).match(/.{2}/g) var aRgb = [
.map(x => parseInt(x, 16)); parseInt(aRgbHex[0], 16),
parseInt(aRgbHex[1], 16),
parseInt(aRgbHex[2], 16)
];
return aRgb;
}
function show_loading(visible) { function show_loading(visible) {
document.getElementById('loading').style.visibility = visible ? 'visible' : 'hidden'; document.getElementById('loading').style.visibility = visible ? 'visible' : 'hidden';
@ -21,9 +26,11 @@
show_loading(true); show_loading(true);
var http = new XMLHttpRequest(); var http = new XMLHttpRequest();
http.open('GET', url, true); http.open('GET', url, true);
http.setRequestHeader('accept', 'application/json');
http.onreadystatechange = function() { http.onreadystatechange = function() {
if (http.readyState == 4 && http.status == 200) { if (http.readyState == 4 && http.status == 200) {
show_loading(false); show_loading(false);
updateStatus(http.responseText);
} }
}; };
http.send(); http.send();
@ -34,6 +41,7 @@
var http = new XMLHttpRequest(); var http = new XMLHttpRequest();
http.open('POST', url, true); http.open('POST', url, true);
http.setRequestHeader('Content-Type', 'application/json'); http.setRequestHeader('Content-Type', 'application/json');
http.setRequestHeader('accept', 'application/json');
http.onreadystatechange = function() { http.onreadystatechange = function() {
if (http.readyState == 4 && http.status == 200) { if (http.readyState == 4 && http.status == 200) {
show_loading(false); show_loading(false);
@ -43,9 +51,33 @@
http.send(json); http.send(json);
} }
function updateStatus(json) {
let status = JSON.parse(json);
document.getElementById('tempo').value = status.features.tempo.bpm;
let regions = status.features.region;
for (let i = 0, len = regions.length; i < len; i++) {
if (regions[i]) {
document.getElementById('region_' + i).classList.remove('region_off');
} else {
document.getElementById('region_' + i).classList.add('region_off');
}
}
let programs = document.getElementsByName('program_select');
for (var i = 0, len = programs.length; i < len; i++) {
if (programs[i].value === status.features.program) {
programs[i].checked = true;
}
}
}
function getProgramSelection() { function getProgramSelection() {
let select = document.getElementById('program'); let radios = document.getElementsByName('program_select');
return select.value; for (var i = 0, length = radios.length; i < length; i++) {
if (radios[i].checked) {
return radios[i].value;
}
}
return null;
} }
function isLoop() { function isLoop() {
@ -70,6 +102,10 @@
function doCancel() { function doCancel() {
send_get('/crash'); send_get('/crash');
} }
function switchRegion(region_id) {
send_get('/region/' + region_id);
}
</script> </script>
</head> </head>
@ -77,32 +113,53 @@
<div class="spacer-row"></div> <div class="spacer-row"></div>
<div class="container"> <div class="container">
<div class="row"> <div class="row">
<button onclick="doInit()">init</button> <button onclick="send_get('/status')">get</button>
<button onclick="doCancel()">end loop</button> <button onclick="doInit()">intro</button>
<button onclick="doBlind()">off</button> <button onclick="doCancel()">halt</button>
<button onclick="doBlind()">leds off</button>
<label><input type="checkbox" id="looping" /><b> loop</b></label> <label><input type="checkbox" id="looping" checked/><b> loop</b></label>
</div>
<div class="row">
<input id="tempo" type="number" step="0.1" onchange="send_get('/tempo/set/' + parseFloat(this.value).toFixed(2))" value="{{ status.features.tempo.bpm }}">
<button onclick="send_get('/tempo/sync')">sync</button>
</div>
<div class="row">
{% for region in regions %}
{% set region_off = '' %}
{% if not status.features.region[region] %}
{% set region_off = ' region_off' %}
{% endif %}
<button id="region_{{ region }}" class="region{{ region_off }}" onclick="switchRegion({{ region }})">zone {{ region }}</button>
{% endfor %}
</div> </div>
<div class="row" id='loading' style="visibility: hidden"><b>LOADING!</b></div> <div class="row" id='loading' style="visibility: hidden"><b>LOADING!</b></div>
<div class="section-rgb"> <div class="row">
<div class="row"> <div class="program_select one-half column">
<div class="one-third column"> {% for group in programs %}
<select id="program" size="10"> <ul>
{% for group in programs %} <li>{{ group.group }}</li>
<optgroup label="{{ group.group }}"> {% for program in group.programs %}
{% for program in group.programs %} {% set pr_checked = '' %}
<option value="{{ program.prg }}">{{ program.name }}</option> {% if status.features.program == program.prg %}
{% endfor %} {% set pr_checked = ' checked' %}
</optgroup> {% endif %}
<li>
<label>
<input type="radio" name="program_select" value="{{ program.prg }}"{{ pr_checked }}>
{{ program.name }}
</label>
</li>
{% endfor %} {% endfor %}
</select> </ul>
</div> {% endfor %}
</div> </div>
</div> </div>
<div class="row"> <div class="row">
<div class ="one-third column"> <div class ="one-third column">
<input class="colorpicker" type="color" value="#ff0000" oninput="isChanged(this.value)"> <input id="colorpicker" class="colorpicker" type="color" value="#ff0000" oninput="isChanged(this.value)">
</div> </div>
<button onclick="isChanged(document.getElementById('colorpicker').value)">Set</button>
</div> </div>
</div> </div>
</body> </body>

Loading…
Cancel
Save