commit 41995634c43a2e3f37d00d329ea10de8a36aa5a2 Author: Arda Aydin Date: Thu Oct 15 08:15:47 2026 +0300 R-ION meeting room panel: Sedona app, page images and simulator package Co-Authored-By: Claude Opus 5.5 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1a7a70e --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +out/sim/ +__pycache__/ +*.pyc diff --git a/README.md b/README.md new file mode 100644 index 0000000..4892ca7 --- /dev/null +++ b/README.md @@ -0,0 +1,29 @@ +# R-ION meeting room panel + +Sedona app and screen images for an R-ION 320x240 meeting room panel. It has four pages: + +- **Room free / Meeting in progress:** start a meeting, extend it by 15 or 30 minutes, end it +- **Lights:** choose one of four lighting scenes +- **Climate:** change the setpoint and see room temperature and humidity + +## Build + +```bash +python build_meeting_room.py "/ontrol/fontBank" +python make_sim.py +``` + +- `build_meeting_room.py` writes `out/app-Rion-MeetingRoom.sax` and `out/imageBank/MR_*.png`. + Copy the images into `/ontrol/imageBank`. +- `make_sim.py` writes `out/app-Rion-MeetingRoom-sim.zip`. Open it in ontrol_VM's simulatorLauncher. + The simulator uses Sox port 1876; log in as admin / 1234. + +Both scripts need Pillow. `make_sim.py` also uses Py_sab2sax and the kits.scode from ontrol_VM's ExampleApp. + +## Outputs to link to your drivers + +| Output | Meaning | +|---|---| +| `/lights/Scene.out` | Lighting scene: 1 Presentation, 2 Meeting, 3 Video call, 4 All off | +| `/climate/SpUD.out` | Temperature setpoint, saved in platform register01 | +| `/meeting/Timer.out` | True while a meeting is running | diff --git a/build_meeting_room.py b/build_meeting_room.py new file mode 100644 index 0000000..c910cad --- /dev/null +++ b/build_meeting_room.py @@ -0,0 +1,427 @@ +#!/usr/bin/env python3 +"""R-ION meeting room panel: builds the page images and the Sedona app (.sax). + + python build_meeting_room.py [out dir] + + the unzipped meeting_panel_assets.zip (uses bin/*_a8.bin icon masks) + /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 = ["", "", ""] +lines += [f' ' for k in KITS] +lines += ["", "", ' ', + ' ', ' '] + + +def emit(c, depth): + ind = " " * depth + lines.append(f"{ind}") + lines.append(f'{ind}') + for k_, v in c.props.items(): + lines.append(f'{ind} ') + for ch in c.children: + emit(ch, depth + 1) + lines.append(f"{ind}") + + +for c in root: + emit(c, 1) +lines += ["", ""] +lines += [f' ' for a, b in links] +lines += ["", "", ""] +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}") diff --git a/make_sim.py b/make_sim.py new file mode 100644 index 0000000..0476c2c --- /dev/null +++ b/make_sim.py @@ -0,0 +1,74 @@ +#!/usr/bin/env python3 +"""Packs out/app-Rion-MeetingRoom.sax for the Ontrol Rion simulator (ontrol_VM). + + python make_sim.py + +Writes out/sim/device/ (app.sab, kits.scode, images/, fonts/, system/) and +out/app-Rion-MeetingRoom-sim.zip, the backup layout simulatorLauncher opens. +kits.scode and the boot logo are taken from ontrol_VM's ExampleApp Rion backup, so +the app's schema is rewritten to that scode's kits and checksums. +""" +import os, re, shutil, sys, zipfile +from PIL import Image + +HERE = os.path.dirname(os.path.abspath(__file__)) +SEDONA_HOME = r"C:\Users\aa\OneDrive - Ontrol\Documents\_is\NiagaraAXSedona\sedona" +VM = r"C:\Users\aa\_aaSourceCodes\ontrol_VM" +EXAMPLE = os.path.join(VM, "ExampleApp", "app-RION-VM_mini.zip") +SAB2SAX = r"C:\Users\aa\_aaSourceCodes\Py_sab2sax" +FONTS = ["16pt Tahoma.bin", "32pt Tahoma.bin", "72pt Tahoma.bin", + "bold 18pt Tahoma.bin", "bold 32pt Tahoma.bin", "bold 48pt Tahoma.bin"] + +OUT = os.path.join(HERE, "out") +SAX = os.path.join(OUT, "app-Rion-MeetingRoom.sax") +SIM = os.path.join(OUT, "sim") +DEV = os.path.join(SIM, "device") +shutil.rmtree(SIM, ignore_errors=True) +for d in ("images", "fonts", "system"): + os.makedirs(os.path.join(DEV, d)) + +# app.sax with the example scode's schema (kit set and checksums must match kits.scode) +with zipfile.ZipFile(EXAMPLE) as z: + ex_sax = z.read("app.sax").decode("utf-8") + open(os.path.join(DEV, "kits.scode"), "wb").write(z.read("device/kits.scode")) + open(os.path.join(DEV, "system", "logo.bin"), "wb").write(z.read("device/system/logo.bin")) +schema = re.search(r".*?", ex_sax, re.S).group(0) +sim_sax = re.sub(r".*?", lambda m: schema, open(SAX, encoding="utf-8").read(), flags=re.S) +open(os.path.join(SIM, "app.sax"), "w", encoding="utf-8", newline="\n").write(sim_sax) + +sys.path.insert(0, SAB2SAX) +import sab # Py_sab2sax's SAX -> SAB encoder +sab.encode(os.path.join(SIM, "app.sax"), SEDONA_HOME, os.path.join(DEV, "app.sab")) + + +def native_image(path, name): + """Rion image file: 32-byte name, flags, u16 LE width/height, 27 pad, RGB565 LE.""" + img = Image.open(path).convert("RGB") + w, h = img.size + out = bytearray(name.encode("ascii")) + bytes(32 - len(name)) + out += bytes([0, w & 0xFF, w >> 8, h & 0xFF, h >> 8]) + bytes(27) + for r, g, b in img.getdata(): + v = (r >> 3) << 11 | (g >> 2) << 5 | (b >> 3) + out += bytes((v & 0xFF, v >> 8)) + return bytes(out) + + +bank = os.path.join(OUT, "imageBank") +for f in sorted(os.listdir(bank)): + name = os.path.splitext(f)[0] + open(os.path.join(DEV, "images", name + ".bin"), "wb").write(native_image(os.path.join(bank, f), name)) +for f in FONTS: + shutil.copy2(os.path.join(SEDONA_HOME, "ontrol", "fontBank", f), os.path.join(DEV, "fonts", f)) + +zpath = os.path.join(OUT, "app-Rion-MeetingRoom-sim.zip") +with zipfile.ZipFile(zpath, "w", zipfile.ZIP_DEFLATED) as z: + for root, _, files in os.walk(SIM): + for f in files: + full = os.path.join(root, f) + z.write(full, os.path.relpath(full, SIM).replace(os.sep, "/")) + for f in os.listdir(bank): + z.write(os.path.join(bank, f), "imageBank/" + f) + for f in FONTS: + z.write(os.path.join(DEV, "fonts", f), "fontBank/" + f) +print("device folder:", DEV) +print("backup zip: ", zpath) diff --git a/meeting_panel_assets.zip b/meeting_panel_assets.zip new file mode 100644 index 0000000..542ea1f Binary files /dev/null and b/meeting_panel_assets.zip differ diff --git a/out/app-Rion-MeetingRoom-sim.zip b/out/app-Rion-MeetingRoom-sim.zip new file mode 100644 index 0000000..5609e9c Binary files /dev/null and b/out/app-Rion-MeetingRoom-sim.zip differ diff --git a/out/app-Rion-MeetingRoom.sax b/out/app-Rion-MeetingRoom.sax new file mode 100644 index 0000000..d14dd7e --- /dev/null +++ b/out/app-Rion-MeetingRoom.sax @@ -0,0 +1,638 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/out/imageBank/MR_clim.png b/out/imageBank/MR_clim.png new file mode 100644 index 0000000..caeeef7 Binary files /dev/null and b/out/imageBank/MR_clim.png differ diff --git a/out/imageBank/MR_e15off.png b/out/imageBank/MR_e15off.png new file mode 100644 index 0000000..eebb70d Binary files /dev/null and b/out/imageBank/MR_e15off.png differ diff --git a/out/imageBank/MR_e15on.png b/out/imageBank/MR_e15on.png new file mode 100644 index 0000000..8dae4f6 Binary files /dev/null and b/out/imageBank/MR_e15on.png differ diff --git a/out/imageBank/MR_e30off.png b/out/imageBank/MR_e30off.png new file mode 100644 index 0000000..c4cc7b8 Binary files /dev/null and b/out/imageBank/MR_e30off.png differ diff --git a/out/imageBank/MR_e30on.png b/out/imageBank/MR_e30on.png new file mode 100644 index 0000000..907b56b Binary files /dev/null and b/out/imageBank/MR_e30on.png differ diff --git a/out/imageBank/MR_free.png b/out/imageBank/MR_free.png new file mode 100644 index 0000000..5e8a053 Binary files /dev/null and b/out/imageBank/MR_free.png differ diff --git a/out/imageBank/MR_inuse.png b/out/imageBank/MR_inuse.png new file mode 100644 index 0000000..1407b6b Binary files /dev/null and b/out/imageBank/MR_inuse.png differ diff --git a/out/imageBank/MR_light.png b/out/imageBank/MR_light.png new file mode 100644 index 0000000..8435b61 Binary files /dev/null and b/out/imageBank/MR_light.png differ diff --git a/out/imageBank/MR_s1off.png b/out/imageBank/MR_s1off.png new file mode 100644 index 0000000..61af8c3 Binary files /dev/null and b/out/imageBank/MR_s1off.png differ diff --git a/out/imageBank/MR_s1on.png b/out/imageBank/MR_s1on.png new file mode 100644 index 0000000..44f04ca Binary files /dev/null and b/out/imageBank/MR_s1on.png differ diff --git a/out/imageBank/MR_s2off.png b/out/imageBank/MR_s2off.png new file mode 100644 index 0000000..2857eb1 Binary files /dev/null and b/out/imageBank/MR_s2off.png differ diff --git a/out/imageBank/MR_s2on.png b/out/imageBank/MR_s2on.png new file mode 100644 index 0000000..6997c10 Binary files /dev/null and b/out/imageBank/MR_s2on.png differ diff --git a/out/imageBank/MR_s3off.png b/out/imageBank/MR_s3off.png new file mode 100644 index 0000000..49474c4 Binary files /dev/null and b/out/imageBank/MR_s3off.png differ diff --git a/out/imageBank/MR_s3on.png b/out/imageBank/MR_s3on.png new file mode 100644 index 0000000..57f1ad1 Binary files /dev/null and b/out/imageBank/MR_s3on.png differ diff --git a/out/imageBank/MR_s4off.png b/out/imageBank/MR_s4off.png new file mode 100644 index 0000000..7809ca2 Binary files /dev/null and b/out/imageBank/MR_s4off.png differ diff --git a/out/imageBank/MR_s4on.png b/out/imageBank/MR_s4on.png new file mode 100644 index 0000000..553fbd4 Binary files /dev/null and b/out/imageBank/MR_s4on.png differ