diff --git a/pyleds/lib/Litsimaja.py b/pyleds/lib/Litsimaja.py index 422426a..e366dc9 100644 --- a/pyleds/lib/Litsimaja.py +++ b/pyleds/lib/Litsimaja.py @@ -3,17 +3,16 @@ from rpi_ws281x import PixelStrip # from lib.strip.TkinterStrip import TkinterStrip from lib.LoopSwitch import LoopSwitch from lib.Regions import Regions +from lib.Tempo import Tempo class Litsimaja(object): - _loops: [] - _regions: Regions - def __init__(self): self._strip = PixelStrip(290, 18, 800000, 10, False, 255, 0, 4104) self._loops = [] self._strip.begin() self._regions: Regions = Regions(self.count_pixels(), [46, 96, 191, 241]) + self._tempo: Tempo = Tempo(60) def count_pixels(self) -> int: return self._strip.numPixels() @@ -38,9 +37,26 @@ class Litsimaja(object): for loop in self._loops: loop.stop() 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 = { + '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 diff --git a/pyleds/lib/Regions.py b/pyleds/lib/Regions.py index 7fa07ce..9bdcc43 100644 --- a/pyleds/lib/Regions.py +++ b/pyleds/lib/Regions.py @@ -21,9 +21,12 @@ class Regions(object): self._pixelsEnabled[i] = enabled self._regions[region_id][2] = enabled - def is_pixel_enabled(self, pixel_id: int): + 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) diff --git a/pyleds/lib/Tempo.py b/pyleds/lib/Tempo.py new file mode 100644 index 0000000..bdae570 --- /dev/null +++ b/pyleds/lib/Tempo.py @@ -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 diff --git a/pyleds/program/peter/DiskoPidu.py b/pyleds/program/peter/DiskoPidu.py index 7f5baf5..c8fc06d 100644 --- a/pyleds/program/peter/DiskoPidu.py +++ b/pyleds/program/peter/DiskoPidu.py @@ -30,7 +30,8 @@ class DiskoPidu(Program): loop = args['loop'] while self.get_loop().status(): - self.disco(10, 500) + self._lm.get_tempo().wait() + self.disco(10, 0) if not loop: break diff --git a/pyleds/program/siinus/HzTick.py b/pyleds/program/siinus/HzTick.py index 7c9e3a0..79ea2de 100644 --- a/pyleds/program/siinus/HzTick.py +++ b/pyleds/program/siinus/HzTick.py @@ -9,6 +9,7 @@ def name(): # 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 @@ -17,8 +18,5 @@ class HzTick(Program): 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() - - # Needs accurate time calculation to tick exactly when real second is passed - time.sleep(1) + tempo.wait() diff --git a/pyleds/run.py b/pyleds/run.py index ca0bc3e..7a46813 100755 --- a/pyleds/run.py +++ b/pyleds/run.py @@ -4,7 +4,8 @@ import sys import lib.ProgramLoading as Pl from lib.Litsimaja import Litsimaja -from flask import Flask, request, Response, render_template +from flask import Flask, request, Response, render_template, json +from flask_accept import accept # logging logger = logging.getLogger('litsimaja') @@ -20,27 +21,35 @@ logger.addHandler(stdout_handler) # start litsimaja lm = Litsimaja() 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']) -def respondroot(): +@accept('text/html') +def respond_root(): return render_template( 'index.html', programs=Pl.list_all(True), regions=lm.get_region_ids(), + status=lm.build_status_array() ) -@app.route('/run') -def run(): - Pl.run('siinus', 'MyProgram', lm, logger, {'color': [50, 50, 50]}) - return Response(status=200) +@app.route('/status', methods=['GET']) +@accept('application/json') +def respond_status(): + return lm_standard_xhr_response() @app.route('/crash', methods=['GET']) def crash(): lm.clear_loops() - return Response(status=200) + return lm_standard_xhr_response() @app.route('/program/', methods=['POST']) @@ -48,13 +57,26 @@ def run_program(program): args = request.get_json(force=True) prg = program.split('.') Pl.run(prg[0], prg[1], lm, logger, args) - return Response(status=200) + return lm_standard_xhr_response() @app.route('/region/', methods=['GET']) def switch_region(region): lm.switch_region(int(region)) - return Response(status=200) + return lm_standard_xhr_response() + + +@app.route('/tempo/set/', 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) diff --git a/pyleds/templates/custom.css b/pyleds/templates/custom.css index 00fd597..6f876f4 100644 --- a/pyleds/templates/custom.css +++ b/pyleds/templates/custom.css @@ -27,4 +27,12 @@ select { .spacer-row { height: 2rem; -} \ No newline at end of file +} + +button.region { + padding: 0 15; +} + +button.region_off { + background-color: gray !important +} diff --git a/pyleds/templates/index.html b/pyleds/templates/index.html index 43d5d5c..a73790d 100644 --- a/pyleds/templates/index.html +++ b/pyleds/templates/index.html @@ -21,9 +21,11 @@ show_loading(true); var http = new XMLHttpRequest(); http.open('GET', url, true); + http.setRequestHeader('accept', 'application/json'); http.onreadystatechange = function() { if (http.readyState == 4 && http.status == 200) { show_loading(false); + updateStatus(http.responseText); } }; http.send(); @@ -34,6 +36,7 @@ var http = new XMLHttpRequest(); http.open('POST', url, true); http.setRequestHeader('Content-Type', 'application/json'); + http.setRequestHeader('accept', 'application/json'); http.onreadystatechange = function() { if (http.readyState == 4 && http.status == 200) { show_loading(false); @@ -43,6 +46,20 @@ http.send(json); } + function updateStatus(json) { + let status = JSON.parse(json); + document.getElementById('tempo').value = status.features.tempo.bpm; + let regions = status.features.region; + let regLen = regions.length; + for(i = 0; i < regLen; i++) { + if (regions[i]) { + document.getElementById('region_' + i).classList.remove('region_off'); + } else { + document.getElementById('region_' + i).classList.add('region_off'); + } + } + } + function getProgramSelection() { let select = document.getElementById('program'); return select.value; @@ -81,16 +98,24 @@
+ - + +
+
+ +
-

Zone switch

{% for region in regions %} - + {% set region_off = '' %} + {% if not status.features.region[region] %} + {% set region_off = ' region_off' %} + {% endif %} + {% endfor %}
@@ -111,8 +136,9 @@
+