Component demo · Test and measurement
Digital Waveform
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>Digital Waveform 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.DigitalWaveform displays digital lines against time, one row per line, with lines grouped into buses whose words are written in hex, binary or decimal.</h2>
<p>
It is the view of a logic analyser and of the classic digital waveform graph. A line is given as samples on a fixed interval or as a list of edges, so a sparse capture of a long idle period costs what it has to say. The view pans and zooms, cursors read every line and every bus at two instants and the time between them, and where more transitions land on a pixel column than it can show, the column is drawn as a block, which is what a real analyser does.
</p>
</section>
<section>
<h2>An SPI transaction, 8 bit words at 10 MHz</h2>
<p>
Chip select falls, the clock runs for three bytes, and the data lines carry a register write: command 0x0B, address 0x2A, value 0xC7. The bus rows read the byte on each edge group. Drag a cursor onto a clock edge or press the arrow keys to step it, use the wheel to zoom about the pointer, hold Shift and drag to zoom into a box, and double-click to fit.
</p>
<smart-digital-waveform id="spi" label="SPI capture, 100 MS/s" x-label="Time"></smart-digital-waveform>
<div class="demo-actions">
<label>Bus format <select id="format">
<option value="hex">hex</option>
<option value="bin">binary</option>
<option value="dec">decimal</option>
</select></label>
<label><input type="checkbox" id="lines" checked /> Show the lines of a bus</label>
<button type="button" id="zoomByte">Zoom to the second byte</button>
<span id="spiLog" class="demo-log">cursorChange, zoom and rowClick are listed here.</span>
</div>
</section>
<section>
<h2>A PLC I/O trace over a shift, as edges</h2>
<p>
Eight inputs and outputs of a packaging machine over eight hours, recorded as edges: an infeed sensor that pulses per product, a jam input, the run output and the guard interlock. A line with a few hundred edges over eight hours is drawn from its edges, and the cursors read the state of every line at two instants.
</p>
<smart-digital-waveform id="plc" label="Line 2 PLC, I/O trace" x-label="Time" x-unit="s" row-height="24" label-width="150"></smart-digital-waveform>
<p class="hint">Keyboard: the plot is a tab stop. The arrow keys move the active cursor, with Shift by ten steps; N makes the next cursor active; + and - zoom, 0 fits, Home and End move the cursor to the ends of the view. The cursor legend is text, so the readings are available to a screen reader.</p>
</section>
</div>
<script type="module" src="node_modules/smart-industrial/source/modules/smart.digitalwaveform.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" />
/* Both captures are generated with known content, so what the bus rows read can be checked
against what was put in. The seam is the signals property: an analyser's driver assigns
the same shape and nothing else on the page changes. */
window.onload = function () {
/* ── SPI ───────────────────────────────────────────────────────────── */
const spi = document.getElementById('spi'),
spiLog = document.getElementById('spiLog'),
format = document.getElementById('format'),
lines = document.getElementById('lines'),
zoomByte = document.getElementById('zoomByte');
const RATE = 100e6, DT = 1 / RATE, CLOCK = 10e6, SAMPLES = 400,
BYTES = [0x0B, 0x2A, 0xC7], REPLY = [0x00, 0x00, 0x5A];
/* Mode 0: data is set on the falling edge and sampled on the rising edge. Chip select
falls 40 samples in; each bit takes 10 samples. */
const sclk = new Uint8Array(SAMPLES), mosi = new Uint8Array(SAMPLES), miso = new Uint8Array(SAMPLES), cs = new Uint8Array(SAMPLES),
csLow = 30, bitSamples = RATE / CLOCK, csHigh = csLow + 20 + BYTES.length * 8 * bitSamples;
for (let i = 0; i < SAMPLES; i++) {
cs[i] = i >= csLow && i < csHigh ? 0 : 1;
const bitIndex = Math.floor((i - csLow - 10) / bitSamples),
inWord = i >= csLow + 10 && bitIndex < BYTES.length * 8,
phase = ((i - csLow - 10) % bitSamples) / bitSamples;
sclk[i] = inWord && phase >= 0.5 ? 1 : 0;
const byte = Math.floor(bitIndex / 8),
bit = 7 - (bitIndex % 8);
mosi[i] = inWord ? (BYTES[byte] >> bit) & 1 : 0;
miso[i] = inWord ? (REPLY[byte] >> bit) & 1 : 0;
}
/* The data lines of the bus, decoded per sample from MOSI at each clock: the bus row
reads the byte as it is assembled. */
const d = [];
for (let b = 0; b < 8; b++) {
d.push(new Uint8Array(SAMPLES));
}
let shift = 0;
for (let i = 0; i < SAMPLES; i++) {
if (i > 0 && sclk[i] === 1 && sclk[i - 1] === 0) {
shift = ((shift << 1) | mosi[i]) & 0xFF;
}
for (let b = 0; b < 8; b++) {
d[b][i] = (shift >> b) & 1;
}
}
spi.signals = [
{ id: 'cs', label: 'CS', samples: cs, t0: 0, dt: DT },
{ id: 'sclk', label: 'SCLK', samples: sclk, t0: 0, dt: DT },
{ id: 'mosi', label: 'MOSI', samples: mosi, t0: 0, dt: DT },
{ id: 'miso', label: 'MISO', samples: miso, t0: 0, dt: DT },
{ id: 'd7', label: 'D7', samples: d[7], t0: 0, dt: DT },
{ id: 'd6', label: 'D6', samples: d[6], t0: 0, dt: DT },
{ id: 'd5', label: 'D5', samples: d[5], t0: 0, dt: DT },
{ id: 'd4', label: 'D4', samples: d[4], t0: 0, dt: DT },
{ id: 'd3', label: 'D3', samples: d[3], t0: 0, dt: DT },
{ id: 'd2', label: 'D2', samples: d[2], t0: 0, dt: DT },
{ id: 'd1', label: 'D1', samples: d[1], t0: 0, dt: DT },
{ id: 'd0', label: 'D0', samples: d[0], t0: 0, dt: DT }
];
spi.buses = [{ id: 'shift', label: 'Shift register[7:0]', lines: ['d7', 'd6', 'd5', 'd4', 'd3', 'd2', 'd1', 'd0'], format: 'hex' }];
spi.cursors = [
{ id: 'c1', label: 'C1', x: (csLow + 10 + 8 * bitSamples) * DT },
{ id: 'c2', label: 'C2', x: (csLow + 10 + 16 * bitSamples) * DT }
];
format.addEventListener('change', function () {
spi.buses = [Object.assign({}, spi.buses[0], { format: format.value })];
});
lines.addEventListener('change', function () {
spi.showLines = lines.checked;
});
zoomByte.addEventListener('click', function () {
spi.zoomTo((csLow + 8 + 8 * bitSamples) * DT, (csLow + 12 + 16 * bitSamples) * DT);
});
spi.addEventListener('cursorChange', function (event) {
spiLog.textContent = 'cursorChange: ' + event.detail.id + ' at ' + (event.detail.x * 1e9).toFixed(0) + ' ns, shift register ' +
spi.busValueAt('shift', event.detail.x).toString(16).toUpperCase().padStart(2, '0');
});
spi.addEventListener('zoom', function (event) {
spiLog.textContent = event.detail.fitted ? 'zoom: fitted' : 'zoom: ' + (event.detail.xMin * 1e9).toFixed(0) + ' to ' + (event.detail.xMax * 1e9).toFixed(0) + ' ns';
});
spi.addEventListener('rowClick', function (event) {
spiLog.textContent = 'rowClick: ' + event.detail.kind + ' ' + event.detail.id + ' at ' + (event.detail.time * 1e9).toFixed(0) + ' ns';
});
/* ── The PLC trace ─────────────────────────────────────────────────── */
const plc = document.getElementById('plc'),
SHIFT = 8 * 3600;
function edgesOf(fn) {
/* A line as edges: the level at a set of times, kept where it changes. */
const edges = [[0, fn(0)]];
for (let t = 1; t < SHIFT; t += 1) {
const level = fn(t);
if (level !== edges[edges.length - 1][1]) {
edges.push([t, level]);
}
}
return edges;
}
const breaks = [[7200, 7500], [16200, 16800], [23400, 23700]],
jamAt = 16200,
guardOpen = [[16250, 16700]];
const running = (t) => breaks.some((b) => t >= b[0] && t < b[1]) ? 0 : 1,
infeed = (t) => running(t) && Math.floor(t / 3) % 2 === 0 ? 1 : 0,
jam = (t) => t >= jamAt && t < jamAt + 40 ? 1 : 0,
guard = (t) => guardOpen.some((g) => t >= g[0] && t < g[1]) ? 0 : 1,
reject = (t) => running(t) && Math.floor(t / 3) % 37 === 5 ? 1 : 0,
lowStock = (t) => (t > 12000 && t < 12900) || (t > 26000 && t < 26300) ? 1 : 0,
hornOut = (t) => jam(t) || (t >= jamAt + 40 && t < jamAt + 60) ? 1 : 0,
lampOut = (t) => running(t) ? 1 : (Math.floor(t) % 2 === 0 ? 1 : 0);
plc.signals = [
{ id: 'i0', label: 'I0.0 Infeed sensor', edges: edgesOf(infeed), end: SHIFT },
{ id: 'i1', label: 'I0.1 Jam detected', edges: edgesOf(jam), end: SHIFT },
{ id: 'i2', label: 'I0.2 Guard closed', edges: edgesOf(guard), end: SHIFT },
{ id: 'i3', label: 'I0.3 Low stock', edges: edgesOf(lowStock), end: SHIFT },
{ id: 'q0', label: 'Q0.0 Run', edges: edgesOf(running), end: SHIFT },
{ id: 'q1', label: 'Q0.1 Reject pusher', edges: edgesOf(reject), end: SHIFT },
{ id: 'q2', label: 'Q0.2 Horn', edges: edgesOf(hornOut), end: SHIFT },
{ id: 'q3', label: 'Q0.3 Stack lamp', edges: edgesOf(lampOut), end: SHIFT }
];
plc.buses = [{ id: 'q', label: 'Q0[3:0]', lines: ['q3', 'q2', 'q1', 'q0'], format: 'bin' }];
plc.showLines = true;
plc.cursors = [{ id: 'a', label: 'jam', x: jamAt + 5 }, { id: 'b', label: 'restart', x: 16805 }];
};
styles.css
smart-digital-waveform {
display: block;
}
/* ── 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.DigitalWaveform displays digital lines against time, one row per line, with lines grouped into buses whose words are written in hex, binary or decimal.</h2>
<p>
It is the view of a logic analyser and of the classic digital waveform graph. A line is given as samples on a fixed interval or as a list of edges, so a sparse capture of a long idle period costs what it has to say. The view pans and zooms, cursors read every line and every bus at two instants and the time between them, and where more transitions land on a pixel column than it can show, the column is drawn as a block, which is what a real analyser does.
</p>
</section>
<section>
<h2>An SPI transaction, 8 bit words at 10 MHz</h2>
<p>
Chip select falls, the clock runs for three bytes, and the data lines carry a register write: command 0x0B, address 0x2A, value 0xC7. The bus rows read the byte on each edge group. Drag a cursor onto a clock edge or press the arrow keys to step it, use the wheel to zoom about the pointer, hold Shift and drag to zoom into a box, and double-click to fit.
</p>
<smart-digital-waveform id="spi" label="SPI capture, 100 MS/s" x-label="Time"></smart-digital-waveform>
<div class="demo-actions">
<label>Bus format <select id="format">
<option value="hex">hex</option>
<option value="bin">binary</option>
<option value="dec">decimal</option>
</select></label>
<label><input type="checkbox" id="lines" checked /> Show the lines of a bus</label>
<button type="button" id="zoomByte">Zoom to the second byte</button>
<span id="spiLog" class="demo-log">cursorChange, zoom and rowClick are listed here.</span>
</div>
</section>
<section>
<h2>A PLC I/O trace over a shift, as edges</h2>
<p>
Eight inputs and outputs of a packaging machine over eight hours, recorded as edges: an infeed sensor that pulses per product, a jam input, the run output and the guard interlock. A line with a few hundred edges over eight hours is drawn from its edges, and the cursors read the state of every line at two instants.
</p>
<smart-digital-waveform id="plc" label="Line 2 PLC, I/O trace" x-label="Time" x-unit="s" [rowHeight]="24" [labelWidth]="150"></smart-digital-waveform>
<p class="hint">Keyboard: the plot is a tab stop. The arrow keys move the active cursor, with Shift by ten steps; N makes the next cursor active; + and - zoom, 0 fits, Home and End move the cursor to the ends of the view. The cursor legend is text, so the readings are available to a screen reader.</p>
</section>
</div>
</div>
angular/app.component.ts
import { Component, AfterViewInit, OnDestroy } from '@angular/core';
import { DigitalWaveformModule } from 'smart-industrial/angular/digitalwaveform';
import { runDemo } from './demo';
/**
* Smart.UI Industrial - DigitalWaveform 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: [DigitalWaveformModule],
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. */
/* Both captures are generated with known content, so what the bus rows read can be checked
against what was put in. The seam is the signals property: an analyser's driver assigns
the same shape and nothing else on the page changes. */
export function runDemo(): void {
/* ── SPI ───────────────────────────────────────────────────────────── */
const spi = document.getElementById('spi'),
spiLog = document.getElementById('spiLog'),
format = document.getElementById('format'),
lines = document.getElementById('lines'),
zoomByte = document.getElementById('zoomByte');
const RATE = 100e6, DT = 1 / RATE, CLOCK = 10e6, SAMPLES = 400,
BYTES = [0x0B, 0x2A, 0xC7], REPLY = [0x00, 0x00, 0x5A];
/* Mode 0: data is set on the falling edge and sampled on the rising edge. Chip select
falls 40 samples in; each bit takes 10 samples. */
const sclk = new Uint8Array(SAMPLES), mosi = new Uint8Array(SAMPLES), miso = new Uint8Array(SAMPLES), cs = new Uint8Array(SAMPLES),
csLow = 30, bitSamples = RATE / CLOCK, csHigh = csLow + 20 + BYTES.length * 8 * bitSamples;
for (let i = 0; i < SAMPLES; i++) {
cs[i] = i >= csLow && i < csHigh ? 0 : 1;
const bitIndex = Math.floor((i - csLow - 10) / bitSamples),
inWord = i >= csLow + 10 && bitIndex < BYTES.length * 8,
phase = ((i - csLow - 10) % bitSamples) / bitSamples;
sclk[i] = inWord && phase >= 0.5 ? 1 : 0;
const byte = Math.floor(bitIndex / 8),
bit = 7 - (bitIndex % 8);
mosi[i] = inWord ? (BYTES[byte] >> bit) & 1 : 0;
miso[i] = inWord ? (REPLY[byte] >> bit) & 1 : 0;
}
/* The data lines of the bus, decoded per sample from MOSI at each clock: the bus row
reads the byte as it is assembled. */
const d = [];
for (let b = 0; b < 8; b++) {
d.push(new Uint8Array(SAMPLES));
}
let shift = 0;
for (let i = 0; i < SAMPLES; i++) {
if (i > 0 && sclk[i] === 1 && sclk[i - 1] === 0) {
shift = ((shift << 1) | mosi[i]) & 0xFF;
}
for (let b = 0; b < 8; b++) {
d[b][i] = (shift >> b) & 1;
}
}
spi.signals = [
{ id: 'cs', label: 'CS', samples: cs, t0: 0, dt: DT },
{ id: 'sclk', label: 'SCLK', samples: sclk, t0: 0, dt: DT },
{ id: 'mosi', label: 'MOSI', samples: mosi, t0: 0, dt: DT },
{ id: 'miso', label: 'MISO', samples: miso, t0: 0, dt: DT },
{ id: 'd7', label: 'D7', samples: d[7], t0: 0, dt: DT },
{ id: 'd6', label: 'D6', samples: d[6], t0: 0, dt: DT },
{ id: 'd5', label: 'D5', samples: d[5], t0: 0, dt: DT },
{ id: 'd4', label: 'D4', samples: d[4], t0: 0, dt: DT },
{ id: 'd3', label: 'D3', samples: d[3], t0: 0, dt: DT },
{ id: 'd2', label: 'D2', samples: d[2], t0: 0, dt: DT },
{ id: 'd1', label: 'D1', samples: d[1], t0: 0, dt: DT },
{ id: 'd0', label: 'D0', samples: d[0], t0: 0, dt: DT }
];
spi.buses = [{ id: 'shift', label: 'Shift register[7:0]', lines: ['d7', 'd6', 'd5', 'd4', 'd3', 'd2', 'd1', 'd0'], format: 'hex' }];
spi.cursors = [
{ id: 'c1', label: 'C1', x: (csLow + 10 + 8 * bitSamples) * DT },
{ id: 'c2', label: 'C2', x: (csLow + 10 + 16 * bitSamples) * DT }
];
format.addEventListener('change', function () {
spi.buses = [Object.assign({}, spi.buses[0], { format: format.value })];
});
lines.addEventListener('change', function () {
spi.showLines = lines.checked;
});
zoomByte.addEventListener('click', function () {
spi.zoomTo((csLow + 8 + 8 * bitSamples) * DT, (csLow + 12 + 16 * bitSamples) * DT);
});
spi.addEventListener('cursorChange', function (event) {
spiLog.textContent = 'cursorChange: ' + event.detail.id + ' at ' + (event.detail.x * 1e9).toFixed(0) + ' ns, shift register ' +
spi.busValueAt('shift', event.detail.x).toString(16).toUpperCase().padStart(2, '0');
});
spi.addEventListener('zoom', function (event) {
spiLog.textContent = event.detail.fitted ? 'zoom: fitted' : 'zoom: ' + (event.detail.xMin * 1e9).toFixed(0) + ' to ' + (event.detail.xMax * 1e9).toFixed(0) + ' ns';
});
spi.addEventListener('rowClick', function (event) {
spiLog.textContent = 'rowClick: ' + event.detail.kind + ' ' + event.detail.id + ' at ' + (event.detail.time * 1e9).toFixed(0) + ' ns';
});
/* ── The PLC trace ─────────────────────────────────────────────────── */
const plc = document.getElementById('plc'),
SHIFT = 8 * 3600;
function edgesOf(fn) {
/* A line as edges: the level at a set of times, kept where it changes. */
const edges = [[0, fn(0)]];
for (let t = 1; t < SHIFT; t += 1) {
const level = fn(t);
if (level !== edges[edges.length - 1][1]) {
edges.push([t, level]);
}
}
return edges;
}
const breaks = [[7200, 7500], [16200, 16800], [23400, 23700]],
jamAt = 16200,
guardOpen = [[16250, 16700]];
const running = (t) => breaks.some((b) => t >= b[0] && t < b[1]) ? 0 : 1,
infeed = (t) => running(t) && Math.floor(t / 3) % 2 === 0 ? 1 : 0,
jam = (t) => t >= jamAt && t < jamAt + 40 ? 1 : 0,
guard = (t) => guardOpen.some((g) => t >= g[0] && t < g[1]) ? 0 : 1,
reject = (t) => running(t) && Math.floor(t / 3) % 37 === 5 ? 1 : 0,
lowStock = (t) => (t > 12000 && t < 12900) || (t > 26000 && t < 26300) ? 1 : 0,
hornOut = (t) => jam(t) || (t >= jamAt + 40 && t < jamAt + 60) ? 1 : 0,
lampOut = (t) => running(t) ? 1 : (Math.floor(t) % 2 === 0 ? 1 : 0);
plc.signals = [
{ id: 'i0', label: 'I0.0 Infeed sensor', edges: edgesOf(infeed), end: SHIFT },
{ id: 'i1', label: 'I0.1 Jam detected', edges: edgesOf(jam), end: SHIFT },
{ id: 'i2', label: 'I0.2 Guard closed', edges: edgesOf(guard), end: SHIFT },
{ id: 'i3', label: 'I0.3 Low stock', edges: edgesOf(lowStock), end: SHIFT },
{ id: 'q0', label: 'Q0.0 Run', edges: edgesOf(running), end: SHIFT },
{ id: 'q1', label: 'Q0.1 Reject pusher', edges: edgesOf(reject), end: SHIFT },
{ id: 'q2', label: 'Q0.2 Horn', edges: edgesOf(hornOut), end: SHIFT },
{ id: 'q3', label: 'Q0.3 Stack lamp', edges: edgesOf(lampOut), end: SHIFT }
];
plc.buses = [{ id: 'q', label: 'Q0[3:0]', lines: ['q3', 'q2', 'q1', 'q0'], format: 'bin' }];
plc.showLines = true;
plc.cursors = [{ id: 'a', label: 'jam', x: jamAt + 5 }, { id: 'b', label: 'restart', x: 16805 }];
}
angular/app.component.css
smart-digital-waveform {
display: block;
}
/* ── 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 { DigitalWaveform } from 'smart-industrial/react/digitalwaveform';
/* The demo logic is the HTML demo's: it feeds the elements the way a plant's data would. */
/* Both captures are generated with known content, so what the bus rows read can be checked
against what was put in. The seam is the signals property: an analyser's driver assigns
the same shape and nothing else on the page changes. */
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;
}
/* ── SPI ───────────────────────────────────────────────────────────── */
const spi = document.getElementById('spi'),
spiLog = document.getElementById('spiLog'),
format = document.getElementById('format'),
lines = document.getElementById('lines'),
zoomByte = document.getElementById('zoomByte');
const RATE = 100e6, DT = 1 / RATE, CLOCK = 10e6, SAMPLES = 400,
BYTES = [0x0B, 0x2A, 0xC7], REPLY = [0x00, 0x00, 0x5A];
/* Mode 0: data is set on the falling edge and sampled on the rising edge. Chip select
falls 40 samples in; each bit takes 10 samples. */
const sclk = new Uint8Array(SAMPLES), mosi = new Uint8Array(SAMPLES), miso = new Uint8Array(SAMPLES), cs = new Uint8Array(SAMPLES),
csLow = 30, bitSamples = RATE / CLOCK, csHigh = csLow + 20 + BYTES.length * 8 * bitSamples;
for (let i = 0; i < SAMPLES; i++) {
cs[i] = i >= csLow && i < csHigh ? 0 : 1;
const bitIndex = Math.floor((i - csLow - 10) / bitSamples),
inWord = i >= csLow + 10 && bitIndex < BYTES.length * 8,
phase = ((i - csLow - 10) % bitSamples) / bitSamples;
sclk[i] = inWord && phase >= 0.5 ? 1 : 0;
const byte = Math.floor(bitIndex / 8),
bit = 7 - (bitIndex % 8);
mosi[i] = inWord ? (BYTES[byte] >> bit) & 1 : 0;
miso[i] = inWord ? (REPLY[byte] >> bit) & 1 : 0;
}
/* The data lines of the bus, decoded per sample from MOSI at each clock: the bus row
reads the byte as it is assembled. */
const d = [];
for (let b = 0; b < 8; b++) {
d.push(new Uint8Array(SAMPLES));
}
let shift = 0;
for (let i = 0; i < SAMPLES; i++) {
if (i > 0 && sclk[i] === 1 && sclk[i - 1] === 0) {
shift = ((shift << 1) | mosi[i]) & 0xFF;
}
for (let b = 0; b < 8; b++) {
d[b][i] = (shift >> b) & 1;
}
}
spi.signals = [
{ id: 'cs', label: 'CS', samples: cs, t0: 0, dt: DT },
{ id: 'sclk', label: 'SCLK', samples: sclk, t0: 0, dt: DT },
{ id: 'mosi', label: 'MOSI', samples: mosi, t0: 0, dt: DT },
{ id: 'miso', label: 'MISO', samples: miso, t0: 0, dt: DT },
{ id: 'd7', label: 'D7', samples: d[7], t0: 0, dt: DT },
{ id: 'd6', label: 'D6', samples: d[6], t0: 0, dt: DT },
{ id: 'd5', label: 'D5', samples: d[5], t0: 0, dt: DT },
{ id: 'd4', label: 'D4', samples: d[4], t0: 0, dt: DT },
{ id: 'd3', label: 'D3', samples: d[3], t0: 0, dt: DT },
{ id: 'd2', label: 'D2', samples: d[2], t0: 0, dt: DT },
{ id: 'd1', label: 'D1', samples: d[1], t0: 0, dt: DT },
{ id: 'd0', label: 'D0', samples: d[0], t0: 0, dt: DT }
];
spi.buses = [{ id: 'shift', label: 'Shift register[7:0]', lines: ['d7', 'd6', 'd5', 'd4', 'd3', 'd2', 'd1', 'd0'], format: 'hex' }];
spi.cursors = [
{ id: 'c1', label: 'C1', x: (csLow + 10 + 8 * bitSamples) * DT },
{ id: 'c2', label: 'C2', x: (csLow + 10 + 16 * bitSamples) * DT }
];
format.addEventListener('change', function () {
spi.buses = [Object.assign({}, spi.buses[0], { format: format.value })];
});
lines.addEventListener('change', function () {
spi.showLines = lines.checked;
});
zoomByte.addEventListener('click', function () {
spi.zoomTo((csLow + 8 + 8 * bitSamples) * DT, (csLow + 12 + 16 * bitSamples) * DT);
});
spi.addEventListener('cursorChange', function (event) {
spiLog.textContent = 'cursorChange: ' + event.detail.id + ' at ' + (event.detail.x * 1e9).toFixed(0) + ' ns, shift register ' +
spi.busValueAt('shift', event.detail.x).toString(16).toUpperCase().padStart(2, '0');
});
spi.addEventListener('zoom', function (event) {
spiLog.textContent = event.detail.fitted ? 'zoom: fitted' : 'zoom: ' + (event.detail.xMin * 1e9).toFixed(0) + ' to ' + (event.detail.xMax * 1e9).toFixed(0) + ' ns';
});
spi.addEventListener('rowClick', function (event) {
spiLog.textContent = 'rowClick: ' + event.detail.kind + ' ' + event.detail.id + ' at ' + (event.detail.time * 1e9).toFixed(0) + ' ns';
});
/* ── The PLC trace ─────────────────────────────────────────────────── */
const plc = document.getElementById('plc'),
SHIFT = 8 * 3600;
function edgesOf(fn) {
/* A line as edges: the level at a set of times, kept where it changes. */
const edges = [[0, fn(0)]];
for (let t = 1; t < SHIFT; t += 1) {
const level = fn(t);
if (level !== edges[edges.length - 1][1]) {
edges.push([t, level]);
}
}
return edges;
}
const breaks = [[7200, 7500], [16200, 16800], [23400, 23700]],
jamAt = 16200,
guardOpen = [[16250, 16700]];
const running = (t) => breaks.some((b) => t >= b[0] && t < b[1]) ? 0 : 1,
infeed = (t) => running(t) && Math.floor(t / 3) % 2 === 0 ? 1 : 0,
jam = (t) => t >= jamAt && t < jamAt + 40 ? 1 : 0,
guard = (t) => guardOpen.some((g) => t >= g[0] && t < g[1]) ? 0 : 1,
reject = (t) => running(t) && Math.floor(t / 3) % 37 === 5 ? 1 : 0,
lowStock = (t) => (t > 12000 && t < 12900) || (t > 26000 && t < 26300) ? 1 : 0,
hornOut = (t) => jam(t) || (t >= jamAt + 40 && t < jamAt + 60) ? 1 : 0,
lampOut = (t) => running(t) ? 1 : (Math.floor(t) % 2 === 0 ? 1 : 0);
plc.signals = [
{ id: 'i0', label: 'I0.0 Infeed sensor', edges: edgesOf(infeed), end: SHIFT },
{ id: 'i1', label: 'I0.1 Jam detected', edges: edgesOf(jam), end: SHIFT },
{ id: 'i2', label: 'I0.2 Guard closed', edges: edgesOf(guard), end: SHIFT },
{ id: 'i3', label: 'I0.3 Low stock', edges: edgesOf(lowStock), end: SHIFT },
{ id: 'q0', label: 'Q0.0 Run', edges: edgesOf(running), end: SHIFT },
{ id: 'q1', label: 'Q0.1 Reject pusher', edges: edgesOf(reject), end: SHIFT },
{ id: 'q2', label: 'Q0.2 Horn', edges: edgesOf(hornOut), end: SHIFT },
{ id: 'q3', label: 'Q0.3 Stack lamp', edges: edgesOf(lampOut), end: SHIFT }
];
plc.buses = [{ id: 'q', label: 'Q0[3:0]', lines: ['q3', 'q2', 'q1', 'q0'], format: 'bin' }];
plc.showLines = true;
plc.cursors = [{ id: 'a', label: 'jam', x: jamAt + 5 }, { id: 'b', label: 'restart', x: 16805 }];
});
return () => {
unmounted = true;
};
}, []);
return (
<div className="react-root" ref={root} theme="industrial">
<div className="smart-demo-container">
<section id="overview">
<h2>Smart.DigitalWaveform displays digital lines against time, one row per line, with lines grouped into buses whose words are written in hex, binary or decimal.</h2>
<p>It is the view of a logic analyser and of the classic digital waveform graph. A line is given as samples on a fixed interval or as a list of edges, so a sparse capture of a long idle period costs what it has to say. The view pans and zooms, cursors read every line and every bus at two instants and the time between them, and where more transitions land on a pixel column than it can show, the column is drawn as a block, which is what a real analyser does.</p>
</section>
<section>
<h2>An SPI transaction, 8 bit words at 10 MHz</h2>
<p>Chip select falls, the clock runs for three bytes, and the data lines carry a register write: command 0x0B, address 0x2A, value 0xC7. The bus rows read the byte on each edge group. Drag a cursor onto a clock edge or press the arrow keys to step it, use the wheel to zoom about the pointer, hold Shift and drag to zoom into a box, and double-click to fit.</p>
<DigitalWaveform id="spi" label="SPI capture, 100 MS/s" xLabel="Time"></DigitalWaveform>
<div className="demo-actions">
<label>
Bus format
<select id="format"><option value="hex">hex</option> <option value="bin">binary</option> <option value="dec">decimal</option></select>
</label>
<label><input type="checkbox" id="lines" defaultChecked /> Show the lines of a bus</label>
<button type="button" id="zoomByte">Zoom to the second byte</button>
<span id="spiLog" className="demo-log">cursorChange, zoom and rowClick are listed here.</span>
</div>
</section>
<section>
<h2>A PLC I/O trace over a shift, as edges</h2>
<p>Eight inputs and outputs of a packaging machine over eight hours, recorded as edges: an infeed sensor that pulses per product, a jam input, the run output and the guard interlock. A line with a few hundred edges over eight hours is drawn from its edges, and the cursors read the state of every line at two instants.</p>
<DigitalWaveform id="plc" label="Line 2 PLC, I/O trace" xLabel="Time" xUnit="s" rowHeight={24} labelWidth={150}></DigitalWaveform>
<p className="hint">Keyboard: the plot is a tab stop. The arrow keys move the active cursor, with Shift by ten steps; N makes the next cursor active; + and - zoom, 0 fits, Home and End move the cursor to the ends of the view. The cursor legend is text, so the readings are available to a screen reader.</p>
</section>
</div>
</div>
);
};
export default App;
react/App.css
smart-digital-waveform {
display: block;
}
/* ── 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.DigitalWaveform displays digital lines against time, one row per line, with lines grouped into buses whose words are written in hex, binary or decimal.</h2>
<p>
It is the view of a logic analyser and of the classic digital waveform graph. A line is given as samples on a fixed interval or as a list of edges, so a sparse capture of a long idle period costs what it has to say. The view pans and zooms, cursors read every line and every bus at two instants and the time between them, and where more transitions land on a pixel column than it can show, the column is drawn as a block, which is what a real analyser does.
</p>
</section>
<section>
<h2>An SPI transaction, 8 bit words at 10 MHz</h2>
<p>
Chip select falls, the clock runs for three bytes, and the data lines carry a register write: command 0x0B, address 0x2A, value 0xC7. The bus rows read the byte on each edge group. Drag a cursor onto a clock edge or press the arrow keys to step it, use the wheel to zoom about the pointer, hold Shift and drag to zoom into a box, and double-click to fit.
</p>
<smart-digital-waveform id="spi" label="SPI capture, 100 MS/s" x-label="Time"></smart-digital-waveform>
<div class="demo-actions">
<label>Bus format <select id="format">
<option value="hex">hex</option>
<option value="bin">binary</option>
<option value="dec">decimal</option>
</select></label>
<label><input type="checkbox" id="lines" checked /> Show the lines of a bus</label>
<button type="button" id="zoomByte">Zoom to the second byte</button>
<span id="spiLog" class="demo-log">cursorChange, zoom and rowClick are listed here.</span>
</div>
</section>
<section>
<h2>A PLC I/O trace over a shift, as edges</h2>
<p>
Eight inputs and outputs of a packaging machine over eight hours, recorded as edges: an infeed sensor that pulses per product, a jam input, the run output and the guard interlock. A line with a few hundred edges over eight hours is drawn from its edges, and the cursors read the state of every line at two instants.
</p>
<smart-digital-waveform id="plc" label="Line 2 PLC, I/O trace" x-label="Time" x-unit="s" row-height="24" label-width="150"></smart-digital-waveform>
<p class="hint">Keyboard: the plot is a tab stop. The arrow keys move the active cursor, with Shift by ten steps; N makes the next cursor active; + and - zoom, 0 fits, Home and End move the cursor to the ends of the view. The cursor legend is text, so the readings are available to a screen reader.</p>
</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.digitalwaveform.js";
/* Both captures are generated with known content, so what the bus rows read can be checked
against what was put in. The seam is the signals property: an analyser's driver assigns
the same shape and nothing else on the page changes. */
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(() => {
/* ── SPI ───────────────────────────────────────────────────────────── */
const spi = document.getElementById('spi'),
spiLog = document.getElementById('spiLog'),
format = document.getElementById('format'),
lines = document.getElementById('lines'),
zoomByte = document.getElementById('zoomByte');
const RATE = 100e6, DT = 1 / RATE, CLOCK = 10e6, SAMPLES = 400,
BYTES = [0x0B, 0x2A, 0xC7], REPLY = [0x00, 0x00, 0x5A];
/* Mode 0: data is set on the falling edge and sampled on the rising edge. Chip select
falls 40 samples in; each bit takes 10 samples. */
const sclk = new Uint8Array(SAMPLES), mosi = new Uint8Array(SAMPLES), miso = new Uint8Array(SAMPLES), cs = new Uint8Array(SAMPLES),
csLow = 30, bitSamples = RATE / CLOCK, csHigh = csLow + 20 + BYTES.length * 8 * bitSamples;
for (let i = 0; i < SAMPLES; i++) {
cs[i] = i >= csLow && i < csHigh ? 0 : 1;
const bitIndex = Math.floor((i - csLow - 10) / bitSamples),
inWord = i >= csLow + 10 && bitIndex < BYTES.length * 8,
phase = ((i - csLow - 10) % bitSamples) / bitSamples;
sclk[i] = inWord && phase >= 0.5 ? 1 : 0;
const byte = Math.floor(bitIndex / 8),
bit = 7 - (bitIndex % 8);
mosi[i] = inWord ? (BYTES[byte] >> bit) & 1 : 0;
miso[i] = inWord ? (REPLY[byte] >> bit) & 1 : 0;
}
/* The data lines of the bus, decoded per sample from MOSI at each clock: the bus row
reads the byte as it is assembled. */
const d = [];
for (let b = 0; b < 8; b++) {
d.push(new Uint8Array(SAMPLES));
}
let shift = 0;
for (let i = 0; i < SAMPLES; i++) {
if (i > 0 && sclk[i] === 1 && sclk[i - 1] === 0) {
shift = ((shift << 1) | mosi[i]) & 0xFF;
}
for (let b = 0; b < 8; b++) {
d[b][i] = (shift >> b) & 1;
}
}
spi.signals = [
{ id: 'cs', label: 'CS', samples: cs, t0: 0, dt: DT },
{ id: 'sclk', label: 'SCLK', samples: sclk, t0: 0, dt: DT },
{ id: 'mosi', label: 'MOSI', samples: mosi, t0: 0, dt: DT },
{ id: 'miso', label: 'MISO', samples: miso, t0: 0, dt: DT },
{ id: 'd7', label: 'D7', samples: d[7], t0: 0, dt: DT },
{ id: 'd6', label: 'D6', samples: d[6], t0: 0, dt: DT },
{ id: 'd5', label: 'D5', samples: d[5], t0: 0, dt: DT },
{ id: 'd4', label: 'D4', samples: d[4], t0: 0, dt: DT },
{ id: 'd3', label: 'D3', samples: d[3], t0: 0, dt: DT },
{ id: 'd2', label: 'D2', samples: d[2], t0: 0, dt: DT },
{ id: 'd1', label: 'D1', samples: d[1], t0: 0, dt: DT },
{ id: 'd0', label: 'D0', samples: d[0], t0: 0, dt: DT }
];
spi.buses = [{ id: 'shift', label: 'Shift register[7:0]', lines: ['d7', 'd6', 'd5', 'd4', 'd3', 'd2', 'd1', 'd0'], format: 'hex' }];
spi.cursors = [
{ id: 'c1', label: 'C1', x: (csLow + 10 + 8 * bitSamples) * DT },
{ id: 'c2', label: 'C2', x: (csLow + 10 + 16 * bitSamples) * DT }
];
format.addEventListener('change', function () {
spi.buses = [Object.assign({}, spi.buses[0], { format: format.value })];
});
lines.addEventListener('change', function () {
spi.showLines = lines.checked;
});
zoomByte.addEventListener('click', function () {
spi.zoomTo((csLow + 8 + 8 * bitSamples) * DT, (csLow + 12 + 16 * bitSamples) * DT);
});
spi.addEventListener('cursorChange', function (event) {
spiLog.textContent = 'cursorChange: ' + event.detail.id + ' at ' + (event.detail.x * 1e9).toFixed(0) + ' ns, shift register ' +
spi.busValueAt('shift', event.detail.x).toString(16).toUpperCase().padStart(2, '0');
});
spi.addEventListener('zoom', function (event) {
spiLog.textContent = event.detail.fitted ? 'zoom: fitted' : 'zoom: ' + (event.detail.xMin * 1e9).toFixed(0) + ' to ' + (event.detail.xMax * 1e9).toFixed(0) + ' ns';
});
spi.addEventListener('rowClick', function (event) {
spiLog.textContent = 'rowClick: ' + event.detail.kind + ' ' + event.detail.id + ' at ' + (event.detail.time * 1e9).toFixed(0) + ' ns';
});
/* ── The PLC trace ─────────────────────────────────────────────────── */
const plc = document.getElementById('plc'),
SHIFT = 8 * 3600;
function edgesOf(fn) {
/* A line as edges: the level at a set of times, kept where it changes. */
const edges = [[0, fn(0)]];
for (let t = 1; t < SHIFT; t += 1) {
const level = fn(t);
if (level !== edges[edges.length - 1][1]) {
edges.push([t, level]);
}
}
return edges;
}
const breaks = [[7200, 7500], [16200, 16800], [23400, 23700]],
jamAt = 16200,
guardOpen = [[16250, 16700]];
const running = (t) => breaks.some((b) => t >= b[0] && t < b[1]) ? 0 : 1,
infeed = (t) => running(t) && Math.floor(t / 3) % 2 === 0 ? 1 : 0,
jam = (t) => t >= jamAt && t < jamAt + 40 ? 1 : 0,
guard = (t) => guardOpen.some((g) => t >= g[0] && t < g[1]) ? 0 : 1,
reject = (t) => running(t) && Math.floor(t / 3) % 37 === 5 ? 1 : 0,
lowStock = (t) => (t > 12000 && t < 12900) || (t > 26000 && t < 26300) ? 1 : 0,
hornOut = (t) => jam(t) || (t >= jamAt + 40 && t < jamAt + 60) ? 1 : 0,
lampOut = (t) => running(t) ? 1 : (Math.floor(t) % 2 === 0 ? 1 : 0);
plc.signals = [
{ id: 'i0', label: 'I0.0 Infeed sensor', edges: edgesOf(infeed), end: SHIFT },
{ id: 'i1', label: 'I0.1 Jam detected', edges: edgesOf(jam), end: SHIFT },
{ id: 'i2', label: 'I0.2 Guard closed', edges: edgesOf(guard), end: SHIFT },
{ id: 'i3', label: 'I0.3 Low stock', edges: edgesOf(lowStock), end: SHIFT },
{ id: 'q0', label: 'Q0.0 Run', edges: edgesOf(running), end: SHIFT },
{ id: 'q1', label: 'Q0.1 Reject pusher', edges: edgesOf(reject), end: SHIFT },
{ id: 'q2', label: 'Q0.2 Horn', edges: edgesOf(hornOut), end: SHIFT },
{ id: 'q3', label: 'Q0.3 Stack lamp', edges: edgesOf(lampOut), end: SHIFT }
];
plc.buses = [{ id: 'q', label: 'Q0[3:0]', lines: ['q3', 'q2', 'q1', 'q0'], format: 'bin' }];
plc.showLines = true;
plc.cursors = [{ id: 'a', label: 'jam', x: jamAt + 5 }, { id: 'b', label: 'restart', x: 16805 }];
});
});
}
};
</script>
<style>
smart-digital-waveform {
display: block;
}
/* ── 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
- An SPI transaction, 8 bit words at 10 MHz
- A PLC I/O trace over a shift, as edges
On the operator screens
Also in test and measurement
Bode PlotIntensity GraphOctave BandsPolar PlotResource PickerWaveform Graph