Operator screen
Units
SI, US customary and a site-specific unit system on one screen.
index.htm as it is written with the smart-industrial package; industrial-demos.css is the screens' shared layout
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" lang="en">
<head>
<title>Unit systems - Smart.Industrial</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="industrial-demos.css" />
<link rel="stylesheet" type="text/css" href="../connectivity/styles.css" />
<link rel="stylesheet" type="text/css" href="styles.css" />
</head>
<body class="viewport" theme="industrial-dark">
<div class="hmi-screen">
<header class="hmi-header">
<div class="hmi-identity">
<span class="hmi-plant">UNIT SYSTEMS</span>
<span class="hmi-area">One historian, one screen, two sites that read it differently</span>
</div>
<div class="hmi-header-status">
<span class="cx-hint" id="systemLabel">Reading in the tags' own units</span>
<button class="cx-button" id="toSource" type="button" aria-pressed="true">As stored</button>
<button class="cx-button" id="toSi" type="button" aria-pressed="false">SI</button>
<button class="cx-button" id="toUs" type="button" aria-pressed="false">US customary</button>
<button class="cx-button" id="toSite" type="button" aria-pressed="false">Site (kPag, °C, L/min)</button>
<button class="hmi-theme-toggle" id="themeToggle" type="button">Light</button>
</div>
</header>
<section class="cx-note" aria-label="What this screen demonstrates">
<p>
The tags on this screen arrive in the units the historian stores them in: m³/h, barg, °C, kg/h and rpm. Each binding names the <code>unit</code> of the tag, <code>Smart.Industrial.Units.system</code> names the unit system used by the site, and <code>Connect.bind()</code> converts every sample on its way to the property and updates the unit, the scale and the alarm limits of the component.
</p>
<p class="cx-note-strong">
The header pressure is a <b>gauge</b> pressure: in the US system it reads psig, not psi, and the absolute readout next to it includes the atmospheric pressure. The jacket ΔT is a temperature <b>difference</b>: 10 K of rise is 18 °F of rise, not 50 °F. The converted readouts keep their <b>resolution</b>, so 0.1 mm does not become 0.0 in.
</p>
</section>
<section class="cx-tiles" aria-label="Bound status tiles">
<smart-status-tile id="tileFlow" label="Feed flow" unit="m³/h" precision-digits="1"></smart-status-tile>
<smart-status-tile id="tilePressure" label="Header pressure" unit="barg" precision-digits="2"></smart-status-tile>
<smart-status-tile id="tileAbsolute" label="Header pressure, absolute" unit="bar" precision-digits="2"></smart-status-tile>
<smart-status-tile id="tileMass" label="Product mass flow" unit="kg/h" precision-digits="0"></smart-status-tile>
<smart-status-tile id="tileSpeed" label="Agitator speed" unit="rpm" precision-digits="0"></smart-status-tile>
<smart-status-tile id="tileVibration" label="Bearing displacement" unit="µm" precision-digits="1"></smart-status-tile>
</section>
<section class="cx-main">
<div class="cx-panel">
<h2 class="cx-panel-title">Bound loops with their scales</h2>
<p class="cx-panel-sub">
<code>Connect.bind(loop, { processValue: { tag: 'FIC-101.PV', unit: 'm³/h' }, setpoint: { tag: 'FIC-101.SP', unit: 'm³/h' } })</code> converts the 0–120 m³/h scale and the 100 m³/h high alarm together with the value, so the needle stays in the same position.
</p>
<div class="cx-faceplates">
<smart-faceplate id="loopFlow" tag="FIC-101" description="Feed flow control"
unit="m³/h" min="0" max="120" precision-digits="1"></smart-faceplate>
<smart-faceplate id="loopTemp" tag="TIC-204" description="Jacket temperature"
unit="°C" min="0" max="150" precision-digits="1"></smart-faceplate>
</div>
</div>
<div class="cx-panel">
<h2 class="cx-panel-title">Temperature differences</h2>
<p class="cx-panel-sub">
The jacket rise of TIC-204 is bound with <code>unit: 'ΔK'</code>. Converted as a temperature it would read 50 °F for 10 K; converted as a difference it reads 18 °F. The <code>Units</code> table keeps the two as separate quantities.
</p>
<div class="un-readouts">
<div class="un-readout">
<span class="un-readout-label">Jacket ΔT</span>
<b id="deltaT" class="un-readout-value">--</b>
</div>
<div class="un-readout">
<span class="un-readout-label">Same number, wrongly as a temperature</span>
<b id="deltaWrong" class="un-readout-value un-readout-wrong">--</b>
</div>
<div class="un-readout">
<span class="un-readout-label">Header pressure, gauge → absolute</span>
<b id="gaugeAbs" class="un-readout-value">--</b>
</div>
<div class="un-readout">
<span class="un-readout-label">Bearing displacement to 0.1 µm</span>
<b id="resolution" class="un-readout-value">--</b>
</div>
</div>
</div>
</section>
<section class="cx-panel cx-wide" aria-label="The conversions in force">
<h2 class="cx-panel-title">Binding details</h2>
<p class="cx-panel-sub">
The source unit, the display unit and the number of decimals for each binding, as resolved for the selected unit system.
</p>
<table class="cx-samples">
<thead>
<tr>
<th scope="col">Tag</th>
<th scope="col">Stored as</th>
<th scope="col">Last value</th>
<th scope="col">Displayed as</th>
<th scope="col">Decimals</th>
<th scope="col">Quantity</th>
</tr>
</thead>
<tbody id="conversionRows"></tbody>
</table>
</section>
</div>
<!-- scripts -->
<script type="module" src="node_modules/smart-industrial/source/modules/smart.industrial.connect.js"></script>
<script type="module" src="node_modules/smart-industrial/source/modules/smart.industrial.units.js"></script>
<script type="module" src="node_modules/smart-industrial/source/modules/smart.faceplate.js"></script>
<script type="module" src="node_modules/smart-industrial/source/modules/smart.statustile.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>
<script>
/* The site's light/dark switch is the reader's choice: a screen opened from the light site
in a new tab opens light too. Framed, the site's page sets the theme itself. */
addEventListener('load', function () {
var light = false,
toggle = document.querySelector('.hmi-theme-toggle');
try { light = window.top === window && localStorage.getItem('smart-industrial-dark') === 'false'; } catch (e) { }
/* after the screen's own onload, which is where its button starts listening */
if (light && toggle) { setTimeout(function () { if (document.body.getAttribute('theme') === 'industrial-dark') { toggle.click(); } }, 0); }
});
</script>
</body>
</html>
index.js
/// <reference path="../../../source/typescript/smart.elements.d.ts" />
/* Unit systems. The historian's tags are simulated in the units a European site stores
them in; the bindings name those units and nothing else. Switching the system changes
Smart.Industrial.Units.system and lets the next sample do the rest - the value, the
element's unit, its scale and its alarm limits all move together. */
window.onload = function () {
const Connect = window.Smart.Industrial.Connect,
Units = window.Smart.Industrial.Units;
/* A site that reads pressure as kPag, temperature in °C and small flows in L/min. */
Units.defineSystem('site', { pressure: 'kPag', temperature: '°C', volumetricFlow: 'L/min', length: 'µm' });
const field = Connect.open('simulator', {
interval: 250,
tags: {
'FIC-101.PV': { min: 62, max: 96, period: 37000, noise: 0.01 },
'FIC-101.SP': { kind: 'step', min: 78, max: 78, noise: 0 },
'FIC-101.OP': { min: 35, max: 65, period: 37000, noise: 0.01 },
'PIC-102.PV': { min: 2.2, max: 3.6, period: 23000, noise: 0.02 },
'TIC-204.PV': { min: 58, max: 74, period: 41000, noise: 0.02 },
'TIC-204.SP': { kind: 'step', min: 65, max: 65, noise: 0 },
'TIC-204.OP': { min: 20, max: 60, period: 41000, noise: 0.01 },
'TIC-204.DT': { min: 8, max: 12, period: 29000, noise: 0.02 },
'WIC-310.PV': { min: 1800, max: 2400, period: 53000, noise: 0.01 },
'SI-410.PV': { min: 118, max: 122, period: 17000, noise: 0.005 },
'VI-410.PV': { min: 18.2, max: 31.6, period: 19000, noise: 0.03 }
}
});
/* Two tiles name their display unit outright and re-choose it with the system: absolute
pressure reads psia beside psig, and a vibration analyst reads displacement in mil, not
in a six-decimal inch. A binding's units are fixed for its life, so these are rebound. */
const OVERRIDES = {
tileAbsolute: { '': 'bar', si: 'bar', us: 'psia', site: 'kPa' },
tileVibration: { '': 'µm', si: 'µm', us: 'mil', site: 'µm' }
};
/* ── Bindings: the tag and the unit it is stored in, nothing about display ────── */
const BINDINGS = [
{ element: 'loopFlow', map: { processValue: { tag: 'FIC-101.PV', unit: 'm³/h' }, setpoint: { tag: 'FIC-101.SP', unit: 'm³/h' }, output: 'FIC-101.OP' } },
{ element: 'loopTemp', map: { processValue: { tag: 'TIC-204.PV', unit: '°C' }, setpoint: { tag: 'TIC-204.SP', unit: '°C' }, output: 'TIC-204.OP' } },
{ element: 'tileFlow', map: { value: { tag: 'FIC-101.PV', unit: 'm³/h', deadband: 0.2 } } },
{ element: 'tilePressure', map: { value: { tag: 'PIC-102.PV', unit: 'barg', deadband: 0.02 } } },
{ element: 'tileAbsolute', map: { value: { tag: 'PIC-102.PV', unit: 'barg', displayUnit: 'bar', deadband: 0.02 } } },
{ element: 'tileMass', map: { value: { tag: 'WIC-310.PV', unit: 'kg/h', deadband: 5 } } },
{ element: 'tileSpeed', map: { value: { tag: 'SI-410.PV', unit: 'rpm', deadband: 0.5 } } },
{ element: 'tileVibration', map: { value: { tag: 'VI-410.PV', unit: 'µm', displayUnit: 'µm', deadband: 0.1 } } }
];
/* The limits are in the stored unit too, and go with the scale. */
document.getElementById('loopFlow').alarmLimits = { hi: 100, hiHi: 110 };
document.getElementById('loopTemp').alarmLimits = { hi: 95, lo: 20 };
/* What each element showed to start with, so the decimals can follow the conversion. */
const sourceDecimals = {};
BINDINGS.forEach(function (entry) {
const element = document.getElementById(entry.element);
sourceDecimals[entry.element] = Number(element.precisionDigits) || 0;
/* The absolute tile is bound separately, because its display unit is re-chosen with
the system and a binding's unit is fixed for its life. */
if (!OVERRIDES[entry.element]) {
Connect.bind(element, entry.map, field);
}
});
const absoluteTile = document.getElementById('tileAbsolute');
/* ── Readouts the bindings cannot express: the cases ─────────────────────────── */
const latest = {};
['TIC-204.DT', 'PIC-102.PV', 'VI-410.PV'].forEach(function (tag) {
field.subscribe(tag, function (sample) {
latest[tag] = sample.value;
showCases();
});
});
function systemUnit(unit) {
return Units.system ? Units.preferred(unit, Units.system) : unit;
}
function showCases() {
const deltaUnit = systemUnit('ΔK'),
tempUnit = systemUnit('°C'),
pressureUnit = systemUnit('barg'),
absoluteUnit = pressureUnit === 'psig' ? 'psia' : (pressureUnit === 'kPag' ? 'kPa' : 'bar'),
lengthUnit = systemUnit('µm');
if (latest['TIC-204.DT'] !== undefined) {
const rise = latest['TIC-204.DT'];
document.getElementById('deltaT').textContent = Units.format(Units.convert(rise, 'ΔK', deltaUnit), deltaUnit, { decimals: 1 });
/* The mistake, shown: the same number pushed through the temperature scale. */
document.getElementById('deltaWrong').textContent = Units.format(Units.convert(rise, '°C', tempUnit), tempUnit, { decimals: 1 });
}
if (latest['PIC-102.PV'] !== undefined) {
const gauge = latest['PIC-102.PV'];
document.getElementById('gaugeAbs').textContent =
Units.format(Units.convert(gauge, 'barg', pressureUnit), pressureUnit, { decimals: Units.decimals(2, 'barg', pressureUnit) }) + ' → ' +
Units.format(Units.convert(gauge, 'barg', absoluteUnit), absoluteUnit, { decimals: Units.decimals(2, 'barg', absoluteUnit) });
}
if (latest['VI-410.PV'] !== undefined) {
const microns = latest['VI-410.PV'];
document.getElementById('resolution').textContent =
Units.format(microns, 'µm', { decimals: 1 }) + ' = ' +
Units.format(Units.convert(microns, 'µm', lengthUnit), lengthUnit, { decimals: Units.decimals(1, 'µm', lengthUnit) });
}
}
/* ── The system switch ───────────────────────────────────────────────────────── */
const BUTTONS = { '': 'toSource', si: 'toSi', us: 'toUs', site: 'toSite' },
LABELS = { '': 'Reading in the tags’ own units', si: 'Reading in SI', us: 'Reading in US customary units', site: 'Reading in the site’s units' };
function setSystem(system) {
Units.system = system;
Object.keys(BUTTONS).forEach(function (code) {
document.getElementById(BUTTONS[code]).setAttribute('aria-pressed', String(code === system));
});
document.getElementById('systemLabel').textContent = LABELS[system];
/* The bindings convert on the next sample; the decimals are the screen's to set,
from the resolution each reading had in its stored unit. */
BINDINGS.forEach(function (entry) {
const element = document.getElementById(entry.element),
first = entry.map[Object.keys(entry.map)[0]],
target = first.displayUnit || systemUnit(first.unit);
if (first.unit && 'precisionDigits' in element) {
element.precisionDigits = Units.decimals(sourceDecimals[entry.element], first.unit, target);
}
});
absoluteTile.precisionDigits = Units.decimals(2, 'barg', OVERRIDES.tileAbsolute[system]);
document.getElementById('tileVibration').precisionDigits = Units.decimals(1, 'µm', OVERRIDES.tileVibration[system]);
showCases();
showConversions();
}
Object.keys(BUTTONS).forEach(function (code) {
document.getElementById(BUTTONS[code]).addEventListener('click', function () {
rebindOverrides(code);
setSystem(code);
});
});
const unbinders = {};
function rebindOverrides(system) {
BINDINGS.forEach(function (entry) {
if (!OVERRIDES[entry.element]) {
return;
}
if (unbinders[entry.element]) {
unbinders[entry.element]();
}
entry.map.value.displayUnit = OVERRIDES[entry.element][system];
unbinders[entry.element] = Connect.bind(document.getElementById(entry.element), entry.map, field);
});
}
rebindOverrides('');
/* ── The table of what each binding resolved ────────────────────────────────── */
const values = {};
['FIC-101.PV', 'PIC-102.PV', 'TIC-204.PV', 'TIC-204.DT', 'WIC-310.PV', 'SI-410.PV', 'VI-410.PV'].forEach(function (tag) {
field.subscribe(tag, function (sample) {
values[tag] = sample.value;
});
});
const ROWS = [
['FIC-101.PV', 'm³/h', 1],
['PIC-102.PV', 'barg', 2],
['TIC-204.PV', '°C', 1],
['TIC-204.DT', 'ΔK', 1],
['WIC-310.PV', 'kg/h', 0],
['SI-410.PV', 'rpm', 0],
['VI-410.PV', 'µm', 1]
];
function showConversions() {
const body = document.getElementById('conversionRows');
body.innerHTML = '';
ROWS.forEach(function (row) {
const tag = row[0],
unit = row[1],
target = systemUnit(unit),
decimals = Units.decimals(row[2], unit, target),
tr = document.createElement('tr'),
value = values[tag];
[tag, unit, value === undefined ? '--' : Units.format(value, unit, { decimals: row[2] }),
value === undefined ? target : Units.format(Units.convert(value, unit, target), target, { decimals: decimals }),
String(decimals), Units.quantityOf(unit)].forEach(function (text) {
const td = document.createElement('td');
td.textContent = text;
tr.appendChild(td);
});
body.appendChild(tr);
});
}
setInterval(showConversions, 1000);
/* ── Chrome ─────────────────────────────────────────────────────────────────── */
const toggle = document.getElementById('themeToggle');
toggle.addEventListener('click', function () {
const dark = document.body.getAttribute('theme') === 'industrial-dark';
document.body.setAttribute('theme', dark ? 'industrial' : 'industrial-dark');
toggle.textContent = dark ? 'Dark' : 'Light';
});
setSystem('');
};
styles.css
/* Unit systems screen. Layout and chrome come from the connectivity stylesheet; only the
case readouts are its own. */
.un-readouts {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
gap: 10px;
}
.un-readout {
display: flex;
flex-direction: column;
gap: 4px;
padding: 10px 12px;
background: var(--smart-industrial-readout-background);
border: 1px solid var(--smart-industrial-panel-border);
}
.un-readout-label {
font-size: 11px;
letter-spacing: 0.04em;
text-transform: uppercase;
color: var(--smart-industrial-readout-color);
opacity: 0.8;
}
.un-readout-value {
font-family: Consolas, Monaco, monospace;
font-size: 18px;
font-variant-numeric: tabular-nums;
color: var(--smart-industrial-readout-color);
}
.un-readout-wrong {
/* struck through in the readout's own colour: red on the dark display reads badly */
opacity: 0.7;
text-decoration: line-through;
}
.cx-button[aria-pressed="true"] {
border-color: var(--smart-industrial-focus);
font-weight: 700;
}