Send regular updates to both display and browser
All checks were successful
docker-image / docker (push) Successful in 9m40s
All checks were successful
docker-image / docker (push) Successful in 9m40s
This commit is contained in:
parent
c16d819146
commit
3568146cd6
8 changed files with 243 additions and 5 deletions
|
@ -1,7 +1,5 @@
|
|||
import json
|
||||
import logging
|
||||
from os import getenv, path
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
class AppConfig:
|
||||
|
@ -17,6 +15,7 @@ class AppConfig:
|
|||
self.templatepath = path.join(self.basepath, "templates")
|
||||
self.url = getenv('BUBA_URL', 'http://localhost:3000')
|
||||
(self.listen_host, self.listen_port) = getenv('BUBA_LISTEN', '127.0.0.1:3000').split(':')
|
||||
self.serial = getenv('BUBA_SERIAL', '/dev/ttyUSB0')
|
||||
|
||||
if self.debug is not None and self.debug.lower not in ('0', 'f', 'false'):
|
||||
self.debug = True
|
||||
|
|
24
buba/BubaAnimator.py
Normal file
24
buba/BubaAnimator.py
Normal file
|
@ -0,0 +1,24 @@
|
|||
import logging
|
||||
from datetime import datetime
|
||||
from threading import Thread
|
||||
from time import sleep
|
||||
|
||||
from buba.BubaCmd import BubaCmd
|
||||
|
||||
|
||||
class BubaAnimator:
|
||||
def __init__(self, buba:BubaCmd):
|
||||
self.log = logging.getLogger(__name__)
|
||||
self.buba = buba
|
||||
Thread(target=self.run, daemon=True).start()
|
||||
|
||||
def run(self):
|
||||
while True:
|
||||
self.buba.simple_text(page=0, row=2, col=1, text="")
|
||||
self.buba.simple_text(page=0, row=3, col=1, text="")
|
||||
self.buba.simple_text(page=0, row=4, col=1, text="")
|
||||
|
||||
self.buba.simple_text(page=0, row=1, col=1, text="CCCHH Buba")
|
||||
self.buba.simple_text(page=0, row=1, col=94, text=datetime.now().strftime("%H:%M:%S"))
|
||||
self.buba.set_page(0)
|
||||
sleep(1)
|
101
buba/BubaCmd.py
Normal file
101
buba/BubaCmd.py
Normal file
|
@ -0,0 +1,101 @@
|
|||
"""
|
||||
Commands to send to the display. Commands are always sent to the websocket, so emulators can display the data. If a serial port is available, send the commands there as well.
|
||||
"""
|
||||
import logging
|
||||
from os import path
|
||||
from typing import Callable
|
||||
|
||||
from pyfis.aegmis import MIS1TextDisplay
|
||||
|
||||
|
||||
class BubaCmd:
|
||||
def __init__(self, serial: str, send: Callable):
|
||||
self.log = logging.getLogger(__name__)
|
||||
self.serial = serial
|
||||
self.display = None
|
||||
if not path.exists(serial):
|
||||
self.serial = None
|
||||
logging.warning(f"Unable to find serial port '{serial}', ignoring")
|
||||
if self.serial is not None:
|
||||
self.display = MIS1TextDisplay(serial)
|
||||
self.send = send
|
||||
|
||||
def simple_text(self, page, row, col, text, align=MIS1TextDisplay.ALIGN_LEFT):
|
||||
"""
|
||||
Send text to the specified row.
|
||||
|
||||
The remainder of the row is cleared. This mirrors MIS1TextDisplay.simple_text
|
||||
:param self:
|
||||
:param page: which page to write the text to
|
||||
:param row: which row to write the text to
|
||||
:param col: in which column to start
|
||||
:param text: to write
|
||||
:param align: alignment options, see MIS1TextDisplay.ALIGN_*
|
||||
:return:
|
||||
"""
|
||||
if self.display is not None:
|
||||
self.display.simple_text(page, row, col, text, align)
|
||||
self.send({
|
||||
'cmd': 'simple_text',
|
||||
'page': page,
|
||||
'row': row,
|
||||
'col': col,
|
||||
'text': text,
|
||||
'align': align,
|
||||
})
|
||||
|
||||
|
||||
def text(self, page, row, col_start, col_end, text, align=MIS1TextDisplay.ALIGN_LEFT):
|
||||
"""
|
||||
Send text to the specified row, placing it between col_start and col_end.
|
||||
|
||||
This mirrors MIS1TextDisplay.text
|
||||
:param self:
|
||||
:param page: which page to write the text to
|
||||
:param row: which row to write the text to
|
||||
:param col_start: starting column
|
||||
:param col_end: ending column
|
||||
:param text: to write
|
||||
:param align: alignment options, see MIS1TextDisplay.ALIGN_*
|
||||
:return:
|
||||
"""
|
||||
if self.display is not None:
|
||||
self.display.text(page, row, col_start, col_end, text, align)
|
||||
self.send({
|
||||
'cmd': 'text',
|
||||
'page': page,
|
||||
'row': row,
|
||||
'col_start': col_start,
|
||||
'col_end': col_end,
|
||||
'text': text,
|
||||
'align': align,
|
||||
})
|
||||
|
||||
|
||||
def set_page(self, page):
|
||||
"""
|
||||
Display the given page.
|
||||
:param self:
|
||||
:param page: page number to display
|
||||
:return:
|
||||
"""
|
||||
return self.set_pages([(page, 255)])
|
||||
|
||||
|
||||
def set_pages(self, pages):
|
||||
"""
|
||||
Configure automatic paging.
|
||||
|
||||
You can specify up to 10 tuples of (pagenumber, delay).
|
||||
The display will automatically step through the list and show each page number for
|
||||
the given time. After all entries have been shown, the process starts over.
|
||||
:param self:
|
||||
:param pages:
|
||||
:return:
|
||||
"""
|
||||
if self.display is not None:
|
||||
self.display.set_pages(pages)
|
||||
self.send({
|
||||
'cmd': 'set_pages',
|
||||
'pages': pages,
|
||||
})
|
|
@ -6,6 +6,9 @@ from bottle_websocket import websocket, GeventWebSocketServer
|
|||
from geventwebsocket.websocket import WebSocket
|
||||
|
||||
from buba.AppConfig import AppConfig
|
||||
from buba.BubaAnimator import BubaAnimator
|
||||
from buba.BubaCmd import BubaCmd
|
||||
from buba.websocketcomm import WebSocketClients
|
||||
|
||||
config = AppConfig()
|
||||
if config.debug:
|
||||
|
@ -18,7 +21,9 @@ app.install(LoggingPlugin(app.config))
|
|||
TEMPLATE_PATH.insert(0, config.templatepath)
|
||||
|
||||
|
||||
# websocket_clients = WebSocketClients()
|
||||
websocket_clients = WebSocketClients()
|
||||
buba = BubaCmd(config.serial, websocket_clients.send)
|
||||
animator = BubaAnimator(buba)
|
||||
# bottle_helpers = BottleHelpers(auth, group=config.requires_group, allowed=config.allowed)
|
||||
# update_poller = UpdatePoller(websocket_clients, ccujack, 1 if config.debug else 0.1)
|
||||
|
||||
|
@ -33,6 +38,19 @@ def server_static(filepath):
|
|||
def root():
|
||||
return {}
|
||||
|
||||
@app.get('/ws', apply=[websocket])
|
||||
def websocket_endpoint(ws: WebSocket):
|
||||
try:
|
||||
websocket_clients.add(ws)
|
||||
# ws.send(json.dumps(update_poller.get_locks(True)))
|
||||
while True:
|
||||
m = ws.receive()
|
||||
except Exception as e:
|
||||
logging.debug("error in websocket", exc_info=e)
|
||||
pass
|
||||
finally:
|
||||
websocket_clients.remove(ws)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
app.run(host=config.listen_host, port=config.listen_port, server=GeventWebSocketServer, debug=config.debug,
|
||||
|
|
|
@ -5,6 +5,7 @@ export default class {
|
|||
templateSvgUrl: "static/geascript-proportional.svg",
|
||||
rows: 4,
|
||||
cols: 120,
|
||||
segments: 24,
|
||||
stripWidth: 0,
|
||||
}, config);
|
||||
console.log("Building display...");
|
||||
|
@ -69,11 +70,34 @@ export default class {
|
|||
this.applyText(3, 1, "`abcdefghijklmnopqrstuvwxyz{|}~\u2302");
|
||||
}
|
||||
|
||||
eraseCol(row, col) {
|
||||
for (let i = 1; i<=this.config.segments; i++) {
|
||||
const e = this.container.querySelector(`#geavision__row_${row}_${col}_${i}`)
|
||||
if (e) {
|
||||
e.classList = "gvsoff";
|
||||
} else {
|
||||
console.log(`Unable to find element #geavision__row_${row}_${col}_${i} for segment '${c}'`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
eraseCols(row, col_start, col_end) {
|
||||
for (let i = col_start; i <= col_end; i++) {
|
||||
this.eraseCol(row, i);
|
||||
}
|
||||
}
|
||||
|
||||
eraseRow(row) {
|
||||
this.eraseCol(row, 1, this.config.cols);
|
||||
}
|
||||
|
||||
applyCharacter(row, col, char) {
|
||||
let f = this.font[char]
|
||||
if (f === undefined)
|
||||
return 0;
|
||||
for (let c of this.font[char]) {
|
||||
if (col > this.config.cols)
|
||||
return this.font[char].length;
|
||||
for (let s = 0; s < c.length; s++) {
|
||||
const e = this.container.querySelector(`#geavision__row_${row}_${col}_${s + 1}`)
|
||||
if (e) {
|
||||
|
@ -94,6 +118,17 @@ export default class {
|
|||
return col;
|
||||
}
|
||||
|
||||
set_pages(pages) {
|
||||
console.log("set pages", pages);
|
||||
// ignore for now
|
||||
}
|
||||
|
||||
simple_text(page, row, col, text, align) {
|
||||
// console.log("simple text", page, row, col, text, align);
|
||||
this.eraseCols(row, col, this.config.cols);
|
||||
this.applyText(row, col, text);
|
||||
}
|
||||
|
||||
defineFont() {
|
||||
/*
|
||||
* This is the font definition for the Geascript Proportional font, reverse engineered from the actual display.
|
||||
|
|
|
@ -1,3 +1,7 @@
|
|||
body {
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
}
|
||||
|
||||
#geavision-display {
|
||||
padding: 1em;
|
||||
background-color: #222;
|
||||
|
|
|
@ -2,6 +2,35 @@ import Display from "./display.js";
|
|||
|
||||
let container = document.getElementById("geavision-display");
|
||||
if (container) {
|
||||
const d = new Display(container);
|
||||
// do more stuff
|
||||
const display = new Display(container);
|
||||
|
||||
function connect() {
|
||||
const ws = new WebSocket("/ws");
|
||||
ws.addEventListener("message", (event) => {
|
||||
let m = JSON.parse(event.data);
|
||||
if (m.cmd === undefined) {
|
||||
console.log("undefined command", m)
|
||||
}
|
||||
switch(m.cmd) {
|
||||
case "set_pages":
|
||||
display.set_pages(m.pages);
|
||||
break;
|
||||
case "simple_text":
|
||||
display.simple_text(m.page, m.row, m.col, m.text, m.align);
|
||||
break;
|
||||
case "text":
|
||||
console.log("text", m);
|
||||
break;
|
||||
}
|
||||
});
|
||||
ws.addEventListener("close", (ev) => {
|
||||
setTimeout(function () {
|
||||
connect();
|
||||
}, 1000)
|
||||
});
|
||||
ws.addEventListener("error", (ev) => {
|
||||
ws.close();
|
||||
});
|
||||
}
|
||||
connect();
|
||||
}
|
||||
|
|
28
buba/websocketcomm.py
Normal file
28
buba/websocketcomm.py
Normal file
|
@ -0,0 +1,28 @@
|
|||
import json
|
||||
import logging
|
||||
|
||||
|
||||
class WebSocketClients:
|
||||
def __init__(self):
|
||||
self.clients = []
|
||||
self.log = logging.getLogger(__name__)
|
||||
|
||||
def add(self, client):
|
||||
self.clients.append(client)
|
||||
|
||||
def remove(self, client):
|
||||
try:
|
||||
client.close()
|
||||
except Exception:
|
||||
pass
|
||||
if client in self.clients:
|
||||
self.clients.remove(client)
|
||||
|
||||
def send(self, data):
|
||||
for client in self.clients:
|
||||
try:
|
||||
client.send(json.dumps(data))
|
||||
except Exception as e:
|
||||
self.log.debug(f"Error sending data", exc_info=e)
|
||||
if client in self.clients:
|
||||
self.remove(client)
|
Loading…
Add table
Add a link
Reference in a new issue