From 3568146cd6f7f76a7823e5f967ca2298ad226106 Mon Sep 17 00:00:00 2001 From: Stefan Bethke Date: Sun, 1 Jun 2025 18:48:59 +0200 Subject: [PATCH] Send regular updates to both display and browser --- buba/AppConfig.py | 3 +- buba/BubaAnimator.py | 24 ++++++++++ buba/BubaCmd.py | 101 +++++++++++++++++++++++++++++++++++++++++ buba/__main__.py | 20 +++++++- buba/static/display.js | 35 ++++++++++++++ buba/static/main.css | 4 ++ buba/static/main.js | 33 +++++++++++++- buba/websocketcomm.py | 28 ++++++++++++ 8 files changed, 243 insertions(+), 5 deletions(-) create mode 100644 buba/BubaAnimator.py create mode 100644 buba/BubaCmd.py create mode 100644 buba/websocketcomm.py diff --git a/buba/AppConfig.py b/buba/AppConfig.py index 5f20386..9124a9d 100644 --- a/buba/AppConfig.py +++ b/buba/AppConfig.py @@ -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 diff --git a/buba/BubaAnimator.py b/buba/BubaAnimator.py new file mode 100644 index 0000000..7606038 --- /dev/null +++ b/buba/BubaAnimator.py @@ -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) \ No newline at end of file diff --git a/buba/BubaCmd.py b/buba/BubaCmd.py new file mode 100644 index 0000000..5d821fc --- /dev/null +++ b/buba/BubaCmd.py @@ -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, + }) diff --git a/buba/__main__.py b/buba/__main__.py index 612e77a..05de4ac 100644 --- a/buba/__main__.py +++ b/buba/__main__.py @@ -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, diff --git a/buba/static/display.js b/buba/static/display.js index 58f68f9..7038a63 100644 --- a/buba/static/display.js +++ b/buba/static/display.js @@ -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. diff --git a/buba/static/main.css b/buba/static/main.css index 63c4519..19924ae 100644 --- a/buba/static/main.css +++ b/buba/static/main.css @@ -1,3 +1,7 @@ +body { + font-family: Arial, Helvetica, sans-serif; +} + #geavision-display { padding: 1em; background-color: #222; diff --git a/buba/static/main.js b/buba/static/main.js index bad29e6..7b8b5f3 100644 --- a/buba/static/main.js +++ b/buba/static/main.js @@ -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(); } diff --git a/buba/websocketcomm.py b/buba/websocketcomm.py new file mode 100644 index 0000000..06e4ec7 --- /dev/null +++ b/buba/websocketcomm.py @@ -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)