Operator screen
DAQ Acquisition
A data acquisition task: device and instrument pickers, a four-channel record on a waveform graph with cursors, and per-channel statistics.
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>DAQ Acquisition - 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="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">DAQ ACQUISITION</span>
<span class="hmi-area">Rig 4 · Motor test cell · Finite acquisition, 4 channels</span>
</div>
<div class="hmi-header-actions">
<span class="hmi-conn" id="connection"><span class="hmi-conn-dot"></span><span id="connectionText">idle</span></span>
<button class="hmi-button hmi-button-primary" id="acquireButton" type="button">Acquire</button>
<button class="hmi-button" id="continuousButton" type="button">Continuous</button>
<button class="hmi-theme-toggle" id="themeToggle" type="button">Light</button>
</div>
</header>
<section class="hmi-body daq-body">
<aside class="hmi-panel daq-config">
<div class="hmi-panel-header"><span>Task</span></div>
<div class="hmi-panel-content daq-config-content">
<smart-resource-picker id="device" label="Device" value="Dev1/ai0:3" refreshable="false"></smart-resource-picker>
<smart-resource-picker id="instrument" label="Reference instrument" value="GPIB0::16::INSTR"></smart-resource-picker>
<div class="daq-field">
<label for="rate">Sample rate</label>
<select id="rate" class="daq-select">
<option value="10000">10 kS/s</option>
<option value="50000">50 kS/s</option>
<option value="100000" selected>100 kS/s</option>
<option value="250000">250 kS/s</option>
</select>
</div>
<div class="daq-field">
<label for="samples">Samples per channel</label>
<select id="samples" class="daq-select">
<option value="5000">5,000</option>
<option value="20000" selected>20,000</option>
<option value="100000">100,000</option>
</select>
</div>
<div class="daq-field">
<label for="trigger">Trigger</label>
<select id="trigger" class="daq-select">
<option value="immediate" selected>Immediate</option>
<option value="edge">Rising edge, drive > 12 V</option>
</select>
</div>
<div class="daq-field daq-field-row">
<label><input type="checkbox" id="stacked" /> Stack the scales</label>
</div>
<div class="daq-channels">
<div class="daq-channels-title">Channels</div>
<table class="hmi-table daq-channel-table" id="channelTable">
<thead><tr><th>Channel</th><th>Name</th><th class="num">Min</th><th class="num">Max</th><th class="num">Mean</th><th class="num">RMS</th></tr></thead>
<tbody></tbody>
</table>
</div>
</div>
</aside>
<div class="hmi-panel daq-graph">
<div class="hmi-panel-header">
<span>Acquired record</span>
<span class="daq-record" id="recordInfo">No record yet</span>
</div>
<div class="hmi-panel-content daq-graph-content">
<smart-waveform-graph id="graph" label="" x-label="Time" x-unit="s"></smart-waveform-graph>
</div>
</div>
</section>
</div>
<!-- scripts -->
<script type="module" src="node_modules/smart-industrial/source/modules/smart.resourcepicker.js"></script>
<script type="module" src="node_modules/smart-industrial/source/modules/smart.waveformgraph.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" />
/* DAQ acquisition screen.
*
* The device is a stand-in: acquire() returns what a driver's read would, four channels of
* samples at the configured rate. The picker, the graph and the statistics table are the
* screen; replace acquire() with the driver call and nothing else changes. */
function gaussian() {
return Math.sqrt(-2 * Math.log(Math.random() || 1e-12)) * Math.cos(2 * Math.PI * Math.random());
}
const CHANNELS = [
{ id: 'ai0', name: 'Drive voltage', unit: 'V', scale: 'v' },
{ id: 'ai1', name: 'Motor current', unit: 'A', scale: 'a' },
{ id: 'ai2', name: 'Bearing DE', unit: 'g', scale: 'g' },
{ id: 'ai3', name: 'Bearing NDE', unit: 'g', scale: 'g' }
];
window.onload = function () {
const device = document.getElementById('device'),
instrument = document.getElementById('instrument'),
rate = document.getElementById('rate'),
samples = document.getElementById('samples'),
trigger = document.getElementById('trigger'),
stacked = document.getElementById('stacked'),
graph = document.getElementById('graph'),
recordInfo = document.getElementById('recordInfo'),
channelTable = document.getElementById('channelTable').querySelector('tbody'),
connection = document.getElementById('connection'),
connectionText = document.getElementById('connectionText'),
acquireButton = document.getElementById('acquireButton'),
continuousButton = document.getElementById('continuousButton'),
themeToggle = document.getElementById('themeToggle');
let continuous = null, captureNumber = 1180;
/* ── Theme ───────────────────────────────────────────────────────── */
function applyTheme(theme) {
document.body.setAttribute('theme', theme);
const elements = document.querySelectorAll('smart-resource-picker, smart-waveform-graph');
for (let i = 0; i < elements.length; i++) {
elements[i].theme = theme;
}
themeToggle.textContent = theme === 'industrial-dark' ? 'Light' : 'Dark';
}
themeToggle.addEventListener('click', function () {
applyTheme(document.body.getAttribute('theme') === 'industrial-dark' ? 'industrial' : 'industrial-dark');
});
/* ── Resources ───────────────────────────────────────────────────── */
device.resources = [
{ name: 'Dev1/ai0:3', alias: 'Rig 4 inputs', description: 'PCIe-6363, 4 differential inputs, ±10 V', available: true },
{ name: 'Dev1/ai0', alias: 'Drive only', description: 'One channel', available: true },
{ name: 'Dev2/ai0:3', alias: 'Vibration module', description: 'Four IEPE inputs, 51.2 kS/s', available: true },
{ name: 'Dev2/ai0:7', alias: 'Rig 5 inputs', description: 'Not connected', available: false }
];
device.refreshable = false;
instrument.resources = [
{ name: 'GPIB0::16::INSTR', alias: 'DMM', description: 'Keysight 34465A, reference for the drive voltage', available: true },
{ name: 'TCPIP0::192.168.10.21::inst0::INSTR', alias: 'Scope', description: 'Tektronix MSO46', available: true },
{ name: 'USB0::0x0957::0x1755::MY12345678::INSTR', alias: 'SMU', description: 'Keysight B2902A', available: true }
];
instrument.addEventListener('refreshRequest', function () {
instrument.busy = true;
setTimeout(function () {
instrument.busy = false;
}, 900);
});
/* ── The stand-in device ──────────────────────────────────────────── */
function acquire() {
const fs = Number(rate.value), n = Number(samples.value), dt = 1 / fs,
edge = trigger.value === 'edge',
/* With an edge trigger the record starts 10 ms before the drive step; without
it, the step lands wherever it lands. */
stepAt = edge ? 0.01 : 0.01 + Math.random() * (n * dt * 0.5),
knockAt = stepAt + 0.03 + Math.random() * 0.05,
records = CHANNELS.map(() => new Float64Array(n));
for (let i = 0; i < n; i++) {
const t = i * dt,
on = t >= stepAt ? 1 : 0,
rise = on ? 1 - Math.exp(-(t - stepAt) / 0.012) : 0;
records[0][i] = 24 * on + 0.4 * Math.sin(2 * Math.PI * 20000 * t) * on + 0.05 * gaussian();
records[1][i] = 8.5 * rise + 0.35 * Math.sin(2 * Math.PI * 20000 * t + 1) * rise + 0.03 * gaussian();
records[2][i] = 0.8 * Math.sin(2 * Math.PI * 47.5 * t) * rise + 0.12 * Math.sin(2 * Math.PI * 95 * t) * rise + 0.05 * gaussian();
records[3][i] = 0.3 * Math.sin(2 * Math.PI * 47.5 * t + 0.7) * rise + 0.04 * gaussian() +
(t > knockAt && t < knockAt + 0.004 ? 6 * Math.exp(-(t - knockAt) / 0.001) * Math.sin(2 * Math.PI * 3200 * (t - knockAt)) : 0);
}
return { records: records, dt: dt, t0: edge ? -0.01 : 0, stepAt: stepAt, knockAt: knockAt };
}
function statistics(record) {
let min = Infinity, max = -Infinity, sum = 0, squares = 0;
for (let i = 0; i < record.length; i++) {
const v = record[i];
min = Math.min(min, v);
max = Math.max(max, v);
sum += v;
squares += v * v;
}
return { min: min, max: max, mean: sum / record.length, rms: Math.sqrt(squares / record.length) };
}
graph.scales = [
{ id: 'v', label: 'Drive', unit: 'V' },
{ id: 'a', label: 'Current', unit: 'A', position: 'right' },
{ id: 'g', label: 'Vibration', unit: 'g', position: 'right' }
];
graph.cursors = [{ id: 'c1', label: 'C1', x: 0.02, plot: 'ai1' }, { id: 'c2', label: 'C2', x: 0.04, plot: 'ai1' }];
function run() {
const result = acquire(),
count = device.value.indexOf(':') !== -1 ? CHANNELS.length : 1;
captureNumber++;
graph.label = 'Capture ' + captureNumber;
graph.plots = CHANNELS.slice(0, count).map(function (channel, i) {
return { id: channel.id, label: channel.name, unit: channel.unit, y: result.records[i], t0: result.t0, dt: result.dt, scale: channel.scale };
});
graph.annotations = [
{ plot: 'ai0', x: result.t0 + result.stepAt, text: 'drive on' }
].concat(count > 3 ? [{ plot: 'ai3', x: result.t0 + result.knockAt + 0.0003, text: 'knock' }] : []);
recordInfo.textContent = count + ' × ' + Number(samples.value).toLocaleString('en-US') + ' samples at ' + (Number(rate.value) / 1000) + ' kS/s, ' +
(Number(samples.value) / Number(rate.value) * 1000).toFixed(0) + ' ms, ' + new Date().toLocaleTimeString();
channelTable.innerHTML = CHANNELS.slice(0, count).map(function (channel, i) {
const s = statistics(result.records[i]),
digits = channel.unit === 'g' ? 3 : 2;
return '<tr><td class="mono">' + device.value.split('/')[0] + '/' + channel.id + '</td><td>' + channel.name + '</td>' +
'<td class="num">' + s.min.toFixed(digits) + '</td><td class="num">' + s.max.toFixed(digits) + '</td>' +
'<td class="num">' + s.mean.toFixed(digits) + '</td><td class="num">' + s.rms.toFixed(digits) + '</td></tr>';
}).join('');
}
acquireButton.addEventListener('click', function () {
connection.setAttribute('data-state', 'running');
connectionText.textContent = 'acquiring';
acquireButton.disabled = true;
setTimeout(function () {
run();
connection.setAttribute('data-state', 'ok');
connectionText.textContent = 'done';
acquireButton.disabled = false;
}, 250);
});
continuousButton.addEventListener('click', function () {
if (continuous) {
clearInterval(continuous);
continuous = null;
continuousButton.textContent = 'Continuous';
connectionText.textContent = 'stopped';
connection.setAttribute('data-state', 'ok');
return;
}
continuous = setInterval(run, 1500);
continuousButton.textContent = 'Stop';
connectionText.textContent = 'running';
connection.setAttribute('data-state', 'running');
});
stacked.addEventListener('change', function () {
graph.layout = stacked.checked ? 'stacked' : 'overlaid';
});
graph.addEventListener('exportRequest', function (event) {
const blob = new Blob([event.detail.data], { type: 'text/csv' }),
link = document.createElement('a');
link.href = URL.createObjectURL(blob);
link.download = 'capture-' + captureNumber + '.csv';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
});
[rate, samples, trigger, device].forEach(function (control) {
control.addEventListener('change', function () {
if (!continuous) {
run();
}
});
});
applyTheme('industrial-dark');
run();
connection.setAttribute('data-state', 'ok');
connectionText.textContent = 'done';
};
styles.css
/* DAQ acquisition screen. The task on the left, the record on the right. */
html,
body {
height: 100%;
margin: 0;
}
body.viewport {
background: var(--smart-industrial-canvas, var(--smart-body-background-color));
color: var(--smart-text-color);
font-family: var(--smart-font-family);
font-size: 13px;
overflow: hidden;
}
.hmi-screen {
display: grid;
grid-template-rows: auto 1fr;
gap: 10px;
padding: 10px;
height: 100%;
box-sizing: border-box;
}
.hmi-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 28px;
padding: 10px 14px;
background: var(--smart-industrial-panel-background);
border: 1px solid var(--smart-industrial-panel-border);
border-radius: 4px;
box-shadow: var(--smart-industrial-panel-shadow);
}
.hmi-identity {
display: flex;
flex-direction: column;
gap: 2px;
}
.hmi-plant {
font-size: 16px;
font-weight: 600;
letter-spacing: .08em;
}
.hmi-area {
color: var(--smart-text-tertiary-color);
font-size: 11px;
letter-spacing: .04em;
}
.hmi-header-actions {
display: flex;
align-items: center;
gap: 10px;
}
.hmi-conn[data-state="running"] .hmi-conn-dot {
background: var(--smart-industrial-alarm-warning);
}
.daq-body {
display: grid;
grid-template-columns: minmax(300px, 380px) minmax(0, 1fr);
gap: 10px;
min-height: 0;
}
.daq-config-content {
display: flex;
flex-direction: column;
gap: 12px;
overflow: auto;
}
.daq-config smart-resource-picker {
width: 100%;
}
.daq-field {
display: flex;
flex-direction: column;
gap: 4px;
}
.daq-field > label {
font-size: 11px;
font-weight: 600;
letter-spacing: .04em;
text-transform: uppercase;
color: var(--smart-text-secondary-color);
}
.daq-field-row > label {
text-transform: none;
letter-spacing: 0;
font-weight: 400;
font-size: 13px;
color: var(--smart-text-color);
}
.daq-select {
font: inherit;
font-size: 13px;
padding: 6px 8px;
border: 1px solid var(--smart-border);
border-radius: 3px;
background: var(--smart-background);
color: var(--smart-text-color);
}
.daq-channels-title {
margin: 6px 0 6px;
font-size: 11px;
font-weight: 600;
letter-spacing: .04em;
text-transform: uppercase;
color: var(--smart-text-secondary-color);
}
.daq-channel-table {
width: 100%;
font-size: 12px;
}
.daq-channel-table th.num,
.daq-channel-table td.num {
text-align: right;
font-variant-numeric: tabular-nums;
}
.daq-channel-table .mono {
font-family: Consolas, "Courier New", monospace;
font-size: 11px;
}
.daq-graph-content {
display: flex;
min-height: 0;
}
.daq-graph-content smart-waveform-graph {
--smart-waveform-height: auto;
flex: 1 1 auto;
height: 100%;
min-height: 320px;
}
.daq-record {
text-transform: none;
letter-spacing: 0;
font-weight: 400;
color: var(--smart-text-secondary-color);
font-variant-numeric: tabular-nums;
}
@media (max-width: 900px) {
body.viewport {
overflow: auto;
}
.hmi-screen {
height: auto;
}
.daq-body {
grid-template-columns: 1fr;
}
}