diff options
-rwxr-xr-x | prog/jelka/chroot.sh | 16 | ||||
-rwxr-xr-x | prog/jelka/daemon.py | 102 | ||||
-rw-r--r-- | prog/jelka/jelka.py | 18 | ||||
-rw-r--r-- | prog/jelka/jelka_config.py | 1 | ||||
-rw-r--r-- | prog/jelka/jelka_hardware.py | 39 | ||||
-rw-r--r-- | prog/jelka/templates/index.html | 88 | ||||
-rwxr-xr-x | prog/jelka/vzorci/anton.py | 13 | ||||
-rwxr-xr-x | prog/jelka/vzorci/nov_vzorec.py | 14 | ||||
-rwxr-xr-x | prog/jelka/vzorci/vzorec.py | 13 | ||||
-rwxr-xr-x | prog/jelka/wrapper.py | 60 |
10 files changed, 364 insertions, 0 deletions
diff --git a/prog/jelka/chroot.sh b/prog/jelka/chroot.sh new file mode 100755 index 0000000..3d92d2c --- /dev/null +++ b/prog/jelka/chroot.sh @@ -0,0 +1,16 @@ +#!/bin/sh +set -xeuo pipefail +mount -t tmpfs -o nr_blocks=1,mode=0755 tmp $2 +if [ x$1 = xstart ] +then + for i in /bin /dev/null /etc /lib /lib64 /usr + do + [ -d $i ] && mkdir -p $2/$i || { mkdir -p $2/`rev <<<$i | cut -d/ -f2- | rev` && touch $2/$i; } + mount --bind -onosuid,ro $i $2/$i + done + mkdir -p $2/jelka + mount --bind -onosuid,ro . $2/jelka + mkdir -p $2/dev/shm +else + mount | grep $2 | cut -d\ -f3 | xargs -I '{}' umount '{}' +fi diff --git a/prog/jelka/daemon.py b/prog/jelka/daemon.py new file mode 100755 index 0000000..a288f35 --- /dev/null +++ b/prog/jelka/daemon.py @@ -0,0 +1,102 @@ +#!/usr/bin/python3 +import flask +import threading +import os +import secrets +import subprocess +import hashlib +import base64 +import time + +http = flask.Flask(__name__) +onemogočeni = set(["onemogočen.py"]) +skrivnost = secrets.token_bytes(8) +čas_vzorca = 5 +proces = None +naslednji = None + +def trenuten(): + if proces: + return proces.args[1] + return None + +def http_event_stream(): + while sprememba_stanja.wait(): + print("geslo je: " + kode()[0]) + sprememba_stanja.clear() + yield "data: " + http.json.dumps({"čas": čas_vzorca, "vzorci": os.listdir("vzorci"), "trenuten": trenuten(), "onemogočeni": [x for x in onemogočeni]}) + "\n\n" + +@http.route("/vzorci/<vzorec>", methods=["GET"]) +def vzorec(vzorec): + if "/" in vzorec: + return flask.Response(http.json.dumps({"napaka": {"koda": 3, "besedilo": "vzorec ne sme vsebovati poševnice"}}), status=403) + return flask.send_file("vzorci/" + vzorec, mimetype="text/x-python") + +@http.route("/", methods=["GET"]) +def index(): + return flask.render_template("index.html"); + +@http.route("/stream/", methods=["GET"]) +def stream(): + return flask.Response(http_event_stream(), headers={"Access-Control-Allow-Origin": "*", "Content-Type": "text/event-stream"}) + +@http.route("/<koda>", methods=["POST"]) +def update(koda): + global onemogočeni + global čas_vzorca + global naslednji + if koda not in kode(): + return flask.Response(http.json.dumps({"napaka": {"koda": 1, "besedilo": "napačna koda"}}), status=200) + data = flask.json.loads(flask.request.data) + if "omogoči" in data.keys(): + onemogočeni -= set(data["omogoči"]) + if "onemogoči" in data.keys(): + onemogočeni |= set(data["onemogoči"]) + if "čas" in data.keys(): + if data["čas"] < 1 or data["čas"] > 300: + return flask.Response(http.json.dumps({"napaka": {"koda": 2, "besedilo": "čas ni v [1,300]"}}), status=200) + čas_vzorca = data["čas"] + sprememba_stanja.set() + if "začni" in data.keys() and type(data["začni"]) == str: + naslednji = data["začni"] + začni_vzorec.set() + return "true" + +@http.route("/<koda>", methods=["OPTIONS"]) +def update_preflight(koda): + return flask.Response("", status=204, headers={"Access-Control-Allow-Origin": "*", "Access-Control-Allow-Methods": "GET, POST, OPTIONS", "Access-Control-Allow-Headers": "Content-Type", "Access-Control-Max-Age": "86400"}) + +def kode(): + return [base64.b64encode(hashlib.sha256(skrivnost+bytes(str(int(time.time()/1000)), encoding="utf-8")).digest()).replace(b"/", b"").replace(b"+", b"")[:4].lower().decode(), base64.b64encode(hashlib.sha256(skrivnost+bytes(str(int(time.time()/1000)-1), encoding="utf-8")).digest()).replace(b"/", b"").replace(b"+", b"")[:4].lower().decode()] + +def zaznaj_smrt(): + proces.wait() + začni_vzorec.set() + +def subprocess_manager(): + global naslednji + global proces + global začni_vzorec + while True: + if not naslednji: + vzorci = set(os.listdir("vzorci"))-set(["začasen.py"])-onemogočeni + naslednji = sorted(vzorci)[0] + print("daemon: starting " + naslednji) + proces = subprocess.Popen(["./wrapper.py", naslednji]) + sprememba_stanja.set() + vzorci = set(os.listdir("vzorci"))-set(["začasen.py"])-onemogočeni + naslednji = sorted(vzorci)[0] + if trenuten() in vzorci: + naslednji = sorted(vzorci)[(sorted(vzorci).index(trenuten())+1)%len(vzorci)] + začni_vzorec.clear() + threading.Thread(target=zaznaj_smrt).start() + razlog = začni_vzorec.wait(timeout=čas_vzorca) + proces.send_signal(subprocess.signal.SIGINT) + proces.wait() + +začni_vzorec = threading.Event() +sprememba_stanja = threading.Event() + +if __name__ == "__main__": + threading.Thread(target=lambda: http.run(host="::", port=6969, debug=True, use_reloader=False)).start() + threading.Thread(target=subprocess_manager).start() diff --git a/prog/jelka/jelka.py b/prog/jelka/jelka.py new file mode 100644 index 0000000..cd8d281 --- /dev/null +++ b/prog/jelka/jelka.py @@ -0,0 +1,18 @@ +#!/usr/bin/env python3 +# API za umetnike -- produkcija +from mmap import mmap +from sys import stdout, argv +from io import FileIO +from jelka_config import luči +shmf = open("/dev/shm/jelka", mode="r+b") +buffer = mmap(shmf.fileno(), 0) +w = FileIO(int(argv[1]), mode="w", closefd=False) +def nastavi(luč, barva): + buffer[luč*3] = barva[0] + buffer[luč*3+1] = barva[1] + buffer[luč*3+2] = barva[2] +def pokaži(tupli): + for i in range(luči): + nastavi(i, tupli[i]) +def izriši(): + w.write(b'\n') diff --git a/prog/jelka/jelka_config.py b/prog/jelka/jelka_config.py new file mode 100644 index 0000000..b7bba71 --- /dev/null +++ b/prog/jelka/jelka_config.py @@ -0,0 +1 @@ +luči = 10 diff --git a/prog/jelka/jelka_hardware.py b/prog/jelka/jelka_hardware.py new file mode 100644 index 0000000..7361ed7 --- /dev/null +++ b/prog/jelka/jelka_hardware.py @@ -0,0 +1,39 @@ +#!/usr/bin/env python3 +import jelka_config +from sys import argv +from rpi_ws281x import PixelStrip, Color + +LED_PIN = 18 # GPIO pin connected to the pixels (18 uses PWM!). +LED_FREQ_HZ = 800000 # LED signal frequency in hertz (usually 800khz) +LED_DMA = 10 # DMA channel to use for generating signal (try 10) +LED_BRIGHTNESS = 255 # Set to 0 for darkest and 255 for brightest +LED_INVERT = False # True to invert the signal (when using NPN transistor level shift) +LED_CHANNEL = 0 # set to '1' for GPIOs 13, 19, 41, 45 or 53 + +luči = LED_COUNT + +strip = PixelStrip(luči, LED_PIN, LED_FREQ_HZ, LED_DMA, LED_INVERT, LED_BRIGHTNESS, LED_CHANNEL) +strip.begin() + +def nastavi(luč, barva): + strip.setPixelColor(luč, Color(barva[0], barva[1], barva[2])) + +def izriši(): + strip.show() + +if __name__ == '__main__': + print(argv[0] + "hardware test ...") + i = 0 + try: + while True: + for k in range(luči): + if (i % 2 == 0): + strip.setPixelColor(k, Color(255, 255, 255)) + else: + strip.setPixelColor(k, Color(0, 0, 0)) + strip.show() + time.sleep(0.1) + i += 1 + + except KeyboardInterrupt: + pass diff --git a/prog/jelka/templates/index.html b/prog/jelka/templates/index.html new file mode 100644 index 0000000..f73d0b7 --- /dev/null +++ b/prog/jelka/templates/index.html @@ -0,0 +1,88 @@ +<meta name=viewport content='width=device-width, initial-scale=1.0'> +<title>jelka na fmf</title> +<h1>jelka na fmf</h1> +<label for=geslo>geslo: </label><input id=geslo type=text placeholder=geslo /> (geslo najdete na LED zaslonu) +<br> +<label for=čas>čas vzorca v sekundah: </label><input id=čas type=number placeholder=10 min=1 max=300 /> <button onclick=nastavi_čas()>nastavi!</button> +<br> +<!-- enkrat poženi poljuben vzorec: <input type=file id=file /> <button id=poženi>poženi</button> --> +<ul id=vzorci></ul> +<script> + function nastavi_čas() { + if (geslo.value == "") { + alert("nastavite geslo!"); + return; + } + fetch(geslo.value, { + method: "POST", + body: JSON.stringify({ + "čas": Number(čas.value) + }), + }).then(data => { + data.json().then(json => { + if (Object.keys(json).includes("napaka")) { + alert(json.napaka.besedilo) + } + }) + }); + } + geslo.value = location.hash.substr(1); + const evt = new EventSource("stream/"); + evt.onmessage = (e) => { + data = JSON.parse(e.data); + console.log(data); + vzorci.innerHTML = ""; + for (let i = 0; i < data.vzorci.length; i++) { + const li = document.createElement("li"); + const začni = document.createElement("button"); + const a = document.createElement("a"); + if (data["trenuten"] == data.vzorci[i]) + li.innerText = "trenuten: "; + začni.innerText = "začni"; + a.innerText = data.vzorci[i]; + a.href = "vzorci/" + data.vzorci[i]; + li.appendChild(a); + začni.onclick = () => { + if (geslo.value == "") { + alert("nastavite geslo!"); + return; + } + fetch(geslo.value, { + method: "POST", + body: JSON.stringify({ + "začni": data.vzorci[i] + }), + }).then(data => { + data.json().then(json => { + if (Object.keys(json).includes("napaka")) { + alert(a.napaka.besedilo) + } + }) + }); + }; + li.appendChild(začni); + const stanje = document.createElement("button"); + stanje.innerText = "onemogoči"; + if (data.onemogočeni.includes(data.vzorci[i])) + stanje.innerText = "omogoči"; + stanje.onclick = () => { + if (geslo.value == "") { + alert("nastavite geslo!"); + return; + } + fetch(geslo.value, { + method: "POST", + body: "{\"" + stanje.innerText + "\": [\"" + data.vzorci[i] + "\"]}", + }).then(data => { + data.json().then(json => { + if (Object.keys(json).includes("napaka")) { + alert(a.napaka.besedilo) + } + }) + }); + }; + li.appendChild(stanje); + vzorci.appendChild(li); + } + } +</script> diff --git a/prog/jelka/vzorci/anton.py b/prog/jelka/vzorci/anton.py new file mode 100755 index 0000000..744eda9 --- /dev/null +++ b/prog/jelka/vzorci/anton.py @@ -0,0 +1,13 @@ +#!/usr/bin/python3 +# Preprost vzorec 2 +from jelka import luči, nastavi, izriši +import time +while True: + for i in range(luči): + nastavi(i, (0x10, 0x10, 0x10)) + izriši() + time.sleep(0.1) + for i in range(luči): + nastavi(i, (1, 1, 1)) + izriši() + time.sleep(0.1) diff --git a/prog/jelka/vzorci/nov_vzorec.py b/prog/jelka/vzorci/nov_vzorec.py new file mode 100755 index 0000000..67ae4a1 --- /dev/null +++ b/prog/jelka/vzorci/nov_vzorec.py @@ -0,0 +1,14 @@ +#!/usr/bin/python3 +# Preprost vzorec 3 +from jelka import luči, nastavi, izriši +import time +run = 0 +while True: + for i in range(luči): + if run % luči == i: + nastavi(i, (0xff, 0xff, 0xff)) + else: + nastavi(i, (0, 0, 0)) + izriši() + time.sleep(0.1) + run += 1 diff --git a/prog/jelka/vzorci/vzorec.py b/prog/jelka/vzorci/vzorec.py new file mode 100755 index 0000000..9e9eab0 --- /dev/null +++ b/prog/jelka/vzorci/vzorec.py @@ -0,0 +1,13 @@ +#!/usr/bin/python3 +# Preprost vzorec +from jelka import luči, nastavi, izriši +import time +while True: + for i in range(luči): + nastavi(i, (0, 255, 0)) + izriši() + time.sleep(0.1) + for i in range(luči): + nastavi(i, (0, 0, 0)) + izriši() + time.sleep(0.1) diff --git a/prog/jelka/wrapper.py b/prog/jelka/wrapper.py new file mode 100755 index 0000000..ad2f11d --- /dev/null +++ b/prog/jelka/wrapper.py @@ -0,0 +1,60 @@ +#!/usr/bin/python3 +import io +import os +import pwd +import subprocess +import psutil +import mmap +import time +import resource +from jelka_config import luči +from sys import argv +if not os.getenv("DRY_RUN"): + import jelka_hardware + +def izriši(): + if os.getenv("DRY_RUN"): + print("wrapper: ", end="") + for i in range(luči*3): + print(f"{buffer[i]:02x}", end="") + print() + return + for i in range(luči): + jelka_hardware.nastavi(i, (buffer[3*i], buffer[3*i+1], buffer[3*i+2])) + jelka_hardware.izriši() + +def demote(uid, gid, chrootpath): + def result(): + resource.setrlimit(resource.RLIMIT_NPROC, (1, 1)) + os.chroot(chrootpath) + os.setgid(gid) + os.setuid(uid) + return result + +shmf = open("chroot/dev/shm/jelka", mode="w+b") +os.ftruncate(shmf.fileno(), luči*3) +buffer = mmap.mmap(shmf.fileno(), 0) +cwd = "/" +r, w = os.pipe() +args = ["/jelka/vzorci/" + argv[1], str(w)] +pw_record = pwd.getpwnam("z") +os.chown("chroot/dev/shm/jelka", 0, pw_record.pw_gid) +os.chmod("chroot/dev/shm/jelka", 0o660) +env = os.environ.copy() +env["HOME"] = pw_record.pw_dir +env["LOGNAME"] = pw_record.pw_name +env["PWD"] = cwd +env["USER"] = pw_record.pw_name +env["PYTHONPATH"] = "/jelka" +process = subprocess.Popen( + args, preexec_fn=demote(pw_record.pw_uid, pw_record.pw_uid, os.path.abspath("chroot")), cwd=cwd, env=env, pass_fds=[w] +) +try: + while io.FileIO(r, closefd=False).read(1): + izriši() + result = process.wait() + buffer.close() + shmf.close() +except: + print("cleaning up!") + process.kill() |