summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorAnton Luka Šijanec <anton@sijanec.eu>2023-11-29 11:05:18 +0100
committerAnton Luka Šijanec <anton@sijanec.eu>2023-11-29 11:05:18 +0100
commitacc8625c563a3b05b1cbfdbc94d39870c7a71b42 (patch)
treee34722420c64be71768923d9b04b611293fcefea
parentb5 (diff)
downloadr-acc8625c563a3b05b1cbfdbc94d39870c7a71b42.tar
r-acc8625c563a3b05b1cbfdbc94d39870c7a71b42.tar.gz
r-acc8625c563a3b05b1cbfdbc94d39870c7a71b42.tar.bz2
r-acc8625c563a3b05b1cbfdbc94d39870c7a71b42.tar.lz
r-acc8625c563a3b05b1cbfdbc94d39870c7a71b42.tar.xz
r-acc8625c563a3b05b1cbfdbc94d39870c7a71b42.tar.zst
r-acc8625c563a3b05b1cbfdbc94d39870c7a71b42.zip
-rwxr-xr-xprog/jelka/chroot.sh16
-rwxr-xr-xprog/jelka/daemon.py102
-rw-r--r--prog/jelka/jelka.py18
-rw-r--r--prog/jelka/jelka_config.py1
-rw-r--r--prog/jelka/jelka_hardware.py39
-rw-r--r--prog/jelka/templates/index.html88
-rwxr-xr-xprog/jelka/vzorci/anton.py13
-rwxr-xr-xprog/jelka/vzorci/nov_vzorec.py14
-rwxr-xr-xprog/jelka/vzorci/vzorec.py13
-rwxr-xr-xprog/jelka/wrapper.py60
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()