This commit is contained in:
parent
3ad796ffb2
commit
2321d7d552
9 changed files with 194 additions and 18 deletions
|
@ -1,11 +1,12 @@
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from bottle import Bottle, static_file, TEMPLATE_PATH, jinja2_view
|
from bottle import Bottle, static_file, TEMPLATE_PATH, jinja2_view, request, HTTPResponse
|
||||||
from bottle_log import LoggingPlugin
|
from bottle_log import LoggingPlugin
|
||||||
from bottle_websocket import websocket, GeventWebSocketServer
|
from bottle_websocket import websocket, GeventWebSocketServer
|
||||||
from geventwebsocket.websocket import WebSocket
|
from geventwebsocket.websocket import WebSocket
|
||||||
|
|
||||||
from buba.animationconfig import setup_animations
|
from buba.animationconfig import setup_animations
|
||||||
|
from buba.animations.webqueue import WebQueue
|
||||||
from buba.appconfig import AppConfig
|
from buba.appconfig import AppConfig
|
||||||
from buba.bubaanimator import BubaAnimator
|
from buba.bubaanimator import BubaAnimator
|
||||||
from buba.bubacmd import BubaCmd
|
from buba.bubacmd import BubaCmd
|
||||||
|
@ -25,7 +26,8 @@ TEMPLATE_PATH.insert(0, config.templatepath)
|
||||||
websocket_clients = WebSocketClients()
|
websocket_clients = WebSocketClients()
|
||||||
buba = BubaCmd(config.serial, websocket_clients.send)
|
buba = BubaCmd(config.serial, websocket_clients.send)
|
||||||
animator = BubaAnimator(buba)
|
animator = BubaAnimator(buba)
|
||||||
setup_animations(config, animator)
|
webqueue = WebQueue(buba)
|
||||||
|
setup_animations(config, animator, webqueue)
|
||||||
|
|
||||||
|
|
||||||
@app.route("/static/<filepath>")
|
@app.route("/static/<filepath>")
|
||||||
|
@ -36,7 +38,9 @@ def server_static(filepath):
|
||||||
@app.get("/")
|
@app.get("/")
|
||||||
@jinja2_view("home.html.j2")
|
@jinja2_view("home.html.j2")
|
||||||
def root():
|
def root():
|
||||||
return {}
|
return {
|
||||||
|
"user_forms": range(webqueue.maxrows),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@app.get('/ws', apply=[websocket])
|
@app.get('/ws', apply=[websocket])
|
||||||
|
@ -53,6 +57,49 @@ def websocket_endpoint(ws: WebSocket):
|
||||||
websocket_clients.remove(ws)
|
websocket_clients.remove(ws)
|
||||||
|
|
||||||
|
|
||||||
|
def json_error(status: str, body: dict) -> HTTPResponse:
|
||||||
|
return HTTPResponse(
|
||||||
|
body=body,
|
||||||
|
status=status,
|
||||||
|
headers={
|
||||||
|
"Content-Type": "application/json"}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def json_error_400(error: str) -> HTTPResponse:
|
||||||
|
return json_error("400 Bad Request", {
|
||||||
|
"status": "error",
|
||||||
|
"error": error,
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/user-entry")
|
||||||
|
def user_entry():
|
||||||
|
log.debug(f"json: {request.json}")
|
||||||
|
title = request.json.get("title")
|
||||||
|
if not isinstance(title, str):
|
||||||
|
raise json_error_400("title must be a string")
|
||||||
|
title = title.strip()
|
||||||
|
if title == "":
|
||||||
|
raise json_error_400("title must not be empty")
|
||||||
|
if not isinstance(request.json.get("rows"), list):
|
||||||
|
raise json_error_400("rows must be a list")
|
||||||
|
rows = []
|
||||||
|
for row in request.json.get("rows"):
|
||||||
|
if not isinstance(row[0], str) or not isinstance(row[0], str):
|
||||||
|
raise json_error_400("rows elements must be strings")
|
||||||
|
row = [v.strip() for v in row]
|
||||||
|
if row[0] != "" or row[1] != "":
|
||||||
|
rows.append(row)
|
||||||
|
rows = rows[:webqueue.maxrows]
|
||||||
|
if webqueue.add(title, rows):
|
||||||
|
return {
|
||||||
|
"status": "success",
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
raise json_error_400("queue is full")
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
app.run(host=config.listen_host, port=config.listen_port, server=GeventWebSocketServer, debug=config.debug,
|
app.run(host=config.listen_host, port=config.listen_port, server=GeventWebSocketServer, debug=config.debug,
|
||||||
quiet=not config.debug)
|
quiet=not config.debug)
|
||||||
|
|
|
@ -8,25 +8,21 @@ from buba.animations.icalevents import IcalEvents
|
||||||
from buba.animations.snake import SnakeAnimation
|
from buba.animations.snake import SnakeAnimation
|
||||||
from buba.animations.spaceapi import Spaceapi
|
from buba.animations.spaceapi import Spaceapi
|
||||||
from buba.animations.time import BubaTime
|
from buba.animations.time import BubaTime
|
||||||
|
from buba.animations.webqueue import WebQueue
|
||||||
from buba.bubaanimator import BubaAnimator
|
from buba.bubaanimator import BubaAnimator
|
||||||
|
|
||||||
LOG = logging.getLogger(__name__)
|
LOG = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
def setup_animations(config, animator: BubaAnimator):
|
def setup_animations(config, animator: BubaAnimator, webqueue: WebQueue):
|
||||||
cs = BubaCharset(animator.buba)
|
cs = BubaCharset(animator.buba)
|
||||||
# animator.add(cs)
|
|
||||||
|
|
||||||
bt = BubaTime(animator.buba)
|
bt = BubaTime(animator.buba)
|
||||||
animator.add(bt)
|
|
||||||
|
|
||||||
snake = SnakeAnimation(animator.buba)
|
snake = SnakeAnimation(animator.buba)
|
||||||
animator.add(snake)
|
|
||||||
|
|
||||||
dbf_ahst = DBF(animator.buba, ds100="AHST", station="Holstenstraße")
|
dbf_ahst = DBF(animator.buba, ds100="AHST", station="Holstenstraße")
|
||||||
dbf_ahs = DBF(animator.buba, ds100="AHS", station="Altona", count=9)
|
dbf_ahs = DBF(animator.buba, ds100="AHS", station="Altona", count=9)
|
||||||
animator.add(dbf_ahst)
|
|
||||||
animator.add(dbf_ahs)
|
|
||||||
|
|
||||||
ccchh_events = IcalEvents(animator.buba,
|
ccchh_events = IcalEvents(animator.buba,
|
||||||
url="https://cloud.hamburg.ccc.de/remote.php/dav/public-calendars/QJAdExziSnNJEz5g?export",
|
url="https://cloud.hamburg.ccc.de/remote.php/dav/public-calendars/QJAdExziSnNJEz5g?export",
|
||||||
|
@ -34,15 +30,24 @@ def setup_animations(config, animator: BubaAnimator):
|
||||||
ccc_events = IcalEvents(animator.buba,
|
ccc_events = IcalEvents(animator.buba,
|
||||||
url="https://events.ccc.de/calendar/events.ics",
|
url="https://events.ccc.de/calendar/events.ics",
|
||||||
title="CCC Events", range=datetime.timedelta(weeks=8))
|
title="CCC Events", range=datetime.timedelta(weeks=8))
|
||||||
animator.add(ccchh_events)
|
|
||||||
animator.add(ccc_events)
|
|
||||||
|
|
||||||
ccchh_spaceapi = Spaceapi(animator.buba, "https://spaceapi.hamburg.ccc.de", "CCCHH")
|
ccchh_spaceapi = Spaceapi(animator.buba, "https://spaceapi.hamburg.ccc.de", "CCCHH")
|
||||||
animator.add(ccchh_spaceapi)
|
|
||||||
|
|
||||||
|
ca = None
|
||||||
if config.clubassistant_token is not None:
|
if config.clubassistant_token is not None:
|
||||||
ca = HomeAssistant(animator.buba, "https://club-assistant.ccchh.net", "Club Assistant",
|
ca = HomeAssistant(animator.buba, "https://club-assistant.ccchh.net", "Club Assistant",
|
||||||
config.clubassistant_token)
|
config.clubassistant_token)
|
||||||
animator.add(ca)
|
|
||||||
else:
|
else:
|
||||||
LOG.warning("Club Assistant token not set, not activating animation")
|
LOG.warning("Club Assistant token not set, not activating animation")
|
||||||
|
|
||||||
|
# animator.add(cs)
|
||||||
|
animator.add(bt)
|
||||||
|
animator.add(snake)
|
||||||
|
animator.add(dbf_ahst)
|
||||||
|
animator.add(dbf_ahs)
|
||||||
|
animator.add(ccchh_events)
|
||||||
|
animator.add(ccc_events)
|
||||||
|
animator.add(ccchh_spaceapi)
|
||||||
|
if ca is not None:
|
||||||
|
animator.add(ca)
|
||||||
|
animator.add(webqueue)
|
||||||
|
|
|
@ -1,13 +1,10 @@
|
||||||
import os
|
import os
|
||||||
from datetime import timedelta, datetime
|
from datetime import timedelta, datetime
|
||||||
from threading import Thread
|
|
||||||
from time import sleep
|
|
||||||
|
|
||||||
import icalevents.icalevents
|
import icalevents.icalevents
|
||||||
from pytz import timezone
|
from pytz import timezone
|
||||||
|
|
||||||
from buba.bubaanimation import BubaAnimation
|
from buba.bubaanimation import BubaAnimation
|
||||||
from buba.bubacmd import BubaCmd
|
|
||||||
|
|
||||||
|
|
||||||
class IcalEvents(BubaAnimation):
|
class IcalEvents(BubaAnimation):
|
||||||
|
|
48
buba/animations/webqueue.py
Normal file
48
buba/animations/webqueue.py
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
from datetime import timedelta
|
||||||
|
from queue import Queue, Empty
|
||||||
|
from time import sleep
|
||||||
|
|
||||||
|
from buba.bubaanimation import BubaAnimation
|
||||||
|
from buba.bubaanimator import LineLayoutColumn
|
||||||
|
from buba.bubacmd import BubaCmd
|
||||||
|
|
||||||
|
|
||||||
|
class WebQueue(BubaAnimation):
|
||||||
|
"""
|
||||||
|
|
||||||
|
"""
|
||||||
|
def __init__(self, buba, maxlen:int=3, maxrows:int=6):
|
||||||
|
"""
|
||||||
|
A queue of pages to show. If
|
||||||
|
:param buba:
|
||||||
|
:param maxlen:
|
||||||
|
"""
|
||||||
|
super().__init__(buba)
|
||||||
|
self.title = "(waiting for input)"
|
||||||
|
self.queue = Queue()
|
||||||
|
self.maxlen = maxlen
|
||||||
|
self.maxrows = maxrows
|
||||||
|
|
||||||
|
def show(self) -> None:
|
||||||
|
try:
|
||||||
|
(self.title, self.rows) = self.queue.get(block=False)
|
||||||
|
self.layout = self.default_layout
|
||||||
|
self.show_pages()
|
||||||
|
except Empty:
|
||||||
|
self.buba.text(page=0, row=0, col_start=0, col_end=119, text="\u0010\u0010 Your Data Here \u0011\u0011", align=BubaCmd.ALIGN_CENTER)
|
||||||
|
self.buba.text(page=0, row=1, col_start=0, col_end=119, text="Submit your own data to", align=BubaCmd.ALIGN_CENTER)
|
||||||
|
self.buba.text(page=0, row=2, col_start=0, col_end=119, text="this display! Go to", align=BubaCmd.ALIGN_CENTER)
|
||||||
|
self.buba.text(page=0, row=3, col_start=0, col_end=119, text="https://buba.ccchh.net", align=BubaCmd.ALIGN_CENTER)
|
||||||
|
sleep(7)
|
||||||
|
|
||||||
|
def add(self, title, rows) -> bool:
|
||||||
|
"""
|
||||||
|
Add an entry to the queue. Returns False if the maximum number of entries has been reached.
|
||||||
|
:param title:
|
||||||
|
:param rows:
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
if self.queue.qsize() >= self.maxlen:
|
||||||
|
return False
|
||||||
|
self.queue.put((title, rows))
|
||||||
|
return True
|
|
@ -64,6 +64,7 @@ class BubaCmd:
|
||||||
:param align: alignment options, see MIS1TextDisplay.ALIGN_*
|
:param align: alignment options, see MIS1TextDisplay.ALIGN_*
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
|
text = text.replace("\u00ad", "") # remove soft hyphen
|
||||||
text = text.encode("CP437", "replace").decode("CP437") # force text to be CP437 compliant
|
text = text.encode("CP437", "replace").decode("CP437") # force text to be CP437 compliant
|
||||||
if self.display is not None:
|
if self.display is not None:
|
||||||
self.display.text(page, row, col_start, col_end, text, align)
|
self.display.text(page, row, col_start, col_end, text, align)
|
||||||
|
|
|
@ -30,3 +30,15 @@ svg.geavision__row {
|
||||||
.gvson {
|
.gvson {
|
||||||
fill: #4f0;
|
fill: #4f0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.info-form__row {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-form__field {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
.info-form__field * {
|
||||||
|
display: block;
|
||||||
|
}
|
|
@ -11,7 +11,7 @@ if (container) {
|
||||||
if (m.cmd === undefined) {
|
if (m.cmd === undefined) {
|
||||||
console.log("undefined command", m)
|
console.log("undefined command", m)
|
||||||
}
|
}
|
||||||
switch(m.cmd) {
|
switch (m.cmd) {
|
||||||
case "set_pages":
|
case "set_pages":
|
||||||
display.set_pages(m.pages);
|
display.set_pages(m.pages);
|
||||||
break;
|
break;
|
||||||
|
@ -32,5 +32,44 @@ if (container) {
|
||||||
ws.close();
|
ws.close();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
connect();
|
connect();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let info_send = document.getElementById("info-send");
|
||||||
|
if (info_send) {
|
||||||
|
document.getElementById("info-form__form").addEventListener("submit", (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
})
|
||||||
|
info_send.addEventListener("click", (e) => {
|
||||||
|
let info = {
|
||||||
|
"title": document.getElementById("info-form__title").value,
|
||||||
|
"rows": []
|
||||||
|
}
|
||||||
|
for (let e of document.querySelectorAll(".info-form__row-data")) {
|
||||||
|
let t = e.querySelector(".info-row__field-text").value;
|
||||||
|
let v = e.querySelector(".info-row__field-value").value;
|
||||||
|
info.rows.push([t, v]);
|
||||||
|
}
|
||||||
|
fetch("/user-entry", {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify(info),
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
}
|
||||||
|
}).then(res => {
|
||||||
|
if (res.status === 200) {
|
||||||
|
alert("Your entry has been sent!")
|
||||||
|
} else {
|
||||||
|
return res.json()
|
||||||
|
}
|
||||||
|
}).then(json => {
|
||||||
|
alert("Your entry could not be accepted: " + json.error)
|
||||||
|
}).catch((err) => {
|
||||||
|
console.log(err);
|
||||||
|
})
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
})
|
||||||
|
}
|
|
@ -8,7 +8,6 @@
|
||||||
<script src="static/main.js" type="module"></script>
|
<script src="static/main.js" type="module"></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<h1>{{ self.page_title() }}</h1>
|
|
||||||
{% block page_body %}{% endblock %}
|
{% block page_body %}{% endblock %}
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
|
@ -2,4 +2,32 @@
|
||||||
{% block page_title %}CCCHH Buba{% endblock %}
|
{% block page_title %}CCCHH Buba{% endblock %}
|
||||||
{% block page_body %}
|
{% block page_body %}
|
||||||
<div id="geavision-display">...</div>
|
<div id="geavision-display">...</div>
|
||||||
|
<h3>Submit Your Own Info</h3>
|
||||||
|
<div class="info-form">
|
||||||
|
<form id="info-form__form">
|
||||||
|
<div class="info-form__row">
|
||||||
|
<div class="info-form__field">
|
||||||
|
<label for="info-form__title">Title</label>
|
||||||
|
<input id="info-form__title" type="text" name="title" size="32">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% for r in user_forms %}
|
||||||
|
<div class="info-form__row info-form__row-data">
|
||||||
|
<div class="info-form__field">
|
||||||
|
<label for="info-form__row-{{ r }}-text">Description</label><br>
|
||||||
|
<input id="info-form__row-{{ r }}-text" class="info-row__field-text" type="text" name="row-{{ r }}-text" size="27"><br>
|
||||||
|
</div>
|
||||||
|
<div class="info-form__field">
|
||||||
|
<label for="info-form__row-{{ r }}-value">Value</label><br>
|
||||||
|
<input id="info-form__row-{{ r }}-value" class="info-row__field-value" type="text" name="row-{{ r }}-value" size="5">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
<div class="info-form__row">
|
||||||
|
<div class="info-form__field">
|
||||||
|
<button id="info-send">Send Info</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue