Component demo · Operator screens and process graphics
Device Control
index.htm as it is written with the smart-industrial package
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" lang="en">
<head>
<title>DeviceControl Overview Demo</title>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="stylesheet" type="text/css" href="node_modules/smart-industrial/source/styles/smart.default.css" />
<link rel="stylesheet" type="text/css" href="node_modules/smart-industrial/source/styles/smart.industrial-elements.css" />
<link rel="stylesheet" type="text/css" href="node_modules/smart-industrial/source/styles/smart.industrial.css" />
<link rel="stylesheet" type="text/css" href="node_modules/smart-industrial/source/styles/smart.industrial-dark.css" />
<link rel="stylesheet" type="text/css" href="styles.css" />
</head>
<body class="viewport" theme="industrial">
<div class="smart-demo-container">
<section id="overview">
<h2>Smart.DeviceControl is a faceplate for a discrete device such as a motor, a pump or an on/off valve.</h2>
<p>
The faceplate shows the device feedback (running, stopped, open or closed, as reported by the device) next to the commanded state, the operating mode that determines who may command the device, and the interlock and permissives that determine whether a command is accepted. The component does not write to the plant. A command button raises a request event, the application performs the write, and the faceplate updates when the new feedback is reported. A command that cannot be requested shows the reason.
</p>
</section>
<section>
<h2>A pump, a valve and a motor</h2>
<div class="demo-row">
<smart-device-control id="pump" tag="P-2101A" description="Feed pump A" kind="pump" state="stopped" commanded="stop" mode="manual" interactive run-hours="1842.5" start-count="312"></smart-device-control>
<smart-device-control id="valve" tag="XV-2104" description="Feed isolation" kind="valve" state="closed" commanded="close" mode="manual" interactive></smart-device-control>
<smart-device-control id="agitator" tag="M-2301" description="Reactor agitator" kind="motor" state="running" commanded="start" mode="auto" interactive run-hours="9930" start-count="41"></smart-device-control>
</div>
<div class="demo-actions">
<button type="button" id="slow">Answer in 2 s</button>
<button type="button" id="refuse">Refuse the next request</button>
<button type="button" id="trip">Trip the pump</button>
<button type="button" id="permissive">Lose seal water</button>
</div>
<p class="demo-log" id="log">Click Start on the pump. The faceplate shows the request as pending until the application reports the feedback. The agitator is in auto mode, so it has to be switched to manual before it accepts a command.</p>
</section>
<section>
<h2>Blocked commands</h2>
<p>
A device under local control or behind an interlock can be stopped from the screen but not started, and a faceplate whose <code>interactive</code> property is not set accepts no commands. The command buttons stay readable and show the reason when pressed.
</p>
<div class="demo-row">
<smart-device-control id="local" tag="P-2101B" description="Feed pump B" kind="pump" state="running" commanded="start" mode="local" interactive></smart-device-control>
<smart-device-control id="interlocked" tag="M-2302" description="Transfer conveyor" kind="motor" state="stopped" commanded="stop" mode="manual" interactive interlocked interlock-reason="Downstream gate closed"></smart-device-control>
<smart-device-control id="readonly" tag="XV-2109" description="Vent" kind="valve" state="open" commanded="open" mode="manual" quality="stale"></smart-device-control>
</div>
</section>
<section>
<h2>Compact mode</h2>
<div class="demo-strip">
<smart-device-control tag="P-2101A" kind="pump" state="running" commanded="start" mode="auto" compact></smart-device-control>
<smart-device-control tag="P-2101B" kind="pump" state="stopped" commanded="stop" mode="auto" compact></smart-device-control>
<smart-device-control tag="XV-2104" kind="valve" state="open" commanded="open" mode="auto" compact></smart-device-control>
<smart-device-control tag="M-2301" kind="motor" state="fault" commanded="start" mode="auto" fault-text="Overload" compact></smart-device-control>
</div>
</section>
</div>
<!-- scripts -->
<script type="module" src="node_modules/smart-industrial/source/modules/smart.devicecontrol.js"></script>
<script type="module" src="index.js"></script>
<script>
/* Shown once every element has rendered, so nothing moves on the way in (styles/demos.css). */
addEventListener('load', function () {
var all = Array.prototype.slice.call(document.querySelectorAll('*')),
ready = function () { document.body.classList.add('ready'); };
Promise.all(all.map(function (e) { return e.localName.indexOf('smart-') === 0 ? customElements.whenDefined(e.localName) : null; })).then(function () {
return Promise.all(all.filter(function (e) { return typeof e.whenRendered === 'function' && !e.isRendered; }).map(function (e) { return e.whenRendered(); }));
}).then(function () { requestAnimationFrame(function () { requestAnimationFrame(ready); }); }, ready);
});
</script>
</body>
</html>
index.js
/// <reference path="../../../source/typescript/smart.elements.d.ts" />
/* Device control overview.
*
* What the page shows is what the host has to do, because the plate deliberately does not:
* every request arrives as an event, the host writes the plant, and the plate moves only when
* the feedback comes back. A stand-in plant answers here, with a delay and a refusal on
* request, so the pending state and the refusal can be watched. */
window.onload = function () {
const pump = document.getElementById('pump'),
valve = document.getElementById('valve'),
agitator = document.getElementById('agitator'),
log = document.getElementById('log'),
plates = [pump, valve, agitator, document.getElementById('local'), document.getElementById('interlocked'), document.getElementById('readonly')];
let delay = 0,
refuseNext = false;
pump.permissives = [
{ label: 'Suction valve XV-2104 open', met: false },
{ label: 'Seal water FS-2101', met: true },
{ label: 'No overload', met: true }
];
/* ── The stand-in plant ─────────────────────────────────────────────── */
/* The feedback a device gives to a command: a transient, then the end state. */
function answer(plate, command) {
const transient = { start: 'starting', stop: 'stopping', open: 'opening', close: 'closing' }[command],
final = { start: 'running', stop: 'stopped', open: 'open', close: 'closed' }[command];
setTimeout(function () {
if (refuseNext) {
refuseNext = false;
plate.refuse('Sequence holds ' + plate.tag);
log.textContent = plate.tag + ': the host refused the ' + command + ' - the plate shows why and stops waiting.';
return;
}
plate.commanded = command;
plate.state = transient;
setTimeout(function () {
plate.state = final;
if (plate === valve) {
/* the pump's permissive follows the valve */
pump.permissives = pump.permissives.map(function (permissive) {
return permissive.label.indexOf('XV-2104') === 0 ? { label: permissive.label, met: final === 'open' } : permissive;
});
}
log.textContent = plate.tag + ': ' + command + ' confirmed by the feedback' + (delay ? ' after ' + delay / 1000 + ' s' : '') + '.';
}, 700);
}, delay);
}
plates.forEach(function (plate) {
plate.addEventListener('commandRequest', function (event) {
log.textContent = event.detail.tag + ': ' + event.detail.command + ' requested - pending until the plant answers.';
answer(plate, event.detail.command);
});
plate.addEventListener('modeRequest', function (event) {
/* the plant grants the mode after a moment */
setTimeout(function () {
plate.mode = event.detail.mode;
log.textContent = event.detail.tag + ' is now in ' + event.detail.mode + '.';
}, 300);
});
plate.addEventListener('resetRequest', function (event) {
setTimeout(function () {
plate.faultText = '';
plate.state = 'stopped';
plate.commanded = 'stop';
log.textContent = event.detail.tag + ': fault reset, stopped.';
}, 500);
});
plate.addEventListener('blocked', function (event) {
log.textContent = event.detail.tag + ': ' + event.detail.command + ' not requested - ' + event.detail.reason + '.';
});
plate.addEventListener('writeTimeout', function (event) {
log.textContent = event.detail.tag + ': no feedback for the ' + event.detail.command + ' within ' + plate.pendingTimeout / 1000 + ' s - the request was dropped.';
});
});
/* ── The buttons ────────────────────────────────────────────────────── */
document.getElementById('slow').addEventListener('click', function () {
delay = delay ? 0 : 2000;
this.textContent = delay ? 'Answer at once' : 'Answer in 2 s';
});
document.getElementById('refuse').addEventListener('click', function () {
refuseNext = true;
log.textContent = 'The next request will be refused by the host.';
});
document.getElementById('trip').addEventListener('click', function () {
pump.faultText = 'Overload trip';
pump.state = 'fault';
pump.discrepancy = false;
log.textContent = 'P-2101A tripped: the fault is announced, and Reset appears.';
});
document.getElementById('permissive').addEventListener('click', function () {
const lost = pump.permissives.some(function (permissive) {
return permissive.label.indexOf('Seal water') === 0 && permissive.met;
});
pump.permissives = pump.permissives.map(function (permissive) {
return permissive.label.indexOf('Seal water') === 0 ? { label: permissive.label, met: !lost } : permissive;
});
this.textContent = lost ? 'Restore seal water' : 'Lose seal water';
log.textContent = lost ? 'Seal water flow lost: the pump may still be stopped, not started.' : 'Seal water back.';
});
/* the agitator's discrepancy: commanded start, feedback says stopped */
setTimeout(function () {
agitator.state = 'stopped';
agitator.discrepancy = true;
}, 9000);
};
styles.css
.smart-demo-container {
max-width: 900px;
}
section {
margin-bottom: 34px;
}
section h2 {
font-size: 16px;
font-weight: 500;
margin: 0 0 8px;
}
section p {
color: var(--smart-text-secondary-color);
margin: 0 0 14px;
max-width: 74ch;
}
code {
background: var(--smart-ui-state-hover);
font-family: Consolas, "Courier New", monospace;
font-size: 12px;
padding: 1px 5px;
}
.demo-actions {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-top: 14px;
}
.demo-actions button {
background: transparent;
border: 1px solid var(--smart-border);
border-radius: var(--smart-border-radius);
color: var(--smart-text-secondary-color);
cursor: pointer;
font-family: inherit;
font-size: 11px;
letter-spacing: .06em;
padding: 7px 14px;
text-transform: uppercase;
}
.demo-actions button:hover {
background: var(--smart-ui-state-hover);
border-color: var(--smart-ui-state-border-hover);
color: var(--smart-ui-state-color-hover);
}
.demo-log,
.demo-counts {
font-family: Consolas, "Courier New", monospace;
font-size: 12px;
margin: 12px 0 0;
color: var(--smart-text-tertiary-color);
}
smart-root-cause-tree {
display: block;
max-width: 720px;
padding: 6px 0;
border: 1px solid var(--smart-border);
border-radius: var(--smart-border-radius);
background: var(--smart-industrial-panel-background, var(--smart-surface));
}
.demo-ranked {
color: var(--smart-text-secondary-color);
font-size: 12px;
}
.demo-line {
display: flex;
flex-wrap: wrap;
gap: 22px;
align-items: flex-end;
padding: 16px 18px;
border: 1px solid var(--smart-border);
border-radius: var(--smart-border-radius);
}
.demo-station {
display: flex;
flex-direction: column;
align-items: center;
gap: 6px;
font-size: 12px;
color: var(--smart-text-secondary-color);
}
.demo-row {
display: flex;
flex-wrap: wrap;
gap: 28px;
align-items: flex-start;
}
.demo-actions label {
display: inline-flex;
align-items: center;
gap: 6px;
font-size: 13px;
}
.demo-actions select {
font: inherit;
padding: 4px 6px;
border: 1px solid var(--smart-border);
border-radius: var(--smart-border-radius);
background: var(--smart-background);
color: var(--smart-text-color);
}
.demo-strip {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.demo-strip smart-device-control {
--smart-device-width: 200px;
}
/* ── Demo chrome, shared by the Industrial demos ─────────────────────────
Headings, prose, buttons and notes are the same on every demo page. This block
is appended by scripts and comes last, so it wins over the rules above. */
.smart-demo-container {
padding: 4px 0 32px;
}
.smart-demo-container section {
margin-bottom: 36px;
}
.smart-demo-container section h2 {
margin: 0 0 8px;
font-size: 17px;
font-weight: 600;
line-height: 1.35;
color: var(--smart-text-color);
}
.smart-demo-container #overview h2 {
margin-bottom: 10px;
font-size: 20px;
max-width: 78ch;
}
.smart-demo-container section > p {
margin: 0 0 14px;
max-width: 84ch;
font-size: 14px;
line-height: 1.55;
color: var(--smart-text-secondary-color);
}
.smart-demo-container code {
padding: 1px 5px;
border-radius: 2px;
background: var(--smart-ui-state-hover);
font-family: Consolas, "Courier New", monospace;
font-size: 12px;
}
.smart-demo-container .demo-actions {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 8px 10px;
margin-top: 12px;
}
.smart-demo-container .demo-actions label {
display: inline-flex;
align-items: center;
gap: 6px;
font-size: 13px;
color: var(--smart-text-color);
}
.smart-demo-container .demo-actions button,
.smart-demo-container .demo-button {
min-height: 30px;
padding: 0 12px;
border: 1px solid var(--smart-border);
border-radius: 3px;
background: var(--smart-industrial-panel-background, var(--smart-surface));
color: var(--smart-text-color);
font: inherit;
font-size: 13px;
font-weight: 500;
letter-spacing: 0;
text-transform: none;
cursor: pointer;
}
.smart-demo-container .demo-actions button:hover,
.smart-demo-container .demo-button:hover {
border-color: var(--smart-ui-state-border-hover);
background: var(--smart-ui-state-hover);
color: var(--smart-ui-state-color-hover);
}
.smart-demo-container .demo-actions button.demo-button-primary,
.smart-demo-container .demo-button-primary {
border-color: var(--smart-primary);
background: var(--smart-primary);
color: var(--smart-primary-color, #fff);
}
.smart-demo-container .demo-actions button[disabled],
.smart-demo-container .demo-button[disabled] {
opacity: 0.5;
cursor: default;
}
.smart-demo-container .demo-actions select,
.smart-demo-container .demo-actions input[type="text"],
.smart-demo-container .demo-actions input[type="number"] {
min-height: 30px;
padding: 2px 6px;
border: 1px solid var(--smart-border);
border-radius: 3px;
background: var(--smart-background);
color: var(--smart-text-color);
font: inherit;
font-size: 13px;
}
.smart-demo-container .hint,
.smart-demo-container .demo-log,
.smart-demo-container .demo-counts {
margin: 10px 0 0;
font-size: 12px;
line-height: 1.5;
color: var(--smart-text-secondary-color);
opacity: 1;
}
.smart-demo-container .demo-log,
.smart-demo-container .demo-counts {
font-family: Consolas, "Courier New", monospace;
}
.smart-demo-container .log {
list-style: none;
margin: 10px 0 0;
padding: 0;
font-size: 12px;
font-variant-numeric: tabular-nums;
color: var(--smart-text-secondary-color);
opacity: 1;
}
.smart-demo-container .log li {
padding: 2px 0;
}
The same demo in Angular, generated from the HTML demo. The frameworks guide has the project setup.
angular/app.component.html
<div class="angular-root" theme="industrial">
<div class="smart-demo-container">
<section id="overview">
<h2>Smart.DeviceControl is a faceplate for a discrete device such as a motor, a pump or an on/off valve.</h2>
<p>
The faceplate shows the device feedback (running, stopped, open or closed, as reported by the device) next to the commanded state, the operating mode that determines who may command the device, and the interlock and permissives that determine whether a command is accepted. The component does not write to the plant. A command button raises a request event, the application performs the write, and the faceplate updates when the new feedback is reported. A command that cannot be requested shows the reason.
</p>
</section>
<section>
<h2>A pump, a valve and a motor</h2>
<div class="demo-row">
<smart-device-control id="pump" tag="P-2101A" description="Feed pump A" kind="pump" state="stopped" commanded="stop" mode="manual" [interactive]="true" [runHours]="1842.5" [startCount]="312"></smart-device-control>
<smart-device-control id="valve" tag="XV-2104" description="Feed isolation" kind="valve" state="closed" commanded="close" mode="manual" [interactive]="true"></smart-device-control>
<smart-device-control id="agitator" tag="M-2301" description="Reactor agitator" kind="motor" state="running" commanded="start" mode="auto" [interactive]="true" [runHours]="9930" [startCount]="41"></smart-device-control>
</div>
<div class="demo-actions">
<button type="button" id="slow">Answer in 2 s</button>
<button type="button" id="refuse">Refuse the next request</button>
<button type="button" id="trip">Trip the pump</button>
<button type="button" id="permissive">Lose seal water</button>
</div>
<p class="demo-log" id="log">Click Start on the pump. The faceplate shows the request as pending until the application reports the feedback. The agitator is in auto mode, so it has to be switched to manual before it accepts a command.</p>
</section>
<section>
<h2>Blocked commands</h2>
<p>
A device under local control or behind an interlock can be stopped from the screen but not started, and a faceplate whose <code>interactive</code> property is not set accepts no commands. The command buttons stay readable and show the reason when pressed.
</p>
<div class="demo-row">
<smart-device-control id="local" tag="P-2101B" description="Feed pump B" kind="pump" state="running" commanded="start" mode="local" [interactive]="true"></smart-device-control>
<smart-device-control id="interlocked" tag="M-2302" description="Transfer conveyor" kind="motor" state="stopped" commanded="stop" mode="manual" [interactive]="true" [interlocked]="true" interlock-reason="Downstream gate closed"></smart-device-control>
<smart-device-control id="readonly" tag="XV-2109" description="Vent" kind="valve" state="open" commanded="open" mode="manual" quality="stale"></smart-device-control>
</div>
</section>
<section>
<h2>Compact mode</h2>
<div class="demo-strip">
<smart-device-control tag="P-2101A" kind="pump" state="running" commanded="start" mode="auto" [compact]="true"></smart-device-control>
<smart-device-control tag="P-2101B" kind="pump" state="stopped" commanded="stop" mode="auto" [compact]="true"></smart-device-control>
<smart-device-control tag="XV-2104" kind="valve" state="open" commanded="open" mode="auto" [compact]="true"></smart-device-control>
<smart-device-control tag="M-2301" kind="motor" state="fault" commanded="start" mode="auto" fault-text="Overload" [compact]="true"></smart-device-control>
</div>
</section>
</div>
</div>
angular/app.component.ts
import { Component, AfterViewInit, OnDestroy } from '@angular/core';
import { DeviceControlModule } from 'smart-industrial/angular/devicecontrol';
import { runDemo } from './demo';
/**
* Smart.UI Industrial - DeviceControl in Angular.
*
* The template is the HTML demo's markup: the elements are the wrappers, so what is
* written on them is checked against their inputs. demo.ts drives them the way the
* plant's data would, once every element in the view has rendered.
*/
@Component({
selector: 'app-root',
standalone: true,
imports: [DeviceControlModule],
templateUrl: './app.component.html',
styleUrl: './app.component.css'
})
export class AppComponent implements AfterViewInit, OnDestroy {
private unmounted = false;
ngAfterViewInit(): void {
const root = document.querySelector('.angular-root') as HTMLElement,
pending: Promise<void>[] = [];
root.querySelectorAll('*').forEach((element: any) => {
if (typeof element.whenRendered === 'function' && !element.isRendered) {
pending.push(element.whenRendered());
}
});
Promise.all(pending).then(() => {
if (!this.unmounted) {
runDemo();
}
});
}
ngOnDestroy(): void {
this.unmounted = true;
}
}
angular/demo.ts
// @ts-nocheck
/* The HTML demo's script, as it was written: page code that feeds the elements, not a typed
API. The types are on the wrappers in app.component.ts. */
/* Device control overview.
*
* What the page shows is what the host has to do, because the plate deliberately does not:
* every request arrives as an event, the host writes the plant, and the plate moves only when
* the feedback comes back. A stand-in plant answers here, with a delay and a refusal on
* request, so the pending state and the refusal can be watched. */
export function runDemo(): void {
const pump = document.getElementById('pump'),
valve = document.getElementById('valve'),
agitator = document.getElementById('agitator'),
log = document.getElementById('log'),
plates = [pump, valve, agitator, document.getElementById('local'), document.getElementById('interlocked'), document.getElementById('readonly')];
let delay = 0,
refuseNext = false;
pump.permissives = [
{ label: 'Suction valve XV-2104 open', met: false },
{ label: 'Seal water FS-2101', met: true },
{ label: 'No overload', met: true }
];
/* ── The stand-in plant ─────────────────────────────────────────────── */
/* The feedback a device gives to a command: a transient, then the end state. */
function answer(plate, command) {
const transient = { start: 'starting', stop: 'stopping', open: 'opening', close: 'closing' }[command],
final = { start: 'running', stop: 'stopped', open: 'open', close: 'closed' }[command];
setTimeout(function () {
if (refuseNext) {
refuseNext = false;
plate.refuse('Sequence holds ' + plate.tag);
log.textContent = plate.tag + ': the host refused the ' + command + ' - the plate shows why and stops waiting.';
return;
}
plate.commanded = command;
plate.state = transient;
setTimeout(function () {
plate.state = final;
if (plate === valve) {
/* the pump's permissive follows the valve */
pump.permissives = pump.permissives.map(function (permissive) {
return permissive.label.indexOf('XV-2104') === 0 ? { label: permissive.label, met: final === 'open' } : permissive;
});
}
log.textContent = plate.tag + ': ' + command + ' confirmed by the feedback' + (delay ? ' after ' + delay / 1000 + ' s' : '') + '.';
}, 700);
}, delay);
}
plates.forEach(function (plate) {
plate.addEventListener('commandRequest', function (event) {
log.textContent = event.detail.tag + ': ' + event.detail.command + ' requested - pending until the plant answers.';
answer(plate, event.detail.command);
});
plate.addEventListener('modeRequest', function (event) {
/* the plant grants the mode after a moment */
setTimeout(function () {
plate.mode = event.detail.mode;
log.textContent = event.detail.tag + ' is now in ' + event.detail.mode + '.';
}, 300);
});
plate.addEventListener('resetRequest', function (event) {
setTimeout(function () {
plate.faultText = '';
plate.state = 'stopped';
plate.commanded = 'stop';
log.textContent = event.detail.tag + ': fault reset, stopped.';
}, 500);
});
plate.addEventListener('blocked', function (event) {
log.textContent = event.detail.tag + ': ' + event.detail.command + ' not requested - ' + event.detail.reason + '.';
});
plate.addEventListener('writeTimeout', function (event) {
log.textContent = event.detail.tag + ': no feedback for the ' + event.detail.command + ' within ' + plate.pendingTimeout / 1000 + ' s - the request was dropped.';
});
});
/* ── The buttons ────────────────────────────────────────────────────── */
document.getElementById('slow').addEventListener('click', function () {
delay = delay ? 0 : 2000;
this.textContent = delay ? 'Answer at once' : 'Answer in 2 s';
});
document.getElementById('refuse').addEventListener('click', function () {
refuseNext = true;
log.textContent = 'The next request will be refused by the host.';
});
document.getElementById('trip').addEventListener('click', function () {
pump.faultText = 'Overload trip';
pump.state = 'fault';
pump.discrepancy = false;
log.textContent = 'P-2101A tripped: the fault is announced, and Reset appears.';
});
document.getElementById('permissive').addEventListener('click', function () {
const lost = pump.permissives.some(function (permissive) {
return permissive.label.indexOf('Seal water') === 0 && permissive.met;
});
pump.permissives = pump.permissives.map(function (permissive) {
return permissive.label.indexOf('Seal water') === 0 ? { label: permissive.label, met: !lost } : permissive;
});
this.textContent = lost ? 'Restore seal water' : 'Lose seal water';
log.textContent = lost ? 'Seal water flow lost: the pump may still be stopped, not started.' : 'Seal water back.';
});
/* the agitator's discrepancy: commanded start, feedback says stopped */
setTimeout(function () {
agitator.state = 'stopped';
agitator.discrepancy = true;
}, 9000);
}
angular/app.component.css
.smart-demo-container {
max-width: 900px;
}
section {
margin-bottom: 34px;
}
section h2 {
font-size: 16px;
font-weight: 500;
margin: 0 0 8px;
}
section p {
color: var(--smart-text-secondary-color);
margin: 0 0 14px;
max-width: 74ch;
}
code {
background: var(--smart-ui-state-hover);
font-family: Consolas, "Courier New", monospace;
font-size: 12px;
padding: 1px 5px;
}
.demo-actions {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-top: 14px;
}
.demo-actions button {
background: transparent;
border: 1px solid var(--smart-border);
border-radius: var(--smart-border-radius);
color: var(--smart-text-secondary-color);
cursor: pointer;
font-family: inherit;
font-size: 11px;
letter-spacing: .06em;
padding: 7px 14px;
text-transform: uppercase;
}
.demo-actions button:hover {
background: var(--smart-ui-state-hover);
border-color: var(--smart-ui-state-border-hover);
color: var(--smart-ui-state-color-hover);
}
.demo-log,
.demo-counts {
font-family: Consolas, "Courier New", monospace;
font-size: 12px;
margin: 12px 0 0;
color: var(--smart-text-tertiary-color);
}
smart-root-cause-tree {
display: block;
max-width: 720px;
padding: 6px 0;
border: 1px solid var(--smart-border);
border-radius: var(--smart-border-radius);
background: var(--smart-industrial-panel-background, var(--smart-surface));
}
.demo-ranked {
color: var(--smart-text-secondary-color);
font-size: 12px;
}
.demo-line {
display: flex;
flex-wrap: wrap;
gap: 22px;
align-items: flex-end;
padding: 16px 18px;
border: 1px solid var(--smart-border);
border-radius: var(--smart-border-radius);
}
.demo-station {
display: flex;
flex-direction: column;
align-items: center;
gap: 6px;
font-size: 12px;
color: var(--smart-text-secondary-color);
}
.demo-row {
display: flex;
flex-wrap: wrap;
gap: 28px;
align-items: flex-start;
}
.demo-actions label {
display: inline-flex;
align-items: center;
gap: 6px;
font-size: 13px;
}
.demo-actions select {
font: inherit;
padding: 4px 6px;
border: 1px solid var(--smart-border);
border-radius: var(--smart-border-radius);
background: var(--smart-background);
color: var(--smart-text-color);
}
.demo-strip {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.demo-strip smart-device-control {
--smart-device-width: 200px;
}
/* ── Demo chrome, shared by the Industrial demos ─────────────────────────
Headings, prose, buttons and notes are the same on every demo page. This block
is appended by scripts and comes last, so it wins over the rules above. */
.smart-demo-container {
padding: 4px 0 32px;
}
.smart-demo-container section {
margin-bottom: 36px;
}
.smart-demo-container section h2 {
margin: 0 0 8px;
font-size: 17px;
font-weight: 600;
line-height: 1.35;
color: var(--smart-text-color);
}
.smart-demo-container #overview h2 {
margin-bottom: 10px;
font-size: 20px;
max-width: 78ch;
}
.smart-demo-container section > p {
margin: 0 0 14px;
max-width: 84ch;
font-size: 14px;
line-height: 1.55;
color: var(--smart-text-secondary-color);
}
.smart-demo-container code {
padding: 1px 5px;
border-radius: 2px;
background: var(--smart-ui-state-hover);
font-family: Consolas, "Courier New", monospace;
font-size: 12px;
}
.smart-demo-container .demo-actions {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 8px 10px;
margin-top: 12px;
}
.smart-demo-container .demo-actions label {
display: inline-flex;
align-items: center;
gap: 6px;
font-size: 13px;
color: var(--smart-text-color);
}
.smart-demo-container .demo-actions button,
.smart-demo-container .demo-button {
min-height: 30px;
padding: 0 12px;
border: 1px solid var(--smart-border);
border-radius: 3px;
background: var(--smart-industrial-panel-background, var(--smart-surface));
color: var(--smart-text-color);
font: inherit;
font-size: 13px;
font-weight: 500;
letter-spacing: 0;
text-transform: none;
cursor: pointer;
}
.smart-demo-container .demo-actions button:hover,
.smart-demo-container .demo-button:hover {
border-color: var(--smart-ui-state-border-hover);
background: var(--smart-ui-state-hover);
color: var(--smart-ui-state-color-hover);
}
.smart-demo-container .demo-actions button.demo-button-primary,
.smart-demo-container .demo-button-primary {
border-color: var(--smart-primary);
background: var(--smart-primary);
color: var(--smart-primary-color, #fff);
}
.smart-demo-container .demo-actions button[disabled],
.smart-demo-container .demo-button[disabled] {
opacity: 0.5;
cursor: default;
}
.smart-demo-container .demo-actions select,
.smart-demo-container .demo-actions input[type="text"],
.smart-demo-container .demo-actions input[type="number"] {
min-height: 30px;
padding: 2px 6px;
border: 1px solid var(--smart-border);
border-radius: 3px;
background: var(--smart-background);
color: var(--smart-text-color);
font: inherit;
font-size: 13px;
}
.smart-demo-container .hint,
.smart-demo-container .demo-log,
.smart-demo-container .demo-counts {
margin: 10px 0 0;
font-size: 12px;
line-height: 1.5;
color: var(--smart-text-secondary-color);
opacity: 1;
}
.smart-demo-container .demo-log,
.smart-demo-container .demo-counts {
font-family: Consolas, "Courier New", monospace;
}
.smart-demo-container .log {
list-style: none;
margin: 10px 0 0;
padding: 0;
font-size: 12px;
font-variant-numeric: tabular-nums;
color: var(--smart-text-secondary-color);
opacity: 1;
}
.smart-demo-container .log li {
padding: 2px 0;
}
The same demo in React, generated from the HTML demo. The frameworks guide has the project setup.
react/App.jsx
import 'smart-industrial/source/styles/smart.default.css';
import 'smart-industrial/source/styles/smart.industrial-elements.css';
import 'smart-industrial/source/styles/smart.industrial.css';
import 'smart-industrial/source/styles/smart.industrial-dark.css';
import './App.css';
import React from 'react';
import { DeviceControl } from 'smart-industrial/react/devicecontrol';
/* The demo logic is the HTML demo's: it feeds the elements the way a plant's data would. */
/* Device control overview.
*
* What the page shows is what the host has to do, because the plate deliberately does not:
* every request arrives as an event, the host writes the plant, and the plate moves only when
* the feedback comes back. A stand-in plant answers here, with a delay and a refusal on
* request, so the pending state and the refusal can be watched. */
const App = () => {
const root = React.useRef(null);
React.useEffect(() => {
/* The elements render after React has mounted them, and the demo's logic - written
for a page's load event - reads their parts, so it waits for every element in the
tree to have rendered first. */
const pending = Array.from(root.current.querySelectorAll('*'))
.filter((element) => typeof element.whenRendered === 'function' && !element.isRendered)
.map((element) => element.whenRendered());
let unmounted = false;
Promise.all(pending).then(() => {
if (unmounted) {
return;
}
const pump = document.getElementById('pump'),
valve = document.getElementById('valve'),
agitator = document.getElementById('agitator'),
log = document.getElementById('log'),
plates = [pump, valve, agitator, document.getElementById('local'), document.getElementById('interlocked'), document.getElementById('readonly')];
let delay = 0,
refuseNext = false;
pump.permissives = [
{ label: 'Suction valve XV-2104 open', met: false },
{ label: 'Seal water FS-2101', met: true },
{ label: 'No overload', met: true }
];
/* ── The stand-in plant ─────────────────────────────────────────────── */
/* The feedback a device gives to a command: a transient, then the end state. */
function answer(plate, command) {
const transient = { start: 'starting', stop: 'stopping', open: 'opening', close: 'closing' }[command],
final = { start: 'running', stop: 'stopped', open: 'open', close: 'closed' }[command];
setTimeout(function () {
if (refuseNext) {
refuseNext = false;
plate.refuse('Sequence holds ' + plate.tag);
log.textContent = plate.tag + ': the host refused the ' + command + ' - the plate shows why and stops waiting.';
return;
}
plate.commanded = command;
plate.state = transient;
setTimeout(function () {
plate.state = final;
if (plate === valve) {
/* the pump's permissive follows the valve */
pump.permissives = pump.permissives.map(function (permissive) {
return permissive.label.indexOf('XV-2104') === 0 ? { label: permissive.label, met: final === 'open' } : permissive;
});
}
log.textContent = plate.tag + ': ' + command + ' confirmed by the feedback' + (delay ? ' after ' + delay / 1000 + ' s' : '') + '.';
}, 700);
}, delay);
}
plates.forEach(function (plate) {
plate.addEventListener('commandRequest', function (event) {
log.textContent = event.detail.tag + ': ' + event.detail.command + ' requested - pending until the plant answers.';
answer(plate, event.detail.command);
});
plate.addEventListener('modeRequest', function (event) {
/* the plant grants the mode after a moment */
setTimeout(function () {
plate.mode = event.detail.mode;
log.textContent = event.detail.tag + ' is now in ' + event.detail.mode + '.';
}, 300);
});
plate.addEventListener('resetRequest', function (event) {
setTimeout(function () {
plate.faultText = '';
plate.state = 'stopped';
plate.commanded = 'stop';
log.textContent = event.detail.tag + ': fault reset, stopped.';
}, 500);
});
plate.addEventListener('blocked', function (event) {
log.textContent = event.detail.tag + ': ' + event.detail.command + ' not requested - ' + event.detail.reason + '.';
});
plate.addEventListener('writeTimeout', function (event) {
log.textContent = event.detail.tag + ': no feedback for the ' + event.detail.command + ' within ' + plate.pendingTimeout / 1000 + ' s - the request was dropped.';
});
});
/* ── The buttons ────────────────────────────────────────────────────── */
document.getElementById('slow').addEventListener('click', function () {
delay = delay ? 0 : 2000;
this.textContent = delay ? 'Answer at once' : 'Answer in 2 s';
});
document.getElementById('refuse').addEventListener('click', function () {
refuseNext = true;
log.textContent = 'The next request will be refused by the host.';
});
document.getElementById('trip').addEventListener('click', function () {
pump.faultText = 'Overload trip';
pump.state = 'fault';
pump.discrepancy = false;
log.textContent = 'P-2101A tripped: the fault is announced, and Reset appears.';
});
document.getElementById('permissive').addEventListener('click', function () {
const lost = pump.permissives.some(function (permissive) {
return permissive.label.indexOf('Seal water') === 0 && permissive.met;
});
pump.permissives = pump.permissives.map(function (permissive) {
return permissive.label.indexOf('Seal water') === 0 ? { label: permissive.label, met: !lost } : permissive;
});
this.textContent = lost ? 'Restore seal water' : 'Lose seal water';
log.textContent = lost ? 'Seal water flow lost: the pump may still be stopped, not started.' : 'Seal water back.';
});
/* the agitator's discrepancy: commanded start, feedback says stopped */
setTimeout(function () {
agitator.state = 'stopped';
agitator.discrepancy = true;
}, 9000);
});
return () => {
unmounted = true;
};
}, []);
return (
<div className="react-root" ref={root} theme="industrial">
<div className="smart-demo-container">
<section id="overview">
<h2>Smart.DeviceControl is a faceplate for a discrete device such as a motor, a pump or an on/off valve.</h2>
<p>The faceplate shows the device feedback (running, stopped, open or closed, as reported by the device) next to the commanded state, the operating mode that determines who may command the device, and the interlock and permissives that determine whether a command is accepted. The component does not write to the plant. A command button raises a request event, the application performs the write, and the faceplate updates when the new feedback is reported. A command that cannot be requested shows the reason.</p>
</section>
<section>
<h2>A pump, a valve and a motor</h2>
<div className="demo-row">
<DeviceControl id="pump" tag="P-2101A" description="Feed pump A" kind="pump" state="stopped" commanded="stop" mode="manual" interactive runHours={1842.5} startCount={312}></DeviceControl>
<DeviceControl id="valve" tag="XV-2104" description="Feed isolation" kind="valve" state="closed" commanded="close" mode="manual" interactive></DeviceControl>
<DeviceControl id="agitator" tag="M-2301" description="Reactor agitator" kind="motor" state="running" commanded="start" mode="auto" interactive runHours={9930} startCount={41}></DeviceControl>
</div>
<div className="demo-actions">
<button type="button" id="slow">Answer in 2 s</button>
<button type="button" id="refuse">Refuse the next request</button>
<button type="button" id="trip">Trip the pump</button>
<button type="button" id="permissive">Lose seal water</button>
</div>
<p className="demo-log" id="log">Click Start on the pump. The faceplate shows the request as pending until the application reports the feedback. The agitator is in auto mode, so it has to be switched to manual before it accepts a command.</p>
</section>
<section>
<h2>Blocked commands</h2>
<p>A device under local control or behind an interlock can be stopped from the screen but not started, and a faceplate whose <code>interactive</code> property is not set accepts no commands. The command buttons stay readable and show the reason when pressed.</p>
<div className="demo-row">
<DeviceControl id="local" tag="P-2101B" description="Feed pump B" kind="pump" state="running" commanded="start" mode="local" interactive></DeviceControl>
<DeviceControl id="interlocked" tag="M-2302" description="Transfer conveyor" kind="motor" state="stopped" commanded="stop" mode="manual" interactive interlocked interlockReason="Downstream gate closed"></DeviceControl>
<DeviceControl id="readonly" tag="XV-2109" description="Vent" kind="valve" state="open" commanded="open" mode="manual" quality="stale"></DeviceControl>
</div>
</section>
<section>
<h2>Compact mode</h2>
<div className="demo-strip">
<DeviceControl tag="P-2101A" kind="pump" state="running" commanded="start" mode="auto" compact></DeviceControl>
<DeviceControl tag="P-2101B" kind="pump" state="stopped" commanded="stop" mode="auto" compact></DeviceControl>
<DeviceControl tag="XV-2104" kind="valve" state="open" commanded="open" mode="auto" compact></DeviceControl>
<DeviceControl tag="M-2301" kind="motor" state="fault" commanded="start" mode="auto" faultText="Overload" compact></DeviceControl>
</div>
</section>
</div>
</div>
);
};
export default App;
react/App.css
.smart-demo-container {
max-width: 900px;
}
section {
margin-bottom: 34px;
}
section h2 {
font-size: 16px;
font-weight: 500;
margin: 0 0 8px;
}
section p {
color: var(--smart-text-secondary-color);
margin: 0 0 14px;
max-width: 74ch;
}
code {
background: var(--smart-ui-state-hover);
font-family: Consolas, "Courier New", monospace;
font-size: 12px;
padding: 1px 5px;
}
.demo-actions {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-top: 14px;
}
.demo-actions button {
background: transparent;
border: 1px solid var(--smart-border);
border-radius: var(--smart-border-radius);
color: var(--smart-text-secondary-color);
cursor: pointer;
font-family: inherit;
font-size: 11px;
letter-spacing: .06em;
padding: 7px 14px;
text-transform: uppercase;
}
.demo-actions button:hover {
background: var(--smart-ui-state-hover);
border-color: var(--smart-ui-state-border-hover);
color: var(--smart-ui-state-color-hover);
}
.demo-log,
.demo-counts {
font-family: Consolas, "Courier New", monospace;
font-size: 12px;
margin: 12px 0 0;
color: var(--smart-text-tertiary-color);
}
smart-root-cause-tree {
display: block;
max-width: 720px;
padding: 6px 0;
border: 1px solid var(--smart-border);
border-radius: var(--smart-border-radius);
background: var(--smart-industrial-panel-background, var(--smart-surface));
}
.demo-ranked {
color: var(--smart-text-secondary-color);
font-size: 12px;
}
.demo-line {
display: flex;
flex-wrap: wrap;
gap: 22px;
align-items: flex-end;
padding: 16px 18px;
border: 1px solid var(--smart-border);
border-radius: var(--smart-border-radius);
}
.demo-station {
display: flex;
flex-direction: column;
align-items: center;
gap: 6px;
font-size: 12px;
color: var(--smart-text-secondary-color);
}
.demo-row {
display: flex;
flex-wrap: wrap;
gap: 28px;
align-items: flex-start;
}
.demo-actions label {
display: inline-flex;
align-items: center;
gap: 6px;
font-size: 13px;
}
.demo-actions select {
font: inherit;
padding: 4px 6px;
border: 1px solid var(--smart-border);
border-radius: var(--smart-border-radius);
background: var(--smart-background);
color: var(--smart-text-color);
}
.demo-strip {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.demo-strip smart-device-control {
--smart-device-width: 200px;
}
/* ── Demo chrome, shared by the Industrial demos ─────────────────────────
Headings, prose, buttons and notes are the same on every demo page. This block
is appended by scripts and comes last, so it wins over the rules above. */
.smart-demo-container {
padding: 4px 0 32px;
}
.smart-demo-container section {
margin-bottom: 36px;
}
.smart-demo-container section h2 {
margin: 0 0 8px;
font-size: 17px;
font-weight: 600;
line-height: 1.35;
color: var(--smart-text-color);
}
.smart-demo-container #overview h2 {
margin-bottom: 10px;
font-size: 20px;
max-width: 78ch;
}
.smart-demo-container section > p {
margin: 0 0 14px;
max-width: 84ch;
font-size: 14px;
line-height: 1.55;
color: var(--smart-text-secondary-color);
}
.smart-demo-container code {
padding: 1px 5px;
border-radius: 2px;
background: var(--smart-ui-state-hover);
font-family: Consolas, "Courier New", monospace;
font-size: 12px;
}
.smart-demo-container .demo-actions {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 8px 10px;
margin-top: 12px;
}
.smart-demo-container .demo-actions label {
display: inline-flex;
align-items: center;
gap: 6px;
font-size: 13px;
color: var(--smart-text-color);
}
.smart-demo-container .demo-actions button,
.smart-demo-container .demo-button {
min-height: 30px;
padding: 0 12px;
border: 1px solid var(--smart-border);
border-radius: 3px;
background: var(--smart-industrial-panel-background, var(--smart-surface));
color: var(--smart-text-color);
font: inherit;
font-size: 13px;
font-weight: 500;
letter-spacing: 0;
text-transform: none;
cursor: pointer;
}
.smart-demo-container .demo-actions button:hover,
.smart-demo-container .demo-button:hover {
border-color: var(--smart-ui-state-border-hover);
background: var(--smart-ui-state-hover);
color: var(--smart-ui-state-color-hover);
}
.smart-demo-container .demo-actions button.demo-button-primary,
.smart-demo-container .demo-button-primary {
border-color: var(--smart-primary);
background: var(--smart-primary);
color: var(--smart-primary-color, #fff);
}
.smart-demo-container .demo-actions button[disabled],
.smart-demo-container .demo-button[disabled] {
opacity: 0.5;
cursor: default;
}
.smart-demo-container .demo-actions select,
.smart-demo-container .demo-actions input[type="text"],
.smart-demo-container .demo-actions input[type="number"] {
min-height: 30px;
padding: 2px 6px;
border: 1px solid var(--smart-border);
border-radius: 3px;
background: var(--smart-background);
color: var(--smart-text-color);
font: inherit;
font-size: 13px;
}
.smart-demo-container .hint,
.smart-demo-container .demo-log,
.smart-demo-container .demo-counts {
margin: 10px 0 0;
font-size: 12px;
line-height: 1.5;
color: var(--smart-text-secondary-color);
opacity: 1;
}
.smart-demo-container .demo-log,
.smart-demo-container .demo-counts {
font-family: Consolas, "Courier New", monospace;
}
.smart-demo-container .log {
list-style: none;
margin: 10px 0 0;
padding: 0;
font-size: 12px;
font-variant-numeric: tabular-nums;
color: var(--smart-text-secondary-color);
opacity: 1;
}
.smart-demo-container .log li {
padding: 2px 0;
}
The same demo in Vue, generated from the HTML demo. The frameworks guide has the project setup.
vue/App.vue
<template>
<div class="vue-root" theme="industrial" v-pre>
<div class="smart-demo-container">
<section id="overview">
<h2>Smart.DeviceControl is a faceplate for a discrete device such as a motor, a pump or an on/off valve.</h2>
<p>
The faceplate shows the device feedback (running, stopped, open or closed, as reported by the device) next to the commanded state, the operating mode that determines who may command the device, and the interlock and permissives that determine whether a command is accepted. The component does not write to the plant. A command button raises a request event, the application performs the write, and the faceplate updates when the new feedback is reported. A command that cannot be requested shows the reason.
</p>
</section>
<section>
<h2>A pump, a valve and a motor</h2>
<div class="demo-row">
<smart-device-control id="pump" tag="P-2101A" description="Feed pump A" kind="pump" state="stopped" commanded="stop" mode="manual" interactive run-hours="1842.5" start-count="312"></smart-device-control>
<smart-device-control id="valve" tag="XV-2104" description="Feed isolation" kind="valve" state="closed" commanded="close" mode="manual" interactive></smart-device-control>
<smart-device-control id="agitator" tag="M-2301" description="Reactor agitator" kind="motor" state="running" commanded="start" mode="auto" interactive run-hours="9930" start-count="41"></smart-device-control>
</div>
<div class="demo-actions">
<button type="button" id="slow">Answer in 2 s</button>
<button type="button" id="refuse">Refuse the next request</button>
<button type="button" id="trip">Trip the pump</button>
<button type="button" id="permissive">Lose seal water</button>
</div>
<p class="demo-log" id="log">Click Start on the pump. The faceplate shows the request as pending until the application reports the feedback. The agitator is in auto mode, so it has to be switched to manual before it accepts a command.</p>
</section>
<section>
<h2>Blocked commands</h2>
<p>
A device under local control or behind an interlock can be stopped from the screen but not started, and a faceplate whose <code>interactive</code> property is not set accepts no commands. The command buttons stay readable and show the reason when pressed.
</p>
<div class="demo-row">
<smart-device-control id="local" tag="P-2101B" description="Feed pump B" kind="pump" state="running" commanded="start" mode="local" interactive></smart-device-control>
<smart-device-control id="interlocked" tag="M-2302" description="Transfer conveyor" kind="motor" state="stopped" commanded="stop" mode="manual" interactive interlocked interlock-reason="Downstream gate closed"></smart-device-control>
<smart-device-control id="readonly" tag="XV-2109" description="Vent" kind="valve" state="open" commanded="open" mode="manual" quality="stale"></smart-device-control>
</div>
</section>
<section>
<h2>Compact mode</h2>
<div class="demo-strip">
<smart-device-control tag="P-2101A" kind="pump" state="running" commanded="start" mode="auto" compact></smart-device-control>
<smart-device-control tag="P-2101B" kind="pump" state="stopped" commanded="stop" mode="auto" compact></smart-device-control>
<smart-device-control tag="XV-2104" kind="valve" state="open" commanded="open" mode="auto" compact></smart-device-control>
<smart-device-control tag="M-2301" kind="motor" state="fault" commanded="start" mode="auto" fault-text="Overload" compact></smart-device-control>
</div>
</section>
</div>
</div>
</template>
<script>
import { onMounted } from "vue";
import "smart-industrial/source/styles/smart.default.css";
import "smart-industrial/source/styles/smart.industrial-elements.css";
import "smart-industrial/source/styles/smart.industrial.css";
import "smart-industrial/source/styles/smart.industrial-dark.css";
import "smart-industrial/source/modules/smart.devicecontrol.js";
/* Device control overview.
*
* What the page shows is what the host has to do, because the plate deliberately does not:
* every request arrives as an event, the host writes the plant, and the plate moves only when
* the feedback comes back. A stand-in plant answers here, with a delay and a refusal on
* request, so the pending state and the refusal can be watched. */
export default {
name: "app",
setup() {
onMounted(() => {
/* The elements render after Vue has mounted them, and the demo's logic - written for
a page's load event - reads their parts, so it waits for every element in the tree
to have rendered first. */
const pending = Array.from(document.querySelector(".vue-root").querySelectorAll("*"))
.filter((element) => typeof element.whenRendered === "function" && !element.isRendered)
.map((element) => element.whenRendered());
Promise.all(pending).then(() => {
const pump = document.getElementById('pump'),
valve = document.getElementById('valve'),
agitator = document.getElementById('agitator'),
log = document.getElementById('log'),
plates = [pump, valve, agitator, document.getElementById('local'), document.getElementById('interlocked'), document.getElementById('readonly')];
let delay = 0,
refuseNext = false;
pump.permissives = [
{ label: 'Suction valve XV-2104 open', met: false },
{ label: 'Seal water FS-2101', met: true },
{ label: 'No overload', met: true }
];
/* ── The stand-in plant ─────────────────────────────────────────────── */
/* The feedback a device gives to a command: a transient, then the end state. */
function answer(plate, command) {
const transient = { start: 'starting', stop: 'stopping', open: 'opening', close: 'closing' }[command],
final = { start: 'running', stop: 'stopped', open: 'open', close: 'closed' }[command];
setTimeout(function () {
if (refuseNext) {
refuseNext = false;
plate.refuse('Sequence holds ' + plate.tag);
log.textContent = plate.tag + ': the host refused the ' + command + ' - the plate shows why and stops waiting.';
return;
}
plate.commanded = command;
plate.state = transient;
setTimeout(function () {
plate.state = final;
if (plate === valve) {
/* the pump's permissive follows the valve */
pump.permissives = pump.permissives.map(function (permissive) {
return permissive.label.indexOf('XV-2104') === 0 ? { label: permissive.label, met: final === 'open' } : permissive;
});
}
log.textContent = plate.tag + ': ' + command + ' confirmed by the feedback' + (delay ? ' after ' + delay / 1000 + ' s' : '') + '.';
}, 700);
}, delay);
}
plates.forEach(function (plate) {
plate.addEventListener('commandRequest', function (event) {
log.textContent = event.detail.tag + ': ' + event.detail.command + ' requested - pending until the plant answers.';
answer(plate, event.detail.command);
});
plate.addEventListener('modeRequest', function (event) {
/* the plant grants the mode after a moment */
setTimeout(function () {
plate.mode = event.detail.mode;
log.textContent = event.detail.tag + ' is now in ' + event.detail.mode + '.';
}, 300);
});
plate.addEventListener('resetRequest', function (event) {
setTimeout(function () {
plate.faultText = '';
plate.state = 'stopped';
plate.commanded = 'stop';
log.textContent = event.detail.tag + ': fault reset, stopped.';
}, 500);
});
plate.addEventListener('blocked', function (event) {
log.textContent = event.detail.tag + ': ' + event.detail.command + ' not requested - ' + event.detail.reason + '.';
});
plate.addEventListener('writeTimeout', function (event) {
log.textContent = event.detail.tag + ': no feedback for the ' + event.detail.command + ' within ' + plate.pendingTimeout / 1000 + ' s - the request was dropped.';
});
});
/* ── The buttons ────────────────────────────────────────────────────── */
document.getElementById('slow').addEventListener('click', function () {
delay = delay ? 0 : 2000;
this.textContent = delay ? 'Answer at once' : 'Answer in 2 s';
});
document.getElementById('refuse').addEventListener('click', function () {
refuseNext = true;
log.textContent = 'The next request will be refused by the host.';
});
document.getElementById('trip').addEventListener('click', function () {
pump.faultText = 'Overload trip';
pump.state = 'fault';
pump.discrepancy = false;
log.textContent = 'P-2101A tripped: the fault is announced, and Reset appears.';
});
document.getElementById('permissive').addEventListener('click', function () {
const lost = pump.permissives.some(function (permissive) {
return permissive.label.indexOf('Seal water') === 0 && permissive.met;
});
pump.permissives = pump.permissives.map(function (permissive) {
return permissive.label.indexOf('Seal water') === 0 ? { label: permissive.label, met: !lost } : permissive;
});
this.textContent = lost ? 'Restore seal water' : 'Lose seal water';
log.textContent = lost ? 'Seal water flow lost: the pump may still be stopped, not started.' : 'Seal water back.';
});
/* the agitator's discrepancy: commanded start, feedback says stopped */
setTimeout(function () {
agitator.state = 'stopped';
agitator.discrepancy = true;
}, 9000);
});
});
}
};
</script>
<style>
.smart-demo-container {
max-width: 900px;
}
section {
margin-bottom: 34px;
}
section h2 {
font-size: 16px;
font-weight: 500;
margin: 0 0 8px;
}
section p {
color: var(--smart-text-secondary-color);
margin: 0 0 14px;
max-width: 74ch;
}
code {
background: var(--smart-ui-state-hover);
font-family: Consolas, "Courier New", monospace;
font-size: 12px;
padding: 1px 5px;
}
.demo-actions {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-top: 14px;
}
.demo-actions button {
background: transparent;
border: 1px solid var(--smart-border);
border-radius: var(--smart-border-radius);
color: var(--smart-text-secondary-color);
cursor: pointer;
font-family: inherit;
font-size: 11px;
letter-spacing: .06em;
padding: 7px 14px;
text-transform: uppercase;
}
.demo-actions button:hover {
background: var(--smart-ui-state-hover);
border-color: var(--smart-ui-state-border-hover);
color: var(--smart-ui-state-color-hover);
}
.demo-log,
.demo-counts {
font-family: Consolas, "Courier New", monospace;
font-size: 12px;
margin: 12px 0 0;
color: var(--smart-text-tertiary-color);
}
smart-root-cause-tree {
display: block;
max-width: 720px;
padding: 6px 0;
border: 1px solid var(--smart-border);
border-radius: var(--smart-border-radius);
background: var(--smart-industrial-panel-background, var(--smart-surface));
}
.demo-ranked {
color: var(--smart-text-secondary-color);
font-size: 12px;
}
.demo-line {
display: flex;
flex-wrap: wrap;
gap: 22px;
align-items: flex-end;
padding: 16px 18px;
border: 1px solid var(--smart-border);
border-radius: var(--smart-border-radius);
}
.demo-station {
display: flex;
flex-direction: column;
align-items: center;
gap: 6px;
font-size: 12px;
color: var(--smart-text-secondary-color);
}
.demo-row {
display: flex;
flex-wrap: wrap;
gap: 28px;
align-items: flex-start;
}
.demo-actions label {
display: inline-flex;
align-items: center;
gap: 6px;
font-size: 13px;
}
.demo-actions select {
font: inherit;
padding: 4px 6px;
border: 1px solid var(--smart-border);
border-radius: var(--smart-border-radius);
background: var(--smart-background);
color: var(--smart-text-color);
}
.demo-strip {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.demo-strip smart-device-control {
--smart-device-width: 200px;
}
/* ── Demo chrome, shared by the Industrial demos ─────────────────────────
Headings, prose, buttons and notes are the same on every demo page. This block
is appended by scripts and comes last, so it wins over the rules above. */
.smart-demo-container {
padding: 4px 0 32px;
}
.smart-demo-container section {
margin-bottom: 36px;
}
.smart-demo-container section h2 {
margin: 0 0 8px;
font-size: 17px;
font-weight: 600;
line-height: 1.35;
color: var(--smart-text-color);
}
.smart-demo-container #overview h2 {
margin-bottom: 10px;
font-size: 20px;
max-width: 78ch;
}
.smart-demo-container section > p {
margin: 0 0 14px;
max-width: 84ch;
font-size: 14px;
line-height: 1.55;
color: var(--smart-text-secondary-color);
}
.smart-demo-container code {
padding: 1px 5px;
border-radius: 2px;
background: var(--smart-ui-state-hover);
font-family: Consolas, "Courier New", monospace;
font-size: 12px;
}
.smart-demo-container .demo-actions {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 8px 10px;
margin-top: 12px;
}
.smart-demo-container .demo-actions label {
display: inline-flex;
align-items: center;
gap: 6px;
font-size: 13px;
color: var(--smart-text-color);
}
.smart-demo-container .demo-actions button,
.smart-demo-container .demo-button {
min-height: 30px;
padding: 0 12px;
border: 1px solid var(--smart-border);
border-radius: 3px;
background: var(--smart-industrial-panel-background, var(--smart-surface));
color: var(--smart-text-color);
font: inherit;
font-size: 13px;
font-weight: 500;
letter-spacing: 0;
text-transform: none;
cursor: pointer;
}
.smart-demo-container .demo-actions button:hover,
.smart-demo-container .demo-button:hover {
border-color: var(--smart-ui-state-border-hover);
background: var(--smart-ui-state-hover);
color: var(--smart-ui-state-color-hover);
}
.smart-demo-container .demo-actions button.demo-button-primary,
.smart-demo-container .demo-button-primary {
border-color: var(--smart-primary);
background: var(--smart-primary);
color: var(--smart-primary-color, #fff);
}
.smart-demo-container .demo-actions button[disabled],
.smart-demo-container .demo-button[disabled] {
opacity: 0.5;
cursor: default;
}
.smart-demo-container .demo-actions select,
.smart-demo-container .demo-actions input[type="text"],
.smart-demo-container .demo-actions input[type="number"] {
min-height: 30px;
padding: 2px 6px;
border: 1px solid var(--smart-border);
border-radius: 3px;
background: var(--smart-background);
color: var(--smart-text-color);
font: inherit;
font-size: 13px;
}
.smart-demo-container .hint,
.smart-demo-container .demo-log,
.smart-demo-container .demo-counts {
margin: 10px 0 0;
font-size: 12px;
line-height: 1.5;
color: var(--smart-text-secondary-color);
opacity: 1;
}
.smart-demo-container .demo-log,
.smart-demo-container .demo-counts {
font-family: Consolas, "Courier New", monospace;
}
.smart-demo-container .log {
list-style: none;
margin: 10px 0 0;
padding: 0;
font-size: 12px;
font-variant-numeric: tabular-nums;
color: var(--smart-text-secondary-color);
opacity: 1;
}
.smart-demo-container .log li {
padding: 2px 0;
}
</style>
What the demo shows
- A pump, a valve and a motor
- Blocked commands
- Compact mode