Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
9318cb5
feat(gemba): live packaging OEE dashboard for the Tizen TV board
mbreissi Jul 21, 2026
fdcea65
fix(gemba): packer tile shows run/jam state; bind palletizer tile to …
mbreissi Jul 21, 2026
6408f22
fix(gemba): bind live delta frames (type 'signal', name under 'signal…
mbreissi Jul 21, 2026
0b49d0a
fix(gemba): normalize delta channel path to short signal name (OPC UA…
mbreissi Jul 21, 2026
604999f
feat(gemba): native Android TV (Sony) Line-1 filling OEE dashboard
mbreissi Jul 21, 2026
8418ba8
fix(gemba): TV-at-distance legibility — raise small label fonts on th…
mbreissi Jul 21, 2026
e776530
fix(gemba): size the process-flow cards to fit (layer line was overfl…
mbreissi Jul 21, 2026
8c2d0b2
fix(gemba): keep step E layer value inline (.machine i descendant sel…
mbreissi Jul 21, 2026
0aa8858
tv-board: fit the packaging OEE board to 1080p and refine 02/03 layout
mbreissi Jul 21, 2026
8386a4b
tv-board: flow the 03 jam card down its full height, aligned with 02
mbreissi Jul 21, 2026
4ed0cbd
google-tv (Line 01): fill the panel deterministically, applying the L…
mbreissi Jul 21, 2026
adaa89a
google-tv (Line 01): fix tile value clipping + label wrap, verified o…
mbreissi Jul 21, 2026
c7c199e
google-tv (Line 01): rebuild the filling board as a composed industri…
mbreissi Jul 21, 2026
5d41fe4
google-tv (Line 01): fix the detail sloppiness — cutoffs, bowl legibi…
mbreissi Jul 21, 2026
851cfae
google-tv (Line 01): flat "broadsheet" redesign, Fable-reviewed on th…
mbreissi Jul 22, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
249 changes: 249 additions & 0 deletions experiments/gemba/apps/tv-board/app.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,249 @@
/* Dallas Line 2 packaging OEE board — LIVE.
*
* Conservative ES5 for the 2019 Tizen web runtime AND ordinary browsers. Replaces the mockup's
* synthetic scenario with the real gateway app-WebSocket: protocol-v1 hello/subscribe, then it
* binds incoming `signals` frames to the dashboard's [data-signal] tiles and drives the pallet
* grid, jam state, motor-current trend, and OEE strip from live values.
*
* Gateway URL: window.GEMBA_TV_CONFIG.gatewayUrl (config.js, used by the packaged TV app) or,
* when served in a browser at /apps/{id}/, derived from location.
*/
(function () {
"use strict";

var config = window.GEMBA_TV_CONFIG || {};
var capabilities = config.capabilities || ["signals", "alarms"];
var socket = null;
var reconnectTimer = null;
var history = [];
var latest = {};
var lastData = 0;
function nowMs() { return new Date().getTime(); }

function appIdFromPath() {
var m = (location.pathname || "").match(/\/apps\/([^\/]+)\//);
return m ? m[1] : "tv-board";
}

function gatewayUrl() {
if (config.gatewayUrl) return config.gatewayUrl;
var proto = location.protocol === "https:" ? "wss:" : "ws:";
return proto + "//" + location.host + "/apps/" + appIdFromPath() + "/ws";
}

function byId(id) { return document.getElementById(id); }
function text(id, value) { var el = byId(id); if (el) el.textContent = value; }
function pad(v) { return v < 10 ? "0" + v : String(v); }
function commas(v) { return String(v).replace(/\B(?=(\d{3})+(?!\d))/g, ","); }
function num(v) { return typeof v === "number" ? v : parseFloat(v); }

function setConnState(state, detail) {
var el = byId("source-state");
if (el) el.setAttribute("data-state", state);
if (detail) text("source-detail", detail);
}

// --- pallet grid (24-cell layer view driven by PalletCaseCount) ---
function buildPallet() {
var grid = byId("pallet-grid");
if (!grid || grid.childNodes.length) return;
for (var i = 0; i < 24; i += 1) {
var cell = document.createElement("span");
cell.textContent = pad(i + 1);
grid.appendChild(cell);
}
}
function renderPallet(palletCases) {
var loaded = palletCases <= 0 ? 0 : (((palletCases - 1) % 24) + 1);
var grid = byId("pallet-grid");
if (grid) {
var cells = grid.getElementsByTagName("span");
for (var i = 0; i < cells.length; i += 1) {
cells[i].className = i < loaded ? "is-loaded" : (i === loaded ? "is-next" : "");
}
}
var pct = Math.min(100, Math.round((palletCases / 120) * 100));
text("pallet-cases", palletCases + " / 120");
text("pallet-percent", pct + "%");
var bar = byId("pallet-progress-bar");
if (bar) bar.style.width = pct + "%";
var layer = Math.min(5, Math.floor((loaded - 1) / (24 / 5)) + 1);
if (layer > 0) { text("pallet-layer", layer + " / 5"); text("pallet-layer2", layer + " / 5"); }
}

// --- motor-current trend ---
function drawCurrent() {
var width = 600, height = 128, low = 3.5, high = 10.5, path = "", i;
for (i = 0; i < history.length; i += 1) {
var x = history.length === 1 ? 0 : (i / (history.length - 1)) * width;
var y = height - ((history[i] - low) / (high - low)) * height;
path += (i === 0 ? "M" : " L") + x.toFixed(1) + " " + y.toFixed(1);
}
var line = byId("current-line"), area = byId("current-area");
if (line) line.setAttribute("d", path);
if (area) area.setAttribute("d", path + " L" + width + " " + height + " L0 " + height + " Z");
}

// --- jam / running board state ---
function renderState(jammed, state) {
var board = byId("board");
if (board) board.className = jammed ? "board is-jammed" : "board";
var pm = byId("packer-machine");
if (pm) pm.className = jammed ? "machine machine--alert" : "machine machine--good";
text("status-text", jammed ? "BLOCKED · PACKER JAM" : "RUNNING CLEAN");
text("status-detail", jammed ? "Robot cell discharge photoeye held" : "Case packer synchronized with palletizer");
text("packer-state", jammed ? "Jammed" : "Running");
text("risk-chip", jammed ? "ACTION NOW" : "LOW RISK");
text("jam-status", jammed ? "BLOCKED" : "CLEAR");
text("attention-text", jammed
? "Clear case at packer discharge and inspect guide rail"
: "Carton magazine refill scheduled");
}

// signal -> how to render its [data-signal] tile text
var FORMAT = {
CaseRateCpm: function (v) { return num(v).toFixed(1); },
GoodCaseCount: function (v) { return commas(Math.floor(num(v))); },
CaseRejectCount: function (v) { return commas(Math.floor(num(v))); },
PackerMotorCurrentA: function (v) { return num(v).toFixed(1); },
VisionPassPct: function (v) { return num(v).toFixed(1) + "%"; },
GlueTempC: function (v) { return String(Math.round(num(v))); },
CaseWeightKg: function (v) { return num(v).toFixed(2); },
CartonMagazinePct: function (v) { return Math.round(num(v)) + "% remaining"; },
JamStatus: function (v) { return (v === true || v === "true") ? "BLOCKED" : "CLEAR"; },
LabelCode: function (v) { return String(v); },
PalletizerState: function (v) { return String(v); },
OEE: function (v) { return num(v).toFixed(1); },
Availability: function (v) { return num(v).toFixed(1); },
Performance: function (v) { return num(v).toFixed(1); },
Quality: function (v) { return num(v).toFixed(1); }
};

function applyTile(name, value) {
var fmt = FORMAT[name];
var out = fmt ? fmt(value) : String(value);
var els = document.querySelectorAll('[data-signal="' + name + '"]');
for (var i = 0; i < els.length; i += 1) {
els[i].textContent = out;
els[i].className = (els[i].className.replace(/\bvalue-updated\b/, "") + " value-updated").replace(/^\s+/, "");
}
}

function ingest(name, value) {
if (value === null || typeof value === "undefined" || !name) return;
lastData = nowMs();
latest[name] = value;
applyTile(name, value);

if (name === "PackerMotorCurrentA") {
history.push(num(value));
if (history.length > 54) history.shift();
drawCurrent();
} else if (name === "PalletCaseCount") {
renderPallet(Math.floor(num(value)));
} else if (name === "JamStatus" || name === "PalletizerState") {
var jammed = latest.JamStatus === true || latest.JamStatus === "true"
|| latest.PalletizerState === "BLOCKED";
renderState(jammed, latest.PalletizerState);
} else if (name === "CaseRateCpm") {
var d = num(value) - 28;
text("rate-delta", (d >= 0 ? "+" : "") + d.toFixed(1));
}
}

// The snapshot frame (type "signals") carries {name, latest}; the live delta frame
// (type "signal") carries {signal, point:{value}} where `signal` is the name string
// (or a {name} object). Read all forms so DELTAS bind, not just the initial snapshot.
// Snapshot series carry the short display name; delta updates carry `signal` as the CHANNEL PATH,
// which differs per adapter — modbus: bare name ("CartonMagazinePct"); OPC UA: full nodeId
// ("GGCommonsTest.Device1.Live.CaseWeightKg" / "Line1.LineSpeedBpm"); telemetry OEE: topic
// ("gemba/oee/availability"). Normalize all of these to the tile's data-signal name.
var OEE_TOPIC = { availability: "Availability", overall: "OEE", performance: "Performance", quality: "Quality" };
function normSignal(s) {
if (!s) return s;
if (s.indexOf("gemba/oee/") >= 0) {
var seg = s.split("/").pop();
return OEE_TOPIC[seg] || seg;
}
if (s.indexOf(".") >= 0) return s.split(".").pop(); // OPC UA nodeId -> last segment
return s;
}
function signalName(x) {
if (!x) return undefined;
if (x.name) return x.name; // snapshot series: short display name
if (x.signal && typeof x.signal === "object" && x.signal.name) return x.signal.name;
if (typeof x.signal === "string") return normSignal(x.signal); // delta: channel path
return x.id;
}

function handleFrame(frame) {
if (!frame) return;
if (frame.type === "signals") {
var series = frame.series || [];
for (var i = 0; i < series.length; i += 1) {
ingest(signalName(series[i]), series[i].latest);
}
} else if (frame.type === "signal") {
var ups = frame.updates || [];
for (var j = 0; j < ups.length; j += 1) {
var u = ups[j];
ingest(signalName(u), u.point ? u.point.value : u.value);
}
}
}

// --- transport ---
function send(obj) { if (socket && socket.readyState === 1) socket.send(JSON.stringify(obj)); }

function handleMessage(event) {
var msg;
try { msg = JSON.parse(event.data); } catch (e) { return; }
if (msg.type === "welcome") {
setConnState("healthy", "Bridge live · " + (msg.appId || appIdFromPath()));
send({ type: "subscribe", protocolVersion: 1, capabilities: capabilities });
} else if (msg.type === "subscribed") {
setConnState("healthy", "Live · max " + (msg.maxUpdateHz || 30) + "/s");
} else if (msg.type === "updates") {
var frames = msg.frames || [];
for (var i = 0; i < frames.length; i += 1) handleFrame(frames[i]);
} else if (msg.type === "error") {
setConnState("warn", msg.code || "error");
}
}

function connect() {
setConnState("warn", "Connecting…");
try { socket = new WebSocket(gatewayUrl()); }
catch (e) { scheduleReconnect(); return; }
socket.onopen = function () { send({ type: "hello", protocolVersion: 1 }); };
socket.onmessage = handleMessage;
socket.onclose = function () { socket = null; setConnState("warn", "Reconnecting…"); scheduleReconnect(); };
socket.onerror = function () { setConnState("warn", "Socket error"); };
}

function scheduleReconnect() {
if (reconnectTimer) return;
reconnectTimer = window.setTimeout(function () { reconnectTimer = null; connect(); }, 3000);
}

function tickClock() {
var d = new Date();
text("clock", pad(d.getHours()) + ":" + pad(d.getMinutes()) + ":" + pad(d.getSeconds()));
}

// Stalled-socket watchdog: a WebSocket can go silent without a clean onclose (e.g. the server
// bounced). If the socket looks open but no data has bound for 12s, force a reconnect so the UI
// reflects reality ("Reconnecting…") instead of a stale "Live".
function watchdog() {
if (socket && socket.readyState === 1 && lastData && (nowMs() - lastData) > 12000) {
setConnState("warn", "Stalled — reconnecting…");
try { socket.close(); } catch (e) { /* onclose triggers reconnect */ }
}
}

buildPallet();
tickClock();
window.setInterval(tickClock, 1000);
window.setInterval(watchdog, 5000);
connect();
}());
11 changes: 11 additions & 0 deletions experiments/gemba/apps/tv-board/config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
/* global window */
/* Browser-served config: leave gatewayUrl unset so app.js derives ws://<host>/apps/{id}/ws from the
* page location (works when the console serves this at /apps/tv-board/). The packaged Tizen build
* ships its own config.js with an explicit LAN gatewayUrl (see experiments/gemba/tv/tizen-gemba). */
(function () {
"use strict";
window.GEMBA_TV_CONFIG = {
protocolVersion: 1,
capabilities: ["signals", "alarms"]
};
}());
109 changes: 109 additions & 0 deletions experiments/gemba/apps/tv-board/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=1920, initial-scale=1, maximum-scale=1">
<title>Dallas Line 2 — Packaging OEE board</title>
<link rel="stylesheet" href="styles.css">
</head>
<body>
<main id="board" class="board" aria-label="Dallas packaging line OEE dashboard">
<header class="header">
<div class="brand">
<span class="brand__mark"><i></i><i></i><i></i></span>
<span><b>BOTTLES R US</b><small>DALLAS · PACKAGING HALL</small></span>
</div>
<div class="line-id"><span>LINE</span><strong>02</strong></div>
<div class="header__meta">
<span id="source-state" class="source-state" data-state="warn"><i></i> <span id="source-detail">Connecting…</span></span>
<time id="clock">--:--:--</time>
<small>SHIFT A · LIVE</small>
</div>
</header>

<section class="oee-band" aria-label="Overall equipment effectiveness">
<div class="oee-band__hero"><span>OEE</span><strong id="oee-value" data-signal="OEE">--</strong><b>%</b></div>
<div class="oee-band__part"><span>AVAILABILITY</span><b><i data-signal="Availability">--</i>%</b></div>
<div class="oee-band__part"><span>PERFORMANCE</span><b><i data-signal="Performance">--</i>%</b></div>
<div class="oee-band__part"><span>QUALITY</span><b><i data-signal="Quality">--</i>%</b></div>
</section>

<section class="status-band">
<div class="status-band__label"><span>PACKAGING STATUS</span><strong id="status-text">RUNNING CLEAN</strong><small id="status-detail">Case packer synchronized with palletizer</small></div>
<div class="status-band__sku"><span>ACTIVE ORDER</span><b data-signal="ActiveOrder">SPRK-LIME-355 · 24 PACK</b><small>WO 71042 · Customer DC-04</small></div>
<div class="status-band__rate"><span>ACTUAL RATE</span><strong id="case-rate" data-signal="CaseRateCpm">--</strong><b>CASES / MIN</b><small>Target 28.0 · <i id="rate-delta">--</i></small></div>
</section>

<section class="content">
<div class="operations">
<section class="machine-flow" aria-labelledby="flow-heading">
<div class="section-heading"><span><small>01 · PROCESS</small><h1 id="flow-heading">Pack flow</h1></span><em>All interlocks made</em></div>
<div class="flow-track">
<article class="machine machine--good"><span class="machine__index">A</span><div><small>CASE ERECTOR</small><b>Running</b><em>62% blanks</em></div><i></i></article>
<span class="flow-arrow">›</span>
<article id="packer-machine" class="machine machine--good"><span class="machine__index">B</span><div><small>ROBOTIC PACKER</small><b id="packer-state">Running</b><em id="packer-detail">6.2 A load</em></div><i></i></article>
<span class="flow-arrow">›</span>
<article class="machine machine--good"><span class="machine__index">C</span><div><small>CASE SEALER</small><b>Running</b><em>176 °C glue</em></div><i></i></article>
<span class="flow-arrow">›</span>
<article class="machine machine--good"><span class="machine__index">D</span><div><small>LABEL + VISION</small><b>Passing</b><em>99.4% verify</em></div><i></i></article>
<span class="flow-arrow">›</span>
<article class="machine machine--good"><span class="machine__index">E</span><div><small>PALLETIZER</small><b data-signal="PalletizerState">Building</b><em>Layer <i id="pallet-layer">4 / 5</i></em></div><i></i></article>
</div>
</section>

<div class="operation-grid">
<div class="op-col">
<div class="section-heading"><span><small>02 · SHIFT</small><h2>Throughput</h2></span><em class="ahead">live</em></div>
<section class="panel throughput-panel">
<div class="throughput-main"><strong id="case-count" data-signal="GoodCaseCount">--</strong><span>good cases</span></div>
<div class="plan-line"><i id="plan-fill"></i><mark></mark></div>
<div class="plan-labels"><span>Good cases</span><span>Reject cases</span></div>
<div class="throughput-stats"><span><small>Reject cases</small><b id="reject-count" data-signal="CaseRejectCount">--</b></span><span><small>Microstops</small><b>7</b></span><span><small>Blocked time</small><b id="blocked-time">--</b></span></div>
</section>
</div>

<div class="op-col">
<div class="section-heading"><span><small>03 · PACKER LOAD</small><h2>Jam early warning</h2></span><em id="risk-chip" class="risk-chip">LOW RISK</em></div>
<section class="panel jam-panel">
<div class="current-reading"><strong id="motor-current" data-signal="PackerMotorCurrentA">--</strong><span>A</span><small>Normal 5.4–7.1 A</small></div>
<svg class="current-chart" viewBox="0 0 600 128" preserveAspectRatio="none" aria-label="Packer motor current trend">
<line class="warn-line" x1="0" y1="27" x2="600" y2="27"></line>
<text x="596" y="20" text-anchor="end">9.0 A JAM RISK</text>
<path id="current-area" d=""></path>
<path id="current-line" d=""></path>
</svg>
<div class="jam-state"><span><i></i>Jam switch <b id="jam-status" data-signal="JamStatus">CLEAR</b></span><span>Palletizer <b data-signal="PalletizerState">BUILDING</b></span></div>
</section>
</div>
</div>

<section class="quality-row">
<article><span class="quality-icon quality-icon--vision">V</span><div><small>VISION PASS</small><b id="vision-pass" data-signal="VisionPassPct">--</b><em>Target ≥ 99.0%</em></div></article>
<article><span class="quality-icon quality-icon--glue">°</span><div><small>GLUE TEMPERATURE</small><b><i id="glue-temp" data-signal="GlueTempC">--</i> °C</b><em>Band 172–180 °C</em></div></article>
<article><span class="quality-icon quality-icon--weight">kg</span><div><small>CASE WEIGHT</small><b><i id="case-weight" data-signal="CaseWeightKg">--</i> kg</b><em>±0.08 kg</em></div></article>
<article><span class="quality-icon quality-icon--label">✓</span><div><small>LABEL VERIFY</small><b id="label-code" data-signal="LabelCode">--</b><em>Grade A · readable</em></div></article>
</section>
</div>

<aside class="pallet-panel" aria-labelledby="pallet-heading">
<div class="pallet-panel__top">
<span><small>LIVE LOAD</small><h2 id="pallet-heading">Pallet build</h2></span>
<em>DOCK 04</em>
</div>
<div class="pallet-progress"><strong id="pallet-percent">--</strong><span>complete</span><i><b id="pallet-progress-bar"></b></i></div>
<div class="pallet-copy"><span><small>Current layer</small><b><i id="pallet-layer2">--</i></b></span><span><small>Cases loaded</small><b id="pallet-cases" data-signal="PalletCaseCount">--</b></span></div>
<div id="pallet-grid" class="pallet-grid" aria-label="Pallet build pattern"></div>
<div class="pallet-key"><span><i class="loaded"></i>Loaded</span><span><i class="next"></i>Next pick</span><span><i></i>Open</span></div>
<div class="pallet-foot"><span><small>Pattern</small><b>24-A · Interlock</b></span><span><small>Pallet #</small><b data-signal="PalletNumber">--</b></span></div>
</aside>
</section>

<footer class="footer">
<div class="material-watch"><span class="material-watch__stripe"></span><span><small>NEXT ATTENTION</small><b id="attention-text">Carton magazine refill scheduled</b></span><em id="magazine-level" data-signal="CartonMagazinePct">--</em></div>
<div class="source-note"><b>OPC UA</b> Kepware · palletizer1 <span></span> <b>MODBUS</b> Host sim · casepacker1</div>
</footer>
</main>
<script src="config.js"></script>
<script src="app.js"></script>
</body>
</html>
Loading
Loading