186 lines
6.2 KiB
Python
186 lines
6.2 KiB
Python
"""Protokoll-Layer fuer die microSYST migra SC/MC Seriell LED-Grossanzeige.
|
|
|
|
Reines Python ohne Abhaengigkeiten, laeuft unter CPython und MicroPython.
|
|
Referenz: Benutzerhandbuch X-M31-9AXX5X-006 (V4.20), Kapitel 3.5 bis 3.9.
|
|
|
|
Telegrammaufbau: STX | DA | SA | FC | [LEN-H LEN-L] | Data-Unit | [CHK-H CHK-L] | ETX
|
|
"""
|
|
|
|
STX = 0x02
|
|
ETX = 0x03
|
|
ESC = 0x1B
|
|
SEP = 0x1F # Trennzeichen ESC-Sequenz -> Online-Text (Handbuch 3.9)
|
|
|
|
MAX_DATA_UNIT = 230
|
|
|
|
# Farben (ESC-A, ESC-F, ESC-P, ESC-H)
|
|
BLACK = "0"
|
|
GREEN = "1"
|
|
RED = "2"
|
|
YELLOW = "3"
|
|
TRANSPARENT = "T" # nur als Hintergrundfarbe
|
|
|
|
STATIC = "0"
|
|
BLINKING = "1"
|
|
|
|
# Fehlercodes aus dem Antworttelegramm (Handbuch 3.8)
|
|
ERRORS = {
|
|
"0": "kein Fehler",
|
|
"1": "falsche Pruefsumme",
|
|
"2": "reserviert",
|
|
"3": "falsche Anzahl Datenbytes (LEN-H/L) oder ungueltige ESC-Sequenz",
|
|
"4": "Element nicht vorhanden oder ungueltiger Parameter",
|
|
"5": "Flash ungueltig",
|
|
}
|
|
|
|
|
|
class MigraError(Exception):
|
|
"""Fehler im Protokoll oder von der Anzeige gemeldeter Fehlercode."""
|
|
|
|
|
|
def encode_text(s):
|
|
"""Text in Anzeige-Bytes wandeln.
|
|
|
|
Die Data-Unit erlaubt 0x20-0xFF sowie 0x0A und 0x0D. Zeichen darueber
|
|
hinaus werden zu '?'. Die Abbildung entspricht Latin-1, das sich mit dem
|
|
Windows-Zeichensatz der Anzeige im Bereich 0xA0-0xFF deckt (u.a. Grad 0xB0).
|
|
"""
|
|
out = bytearray()
|
|
for ch in s:
|
|
c = ord(ch)
|
|
if c in (0x0A, 0x0D) or 0x20 <= c <= 0xFF:
|
|
out.append(c)
|
|
else:
|
|
out.append(0x3F) # '?'
|
|
return bytes(out)
|
|
|
|
|
|
def _digits(value, places):
|
|
"""Zahl als ASCII-Ziffern fester Laenge (Handbuch 3.7: alle Werte ASCII-codiert)."""
|
|
if value < 0 or value >= 10 ** places:
|
|
raise MigraError("Wert %d passt nicht in %d Stellen" % (value, places))
|
|
return ("%0*d" % (places, value)).encode("ascii")
|
|
|
|
|
|
class DataUnit:
|
|
"""Baut eine Data-Unit aus mehreren Teiltelegrammen.
|
|
|
|
Setzt das Trennzeichen 0x1F selbsttaetig, wenn auf eine ESC-Sequenz ein
|
|
Online-Text folgt. Nur dieser Uebergang braucht es (Handbuch 3.9).
|
|
"""
|
|
|
|
def __init__(self):
|
|
self._buf = bytearray()
|
|
self._after_esc = False
|
|
|
|
def __len__(self):
|
|
return len(self._buf)
|
|
|
|
def bytes(self):
|
|
return bytes(self._buf)
|
|
|
|
def _esc(self, *parts):
|
|
self._buf.append(ESC)
|
|
for p in parts:
|
|
self._buf.extend(p if isinstance(p, bytes) else p.encode("ascii"))
|
|
self._after_esc = True
|
|
return self
|
|
|
|
# -- Online-Text ------------------------------------------------------
|
|
def text(self, s):
|
|
if self._after_esc:
|
|
self._buf.append(SEP)
|
|
self._buf.extend(encode_text(s))
|
|
self._after_esc = False
|
|
return self
|
|
|
|
# -- Zeichensatz, Cursor, Attribute (3.7.1) ---------------------------
|
|
def charset(self, number, spaced=True):
|
|
"""spaced=True -> 'z' (Sperrschrift, gleiche Breite), False -> 'Z'."""
|
|
return self._esc("z" if spaced else "Z", _digits(number, 2))
|
|
|
|
def cursor(self, x, y):
|
|
"""Cursor auf Pixelposition x/y. 0/0 ist links oben."""
|
|
return self._esc("C", _digits(x, 3), _digits(y, 3))
|
|
|
|
def attributes(self, fg=RED, bg=BLACK, blink=STATIC):
|
|
return self._esc("A", fg, bg, blink)
|
|
|
|
# -- Direkte Grafik (3.7.3) -------------------------------------------
|
|
def fill(self, color=BLACK):
|
|
"""Anzeige komplett fuellen. fill(BLACK) = loeschen."""
|
|
return self._esc("F", color)
|
|
|
|
def point(self, x, y, color=RED):
|
|
return self._esc("P", color, _digits(x, 3), _digits(y, 3))
|
|
|
|
# -- Gespeicherte Objekte (3.7.2) -------------------------------------
|
|
def stored_text(self, number, show=True):
|
|
return self._esc("T", "+" if show else "-", _digits(number, 3))
|
|
|
|
def graphic(self, number, show=True):
|
|
return self._esc("G", "+" if show else "-", _digits(number, 3))
|
|
|
|
# -- Allgemeine Funktionen (3.7.4) ------------------------------------
|
|
def brightness(self, percent, color=RED):
|
|
"""Helligkeit 0-100 %. Wird geraeteintern noch mit dem HEX-Schalter multipliziert."""
|
|
if not 0 <= percent <= 100:
|
|
raise MigraError("Helligkeit %r ausserhalb 0-100 %%" % percent)
|
|
return self._esc("H", color, _digits(percent, 3))
|
|
|
|
def blink_period(self, level):
|
|
"""0 = 2 s bis 9 = 0,2 s."""
|
|
return self._esc("B", _digits(level, 1))
|
|
|
|
|
|
def build_telegram(data, dest=1, src=0, checksum=False, response=True):
|
|
"""Data-Unit in ein vollstaendiges Telegramm rahmen.
|
|
|
|
dest/src sind die reinen Adressen (0-126); Bit 7 wird hier gesetzt.
|
|
checksum=False bedeutet: LEN-H/L und CHK-H/L duerfen *nicht* mitgesendet
|
|
werden (Handbuch 3.5.1).
|
|
"""
|
|
if isinstance(data, DataUnit):
|
|
data = data.bytes()
|
|
if len(data) > MAX_DATA_UNIT:
|
|
raise MigraError("Data-Unit hat %d Bytes, erlaubt sind %d" % (len(data), MAX_DATA_UNIT))
|
|
if not 0 <= dest <= 127 or not 0 <= src <= 126:
|
|
raise MigraError("Adresse ausserhalb des gueltigen Bereichs")
|
|
|
|
da = 0x80 | dest
|
|
sa = 0x80 | src
|
|
fc = 0x80 | (0x02 if checksum else 0x00) | (0x01 if response else 0x00)
|
|
|
|
frame = bytearray([STX, da, sa, fc])
|
|
if checksum:
|
|
n = len(data)
|
|
frame.append(0xF0 | ((n >> 4) & 0x0F))
|
|
frame.append(0xF0 | (n & 0x0F))
|
|
frame.extend(data)
|
|
if checksum:
|
|
# Summe aus DA, SA, FC, LEN-H, LEN-L und allen Datenbytes; ohne STX.
|
|
chk = sum(frame[1:]) & 0xFF
|
|
frame.append(0xF0 | ((chk >> 4) & 0x0F))
|
|
frame.append(0xF0 | (chk & 0x0F))
|
|
frame.append(ETX)
|
|
return bytes(frame)
|
|
|
|
|
|
def parse_response(frame, src=0):
|
|
"""Antworttelegramm auswerten und den Fehlercode als Zeichen liefern.
|
|
|
|
Erwartet: STX | DA | SA | FC | Fehlercode | ETX (Handbuch 3.8)
|
|
Wirft MigraError, wenn die Anzeige einen Fehler ungleich '0' meldet.
|
|
"""
|
|
if not frame:
|
|
raise MigraError("keine Antwort von der Anzeige (Timeout)")
|
|
if len(frame) < 6 or frame[0] != STX or frame[-1] != ETX:
|
|
raise MigraError("unplausibles Antworttelegramm: %s" % frame.hex())
|
|
if frame[1] != (0x80 | src):
|
|
raise MigraError("Antwort nicht an uns adressiert: %s" % frame.hex())
|
|
|
|
code = chr(frame[4])
|
|
if code != "0":
|
|
raise MigraError("Anzeige meldet Fehler '%s': %s" % (code, ERRORS.get(code, "unbekannt")))
|
|
return code
|