428 lines
18 KiB
Python
428 lines
18 KiB
Python
#!/usr/bin/env python3
|
|
"""R-ION meeting room panel: builds the page images and the Sedona app (.sax).
|
|
|
|
python build_meeting_room.py <assets dir> <fontBank dir> [out dir]
|
|
|
|
<assets dir> the unzipped meeting_panel_assets.zip (uses bin/*_a8.bin icon masks)
|
|
<fontBank dir> <sedona home>/ontrol/fontBank - static captions are drawn with the
|
|
device's own bitmap fonts so they match the runtime labels exactly
|
|
out dir default ./out -> out/imageBank/MR_*.png + out/app-Rion-MeetingRoom.sax
|
|
|
|
Static captions are baked into the images; only values that change at runtime
|
|
(room temperature, remaining time, setpoint, humidity) are Ox labels.
|
|
"""
|
|
import base64, os, sys
|
|
from xml.sax.saxutils import quoteattr
|
|
from PIL import Image, ImageDraw
|
|
|
|
ASSETS, FONTS = sys.argv[1], sys.argv[2]
|
|
OUT = sys.argv[3] if len(sys.argv) > 3 else os.path.join(os.path.dirname(os.path.abspath(__file__)), "out")
|
|
IMG_OUT = os.path.join(OUT, "imageBank")
|
|
os.makedirs(IMG_OUT, exist_ok=True)
|
|
|
|
# ---- palette (ui_assets.h) -----------------------------------------------------------
|
|
HEX = {"BG": "#101418", "SURFACE": "#202830", "RAISED": "#303C48", "DISABLED": "#586470",
|
|
"MUTED": "#A0ACB8", "TEXT": "#FFFFFF", "INUSE": "#C83C10", "INUSE_EDGE": "#E85020",
|
|
"AVAILABLE": "#30C060", "PRIMARY": "#F8B830", "COOLING": "#3890F0"}
|
|
RGB = {k: tuple(int(v[i:i + 2], 16) for i in (1, 3, 5)) for k, v in HEX.items()}
|
|
C565 = {k: ((r >> 3) << 11) | ((g >> 2) << 5) | (b >> 3) for k, (r, g, b) in RGB.items()}
|
|
|
|
# ---- device bitmap fonts (same layout the Sedona editor reads) -----------------------
|
|
class BitmapFont:
|
|
def __init__(self, path):
|
|
d = self.data = open(path, "rb").read()
|
|
self.height = d[34]
|
|
self.glyphs = {}
|
|
for i in range(d[32] | d[33] << 8):
|
|
e = d[64 + i * 10:74 + i * 10]
|
|
code = e[0] if e[0] < 192 and e[1] == 0 else e[0] | e[1] << 8
|
|
self.glyphs[code] = (e[2] | e[3] << 8 | e[4] << 16, e[5], e[6],
|
|
e[7] - 256 if e[7] > 127 else e[7], e[8], e[9])
|
|
|
|
def layout(self, text):
|
|
out, x = [], 0
|
|
for ch in text:
|
|
g = self.glyphs.get(ord(ch))
|
|
if not g:
|
|
continue
|
|
_, w, _, xo, _, adv = g
|
|
xoff = 1 if xo < 0 else xo + 1
|
|
out.append((ord(ch), x + xoff))
|
|
x += max(adv, w, xoff + w)
|
|
return out, x
|
|
|
|
def width(self, text):
|
|
return self.layout(text)[1]
|
|
|
|
def mask(self, code):
|
|
off, w, h, _, yo, _ = self.glyphs[code]
|
|
m = Image.new("L", (max(1, w), max(1, h)))
|
|
px = bytearray(w * h)
|
|
for i in range(w * h):
|
|
b = self.data[off + (i >> 1)]
|
|
px[i] = ((b >> 4) if i % 2 == 0 else (b & 0x0F)) * 17
|
|
if w and h:
|
|
m.putdata(px)
|
|
return m, yo
|
|
|
|
|
|
_fonts = {}
|
|
def font(size, bold=False):
|
|
key = (size, bold)
|
|
if key not in _fonts:
|
|
_fonts[key] = BitmapFont(os.path.join(FONTS, f"{'bold ' if bold else ''}{size}pt Tahoma.bin"))
|
|
return _fonts[key]
|
|
|
|
|
|
def text(im, s, x, y, h, f, colour, align="left", w=0):
|
|
"""Draw s in a box (x, y, w, h), vertically centred like Draw_Text_Box."""
|
|
glyphs, tw = f.layout(s)
|
|
if align == "center":
|
|
x += (w - tw) // 2
|
|
elif align == "right":
|
|
x += w - tw
|
|
top = y + (h - f.height) // 2
|
|
solid = Image.new("RGB", (1, 1), RGB[colour])
|
|
for code, gx in glyphs:
|
|
m, yo = f.mask(code)
|
|
im.paste(solid.resize(m.size), (x + gx, top + yo), m)
|
|
return tw
|
|
|
|
|
|
def icon(im, name, x, y, colour):
|
|
size = {"play": 20, "minus": 32, "plus": 32}.get(name, 18 if name.startswith("tab_") else 26)
|
|
raw = open(os.path.join(ASSETS, "bin", f"ico_{name}_a8.bin"), "rb").read()
|
|
m = Image.frombytes("L", (size, size), raw)
|
|
im.paste(Image.new("RGB", (size, size), RGB[colour]), (x, y), m)
|
|
return size
|
|
|
|
|
|
def rect(d, x0, y0, x1, y1, fill, border=None, bw=0):
|
|
if border:
|
|
d.rectangle([x0, y0, x1, y1], fill=RGB[border])
|
|
d.rectangle([x0 + bw, y0 + bw, x1 - bw, y1 - bw], fill=RGB[fill])
|
|
else:
|
|
d.rectangle([x0, y0, x1, y1], fill=RGB[fill])
|
|
|
|
|
|
def save(im, name):
|
|
assert len(name) <= 12, name
|
|
im.save(os.path.join(IMG_OUT, name + ".png"))
|
|
|
|
|
|
# ---- layout ---------------------------------------------------------------------------
|
|
ROOM_NAME = "Meeting Room"
|
|
TABS = [(0, 106, "tab_calendar", "Meeting"), (107, 212, "tab_bulb", "Lights"), (213, 319, "tab_thermo", "Climate")]
|
|
EXT_X = [8, 112, 216]
|
|
TILES = [(8, 32), (164, 32), (8, 116), (164, 116)] # 148 x 76
|
|
SCENES = [("presentation", "Presentation"), ("meeting", "Meeting"), ("video", "Video call"), ("power", "All off")]
|
|
|
|
|
|
def page(active_tab):
|
|
im = Image.new("RGB", (320, 240), RGB["BG"])
|
|
d = ImageDraw.Draw(im)
|
|
rect(d, 0, 23, 319, 23, "RAISED")
|
|
rect(d, 0, 192, 319, 192, "RAISED")
|
|
text(im, ROOM_NAME, 8, 0, 23, font(16, True), "TEXT")
|
|
for i, (x0, x1, ico, label) in enumerate(TABS):
|
|
on = i == active_tab
|
|
if on:
|
|
rect(d, x0, 193, x1, 239, "SURFACE")
|
|
rect(d, x0, 193, x1, 195, "PRIMARY")
|
|
col = "TEXT" if on else "MUTED"
|
|
f = font(16, on)
|
|
gw = 18 + 6 + f.width(label)
|
|
gx = x0 + (x1 - x0 + 1 - gw) // 2
|
|
icon(im, ico, gx, 196 + (44 - 18) // 2, col)
|
|
text(im, label, gx + 24, 196, 44, f, col)
|
|
return im, d
|
|
|
|
|
|
# Meeting - room free
|
|
im, d = page(0)
|
|
rect(d, 8, 32, 311, 127, "AVAILABLE")
|
|
text(im, "Available", 20, 38, 44, font(32, True), "BG")
|
|
text(im, "Tap below to start a meeting", 20, 84, 24, font(16), "BG")
|
|
rect(d, 8, 136, 311, 191, "PRIMARY")
|
|
f = font(18, True)
|
|
gw = 20 + 8 + f.width("Start meeting")
|
|
gx = 8 + (304 - gw) // 2
|
|
icon(im, "play", gx, 136 + 18, "BG")
|
|
text(im, "Start meeting", gx + 28, 136, 56, f, "BG")
|
|
save(im, "MR_free")
|
|
|
|
# Meeting - in progress (extend buttons are BooleanImages drawn on top)
|
|
im, d = page(0)
|
|
rect(d, 8, 32, 311, 127, "INUSE")
|
|
text(im, "Meeting in progress", 20, 36, 32, font(24, True), "TEXT")
|
|
x = 216
|
|
rect(d, x, 136, x + 95, 191, "SURFACE", "INUSE_EDGE", 2)
|
|
f = font(18, True)
|
|
gw = 26 + 4 + f.width("End")
|
|
gx = x + (96 - gw) // 2
|
|
icon(im, "power", gx, 136 + 15, "INUSE_EDGE")
|
|
text(im, "End", gx + 30, 136, 56, f, "TEXT")
|
|
save(im, "MR_inuse")
|
|
|
|
for mins in (15, 30):
|
|
for on in (True, False):
|
|
b = Image.new("RGB", (96, 56)); bd = ImageDraw.Draw(b)
|
|
rect(bd, 0, 0, 95, 55, "RAISED" if on else "SURFACE")
|
|
text(b, f"+{mins} min", 0, 0, 56, font(18, True), "TEXT" if on else "DISABLED", "center", 96)
|
|
save(b, f"MR_e{mins}{'on' if on else 'off'}")
|
|
|
|
# Lights
|
|
im, d = page(1)
|
|
save(im, "MR_light")
|
|
for n, (ico, label) in enumerate(SCENES, 1):
|
|
for on in (True, False):
|
|
t = Image.new("RGB", (148, 76)); td = ImageDraw.Draw(t)
|
|
if on:
|
|
rect(td, 0, 0, 147, 75, "PRIMARY")
|
|
else:
|
|
rect(td, 0, 0, 147, 75, "SURFACE", "RAISED", 2)
|
|
col = "BG" if on else "TEXT"
|
|
icon(t, ico, 12, 25, col)
|
|
f = font(16, True)
|
|
if f.width(label) > 148 - 46 - 6:
|
|
f = font(16)
|
|
text(t, label, 46, 0, 76, f, col)
|
|
save(t, f"MR_s{n}{'on' if on else 'off'}")
|
|
|
|
# Climate
|
|
im, d = page(2)
|
|
rect(d, 8, 32, 71, 135, "RAISED")
|
|
rect(d, 80, 32, 231, 135, "SURFACE")
|
|
rect(d, 240, 32, 311, 135, "RAISED")
|
|
rect(d, 8, 144, 311, 191, "SURFACE")
|
|
icon(im, "minus", 24, 68, "TEXT")
|
|
icon(im, "plus", 260, 68, "TEXT")
|
|
text(im, "Setpoint", 80, 36, 18, font(12), "MUTED", "center", 152)
|
|
text(im, "Room", 20, 146, 18, font(12), "MUTED")
|
|
text(im, "Humidity", 168, 146, 18, font(12), "MUTED")
|
|
save(im, "MR_clim")
|
|
|
|
|
|
# ---- Sedona app ---------------------------------------------------------------------
|
|
def buf(s):
|
|
"""Non-@asStr Buf props are stored base64 with the terminating NUL."""
|
|
return base64.b64encode(s.encode("utf-8") + b"\0").decode()
|
|
|
|
|
|
class Comp:
|
|
_next = 1
|
|
|
|
def __init__(self, name, type_, props=None, ws=None, parent=None):
|
|
assert len(name) <= 7, name
|
|
self.name, self.type, self.children = name, type_, []
|
|
self.props = dict(props or {})
|
|
self.id = Comp._next
|
|
Comp._next += 1
|
|
if ws: # wiresheet position: meta = y<<24 | x<<16 | 1
|
|
self.props = {"meta": (ws[1] << 24) | (ws[0] << 16) | 1, **self.props}
|
|
self.parent = parent
|
|
if parent:
|
|
parent.children.append(self)
|
|
|
|
@property
|
|
def path(self):
|
|
return (self.parent.path if self.parent else "") + "/" + self.name
|
|
|
|
|
|
links = []
|
|
def link(a, slot_a, b, slot_b):
|
|
links.append((f"{a.path}.{slot_a}", f"{b.path}.{slot_b}"))
|
|
|
|
|
|
root = []
|
|
def top(name, type_="sys::Folder", props=None, ws=None):
|
|
c = Comp(name, type_, props, ws)
|
|
root.append(c)
|
|
return c
|
|
|
|
|
|
# services
|
|
service = top("service", ws=(2, 2))
|
|
plat = Comp("RionPlt", "ontrolRion::RionPlatformService", {"register01": "22.0"}, (2, 2), service)
|
|
oxbase = Comp("OxBase", "ontrolOx::OxBase", {"lcdSleepBrightness": 10, "lcdSleepTimeout": 60}, (2, 2), plat)
|
|
users = Comp("users", "sys::UserService", {}, (2, 8), service)
|
|
Comp("admin", "sys::User", {"cred": "wHy/iCHUdXWkccFgala3nl9uamg=", "perm": 2147483647, "prov": 255}, (2, 2), users)
|
|
Comp("guest", "sys::User", {"cred": "jVShQjDWIkrr+o3lYLcAPPvNVio=", "perm": 63}, (2, 8), users)
|
|
Comp("sox", "sox::SoxService", {}, (2, 14), service)
|
|
|
|
# pages
|
|
ui = top("ui", ws=(16, 2))
|
|
W = lambda x, y, w, h: {"x": x, "y": y, "width": w, "height": h}
|
|
TOUCH = {"backgroundColor": C565["BG"], "transparent": "true"}
|
|
|
|
|
|
def header_temp(p):
|
|
return Comp("RoomT", "ontrolOx::NumericLabel", {
|
|
**W(232, 1, 80, 21), "fontSize": 16, "attributes": 0x28, # right aligned, background
|
|
"text": buf("%.1f°C"), "textColor": C565["MUTED"], "backgroundColor": C565["BG"]}, parent=p)
|
|
|
|
|
|
def tab_touch(name, p, tab):
|
|
x0, x1 = TABS[tab][0], TABS[tab][1]
|
|
return Comp(name, "ontrolOx::Geometry", {**W(x0, 193, x1 - x0 + 1, 47), **TOUCH}, parent=p)
|
|
|
|
|
|
def page_comp(name, image, timeout, ws):
|
|
return Comp(name, "ontrolOx::OxPage", {"backgroundColor": C565["BG"], "imageFileName": image,
|
|
"touchTimeout": timeout}, ws, ui)
|
|
|
|
|
|
pFree = page_comp("Free", "MR_free", 0, (2, 2))
|
|
tFreeT = header_temp(pFree)
|
|
start = Comp("Start", "ontrolOx::Geometry", {**W(8, 136, 304, 56), **TOUCH}, parent=pFree)
|
|
fTabL, fTabC = tab_touch("TabLt", pFree, 1), tab_touch("TabCl", pFree, 2)
|
|
|
|
pUse = page_comp("InUse", "MR_inuse", 0, (2, 10))
|
|
tUseT = header_temp(pUse)
|
|
left = Comp("Left", "ontrolOx::NumericLabel", {
|
|
**W(20, 72, 280, 48), "fontSize": 32, "fontAttributes": 1, "attributes": 0x08,
|
|
"text": buf("%.0f min left"), "textColor": C565["TEXT"], "backgroundColor": C565["INUSE"]}, parent=pUse)
|
|
ext = {}
|
|
for n, mins in enumerate((15, 30)):
|
|
ext[mins] = Comp(f"Ext{mins}", "ontrolOx::BooleanImage", {
|
|
**W(EXT_X[n], 136, 96, 56), "falseImageFileName": f"MR_e{mins}off",
|
|
"trueImageFileName": f"MR_e{mins}on"}, parent=pUse)
|
|
end = Comp("End", "ontrolOx::Geometry", {**W(216, 136, 96, 56), **TOUCH}, parent=pUse)
|
|
uTabL, uTabC = tab_touch("TabLt", pUse, 1), tab_touch("TabCl", pUse, 2)
|
|
|
|
pLight = page_comp("Lights", "MR_light", 60, (2, 18))
|
|
tLightT = header_temp(pLight)
|
|
tiles = [Comp(f"Scene{n}", "ontrolOx::BooleanImage", {
|
|
**W(x, y, 148, 76), "falseImageFileName": f"MR_s{n}off", "trueImageFileName": f"MR_s{n}on"},
|
|
parent=pLight) for n, (x, y) in enumerate(TILES, 1)]
|
|
lTabM, lTabC = tab_touch("TabMt", pLight, 0), tab_touch("TabCl", pLight, 2)
|
|
|
|
pClim = page_comp("Climate", "MR_clim", 60, (2, 26))
|
|
tClimT = header_temp(pClim)
|
|
setpt = Comp("SetPt", "ontrolOx::NumericLabel", {
|
|
**W(80, 56, 152, 64), "fontSize": 48, "fontAttributes": 1, "attributes": 0x18,
|
|
"text": buf("%.1f°"), "textColor": C565["TEXT"], "backgroundColor": C565["SURFACE"]}, parent=pClim)
|
|
roomV = Comp("RoomV", "ontrolOx::NumericLabel", {
|
|
**W(20, 164, 136, 24), "fontSize": 18, "fontAttributes": 1, "attributes": 0x08,
|
|
"text": buf("%.1f°C"), "textColor": C565["TEXT"], "backgroundColor": C565["SURFACE"]}, parent=pClim)
|
|
humV = Comp("HumV", "ontrolOx::NumericLabel", {
|
|
**W(168, 164, 136, 24), "fontSize": 18, "fontAttributes": 1, "attributes": 0x08,
|
|
"text": buf("%.0f %%"), "textColor": C565["TEXT"], "backgroundColor": C565["SURFACE"]}, parent=pClim)
|
|
down = Comp("Down", "ontrolOx::Geometry", {**W(8, 32, 64, 104), **TOUCH}, parent=pClim)
|
|
up = Comp("Up", "ontrolOx::Geometry", {**W(240, 32, 72, 104), **TOUCH}, parent=pClim)
|
|
cTabM, cTabL = tab_touch("TabMt", pClim, 0), tab_touch("TabLt", pClim, 1)
|
|
|
|
# meeting logic
|
|
mtg = top("meeting", ws=(30, 2))
|
|
timer = Comp("Timer", "ontrolControl::CountDown", {}, (2, 2), mtg) # seconds left; out = meeting on (BootA clears it)
|
|
defDur = Comp("DefDur", "control::ConstInt", {"out": 3600}, (2, 10), mtg) # Start = 60 min
|
|
maxDur = Comp("MaxDur", "control::ConstFloat", {"out": "14400.0"}, (2, 16), mtg) # max 4 h
|
|
k = {v: Comp(f"K{v}", "control::ConstFloat", {"out": f"{v}.0"}, (2, 22 + 6 * i), mtg)
|
|
for i, v in enumerate((59, 60, 900, 1800))}
|
|
bootOn = Comp("BootOn", "control::ConstBool", {"out": "true"}, (2, 46), mtg)
|
|
|
|
TRIG = {"componentId": timer.id, "slotId": 1, "trigger": "true"} # CountDown.count
|
|
startA = Comp("StartA", "ontrolTrigger::TrigActionInt", TRIG, (14, 2), mtg)
|
|
endA = Comp("EndA", "ontrolTrigger::TrigActionInt", TRIG, (14, 10), mtg)
|
|
bootA = Comp("BootA", "ontrolTrigger::TrigActionInt", TRIG, (14, 18), mtg)
|
|
link(start, "touch", startA, "in"); link(defDur, "out", startA, "value")
|
|
link(end, "touch", endA, "in")
|
|
link(bootOn, "out", bootA, "in")
|
|
|
|
cntF = Comp("CntF", "control::I2F", {}, (14, 26), mtg)
|
|
link(timer, "count", cntF, "in")
|
|
# minutes left, rounded up: (count + 59) / 60
|
|
add59 = Comp("Add59", "control::Add2", {}, (26, 26), mtg)
|
|
div60 = Comp("Div60", "control::Div2", {}, (38, 26), mtg)
|
|
minI = Comp("MinI", "control::F2I", {}, (50, 26), mtg)
|
|
minF = Comp("MinF", "control::I2F", {}, (62, 26), mtg)
|
|
link(cntF, "out", add59, "in1"); link(k[59], "out", add59, "in2")
|
|
link(add59, "out", div60, "in1"); link(k[60], "out", div60, "in2")
|
|
link(div60, "out", minI, "in"); link(minI, "out", minF, "in")
|
|
link(minF, "out", left, "in")
|
|
|
|
# extend: count += 900 / 1800, only while the result stays under MaxDur
|
|
for row, mins in enumerate((15, 30)):
|
|
y = 34 + row * 16
|
|
sec = mins * 60
|
|
add = Comp(f"Add{mins}", "control::Add2", {}, (26, y), mtg)
|
|
toI = Comp(f"Ext{mins}I", "control::F2I", {}, (38, y), mtg)
|
|
act = Comp(f"Ext{mins}A", "ontrolTrigger::TrigActionInt", TRIG, (62, y), mtg)
|
|
lim = Comp(f"Lim{mins}", "control::Sub2", {}, (26, y + 8), mtg)
|
|
cmp_ = Comp(f"Cmp{mins}", "control::Cmpr", {}, (38, y + 8), mtg)
|
|
gate = Comp(f"And{mins}", "control::And2", {}, (50, y + 8), mtg)
|
|
link(cntF, "out", add, "in1"); link(k[sec], "out", add, "in2")
|
|
link(add, "out", toI, "in"); link(toI, "out", act, "value")
|
|
link(maxDur, "out", lim, "in1"); link(k[sec], "out", lim, "in2")
|
|
link(cntF, "out", cmp_, "x"); link(lim, "out", cmp_, "y")
|
|
link(cmp_, "xly", ext[mins], "in")
|
|
link(ext[mins], "touch", gate, "in1"); link(cmp_, "xly", gate, "in2")
|
|
link(gate, "out", act, "in")
|
|
|
|
# home page follows the meeting state; idle pages fall back to it
|
|
pageSw = Comp("PageSw", "control::ISW", {}, (26, 2), mtg)
|
|
link(pFree, "pageId", pageSw, "in1"); link(pUse, "pageId", pageSw, "in2")
|
|
link(timer, "out", pageSw, "s1"); link(pageSw, "out", oxbase, "homePageId")
|
|
|
|
# lighting scenes: Scene.out = 1 Presentation, 2 Meeting, 3 Video call, 4 All off
|
|
lights = top("lights", ws=(30, 10))
|
|
scene = Comp("Scene", "ontrolTrigger::TrigNumericSelect12",
|
|
{"value1": "1.0", "value2": "2.0", "value3": "3.0", "value4": "4.0"}, (14, 2), lights)
|
|
startSc = Comp("StartSc", "control::Or2", {}, (2, 2), lights) # Start meeting -> Meeting scene
|
|
link(tiles[1], "touch", startSc, "in1"); link(start, "touch", startSc, "in2")
|
|
for n, tile in enumerate(tiles, 1):
|
|
kc = Comp(f"S{n}", "control::ConstFloat", {"out": f"{n}.0"}, (2, 6 + 6 * n), lights)
|
|
cm = Comp(f"Is{n}", "control::Cmpr", {}, (26, 6 + 6 * n), lights)
|
|
link(scene, "out", cm, "x"); link(kc, "out", cm, "y"); link(cm, "xey", tile, "in")
|
|
if n == 2:
|
|
link(startSc, "out", scene, "trigIn2")
|
|
else:
|
|
link(tile, "touch", scene, f"trigIn{n}")
|
|
|
|
# climate: setpoint kept in platform register01 across restarts
|
|
clim = top("climate", ws=(30, 18))
|
|
spReg = Comp("SpReg", "ontrolControl::RegFloat", {"address": 1}, (2, 2), clim)
|
|
spUd = Comp("SpUD", "ontrolTrigger::TrigNumericUpDown",
|
|
{"delta": "0.5", "min": "16.0", "max": "30.0"}, (14, 2), clim)
|
|
link(spReg, "value", spUd, "value"); link(spUd, "out", spReg, "in")
|
|
link(up, "touch", spUd, "trigUp"); link(down, "touch", spUd, "trigDown")
|
|
link(spUd, "out", setpt, "in")
|
|
for lab in (tFreeT, tUseT, tLightT, tClimT, roomV):
|
|
link(plat, "roomTemperature", lab, "in")
|
|
link(plat, "roomRh", humV, "in")
|
|
|
|
# tab navigation
|
|
link(fTabL, "touch", pLight, "trigThisPage"); link(fTabC, "touch", pClim, "trigThisPage")
|
|
link(uTabL, "touch", pLight, "trigThisPage"); link(uTabC, "touch", pClim, "trigThisPage")
|
|
link(lTabM, "touch", pLight, "trigHome"); link(lTabC, "touch", pClim, "trigThisPage")
|
|
link(cTabM, "touch", pClim, "trigHome"); link(cTabL, "touch", pLight, "trigThisPage")
|
|
|
|
# ---- write .sax ----------------------------------------------------------------------
|
|
KITS = ["sys", "control", "inet", "sox", "ontrolPlatform", "ontrolRion", "ontrolOx",
|
|
"ontrolControl", "ontrolTrigger"]
|
|
lines = ["<?xml version='1.0'?>", "<sedonaApp>", "<schema>"]
|
|
lines += [f' <kit name="{k}" />' for k in KITS]
|
|
lines += ["</schema>", "<app>", ' <prop name="deviceName" val="Rion"/>',
|
|
' <prop name="appName" val="MeetRoom"/>', ' <prop name="scanPeriod" val="100"/>']
|
|
|
|
|
|
def emit(c, depth):
|
|
ind = " " * depth
|
|
lines.append(f"{ind}<!-- {c.path} -->")
|
|
lines.append(f'{ind}<comp name="{c.name}" id="{c.id}" type="{c.type}">')
|
|
for k_, v in c.props.items():
|
|
lines.append(f'{ind} <prop name="{k_}" val={quoteattr(str(v))}/>')
|
|
for ch in c.children:
|
|
emit(ch, depth + 1)
|
|
lines.append(f"{ind}</comp>")
|
|
|
|
|
|
for c in root:
|
|
emit(c, 1)
|
|
lines += ["</app>", "<links>"]
|
|
lines += [f' <link from="{a}" to="{b}"/>' for a, b in links]
|
|
lines += ["</links>", "</sedonaApp>", ""]
|
|
with open(os.path.join(OUT, "app-Rion-MeetingRoom.sax"), "w", encoding="utf-8", newline="\n") as fp:
|
|
fp.write("\n".join(lines))
|
|
print(f"{len(os.listdir(IMG_OUT))} images, {Comp._next - 1} components, {len(links)} links -> {OUT}")
|