Operator screen
Logic Analyser
A logic analyser: a 16-channel capture of a memory bus with address and data buses decoded, cursors on a cycle and an analyser log.
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>Logic Analyser - 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">LOGIC ANALYSER</span>
<span class="hmi-area">Bench 2 · Controller board rev C · External memory bus, 100 MS/s, 16 channels</span>
</div>
<div class="la-trigger">
<smart-selector-switch id="triggerSource" label="Trigger" value="rd"></smart-selector-switch>
</div>
<div class="hmi-header-actions">
<span class="hmi-conn" id="state"><span class="hmi-conn-dot"></span><span id="stateText">stopped</span></span>
<button class="hmi-button hmi-button-primary" id="singleButton" type="button">Single</button>
<button class="hmi-button" id="runButton" type="button">Run</button>
<button class="hmi-theme-toggle" id="themeToggle" type="button">Light</button>
</div>
</header>
<section class="hmi-body la-body">
<div class="hmi-panel la-capture">
<div class="hmi-panel-header">
<span>Capture</span>
<span class="la-info" id="captureInfo">no capture</span>
</div>
<div class="hmi-panel-content la-capture-content">
<smart-digital-waveform id="capture" label="" x-label="Time" row-height="22" label-width="120"></smart-digital-waveform>
</div>
</div>
<div class="hmi-panel la-decode">
<div class="hmi-panel-header">
<span>Decoded bus cycles</span>
<span class="hmi-panel-note" id="decodeNote">click a cycle to place the cursors on it</span>
</div>
<div class="hmi-panel-content la-decode-content">
<table class="hmi-table la-table" id="cycleTable">
<thead><tr><th>#</th><th>Start</th><th>Type</th><th>Address</th><th>Data</th><th>Setup</th><th>Width</th></tr></thead>
<tbody></tbody>
</table>
</div>
</div>
<div class="hmi-panel la-log">
<div class="hmi-panel-header"><span>Analyser log</span></div>
<div class="hmi-panel-content la-log-content">
<smart-terminal id="log" max-lines="200" prompt="LA> " timestamp-format="time"></smart-terminal>
</div>
</div>
</section>
</div>
<!-- scripts -->
<script type="module" src="node_modules/smart-industrial/source/modules/smart.selectorswitch.js"></script>
<script type="module" src="node_modules/smart-industrial/source/modules/smart.digitalwaveform.js"></script>
<script type="module" src="node_modules/smart-industrial/source/modules/smart.terminal.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" />
/* Logic analyser screen.
*
* The capture is generated: an 8-bit microcontroller's external memory bus, with a latched
* address, a read strobe and a write strobe, running a few read and write cycles around
* the trigger. The decoder walks the capture the way an analyser's protocol decoder does.
* Replace capture() with the analyser's buffer and nothing else on the screen changes. */
window.onload = function () {
const capture = document.getElementById('capture'),
captureInfo = document.getElementById('captureInfo'),
cycleTable = document.getElementById('cycleTable').querySelector('tbody'),
decodeNote = document.getElementById('decodeNote'),
log = document.getElementById('log'),
triggerSource = document.getElementById('triggerSource'),
state = document.getElementById('state'),
stateText = document.getElementById('stateText'),
singleButton = document.getElementById('singleButton'),
runButton = document.getElementById('runButton'),
themeToggle = document.getElementById('themeToggle');
const RATE = 100e6, DT = 1 / RATE, SAMPLES = 900;
let running = null, captureNumber = 0;
/* ── Theme ───────────────────────────────────────────────────────── */
function applyTheme(theme) {
document.body.setAttribute('theme', theme);
const elements = document.querySelectorAll('smart-selector-switch, smart-digital-waveform, smart-terminal');
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');
});
/* ── The bus ─────────────────────────────────────────────────────── */
triggerSource.positions = [
{ id: 'rd', label: 'RD falling', description: 'The first read cycle after arming' },
{ id: 'wr', label: 'WR falling', description: 'The first write cycle after arming' },
{ id: 'ale', label: 'ALE', description: 'Any address latch' }
];
triggerSource.addEventListener('positionRequest', function (event) {
triggerSource.value = event.detail.position;
log.write('trigger source ' + event.detail.position.toUpperCase(), 'info');
});
/* A bus cycle: ALE high while the address is driven on the multiplexed lines, then
RD or WR low for the data phase. Times in samples at 100 MS/s. */
function generate() {
const lines = {
clk: new Uint8Array(SAMPLES), ale: new Uint8Array(SAMPLES), rd: new Uint8Array(SAMPLES), wr: new Uint8Array(SAMPLES),
cs: new Uint8Array(SAMPLES), rdy: new Uint8Array(SAMPLES)
};
const a = [], d = [];
for (let b = 0; b < 8; b++) {
a.push(new Uint8Array(SAMPLES));
d.push(new Uint8Array(SAMPLES));
}
lines.rd.fill(1);
lines.wr.fill(1);
lines.cs.fill(1);
lines.rdy.fill(1);
const cycles = [],
program = [
{ type: 'read', address: 0x2000 + Math.floor(Math.random() * 16), data: 0x3C },
{ type: 'read', address: 0x2001, data: 0xA7 },
{ type: 'write', address: 0x4010, data: 0x01 },
{ type: 'read', address: 0x2002, data: 0x5A + Math.floor(Math.random() * 4) },
{ type: 'write', address: 0x4011, data: 0xFF },
{ type: 'read', address: 0x2003, data: 0x00 }
],
width = 36, setup = 9, hold = 4;
let t = 40 + Math.floor(Math.random() * 20);
/* The trigger: the capture is aligned so the chosen strobe's first edge sits at
10 % of the record. */
let triggerAt = null;
program.forEach(function (cycle) {
const aleStart = t, aleEnd = t + 12, strobeStart = aleEnd + 6, strobeEnd = strobeStart + width;
for (let i = aleStart; i < aleEnd; i++) {
lines.ale[i] = 1;
}
/* The high address byte is held for the whole cycle; the low byte shares the
data lines and is latched by ALE. */
for (let i = aleStart - 2; i < strobeEnd + hold; i++) {
for (let b = 0; b < 8; b++) {
a[b][i] = (cycle.address >> (8 + b)) & 1;
}
}
for (let i = aleStart - 2; i < aleEnd + 2; i++) {
for (let b = 0; b < 8; b++) {
d[b][i] = (cycle.address >> b) & 1;
}
}
for (let i = strobeStart - 8; i < strobeEnd + 10; i++) {
lines.cs[i] = 0;
}
const strobe = cycle.type === 'read' ? lines.rd : lines.wr,
dataStart = cycle.type === 'read' ? strobeStart + setup : strobeStart - setup,
dataEnd = strobeEnd + hold;
for (let i = strobeStart; i < strobeEnd; i++) {
strobe[i] = 0;
}
for (let i = dataStart; i < dataEnd; i++) {
for (let b = 0; b < 8; b++) {
d[b][i] = (cycle.data >> b) & 1;
}
}
if (cycle.type === 'read') {
for (let i = strobeStart + 2; i < strobeStart + setup; i++) {
lines.rdy[i] = 0;
}
}
if (triggerAt === null && ((triggerSource.value === 'rd' && cycle.type === 'read') || (triggerSource.value === 'wr' && cycle.type === 'write') || triggerSource.value === 'ale')) {
triggerAt = triggerSource.value === 'ale' ? aleStart : strobeStart;
}
cycles.push({
type: cycle.type, address: cycle.address, data: cycle.data,
start: strobeStart, end: strobeEnd, setup: cycle.type === 'read' ? setup : setup, aleStart: aleStart
});
t = strobeEnd + 24 + Math.floor(Math.random() * 20);
});
for (let i = 0; i < SAMPLES; i++) {
lines.clk[i] = (i % 10) < 5 ? 1 : 0;
}
return { lines: lines, a: a, d: d, cycles: cycles, t0: triggerAt === null ? 0 : -triggerAt * DT };
}
function show() {
const record = generate(),
t0 = record.t0;
captureNumber++;
const signals = [
{ id: 'clk', label: 'CLK', samples: record.lines.clk, t0: t0, dt: DT },
{ id: 'ale', label: 'ALE', samples: record.lines.ale, t0: t0, dt: DT },
{ id: 'cs', label: 'CS', samples: record.lines.cs, t0: t0, dt: DT },
{ id: 'rd', label: 'RD', samples: record.lines.rd, t0: t0, dt: DT },
{ id: 'wr', label: 'WR', samples: record.lines.wr, t0: t0, dt: DT },
{ id: 'rdy', label: 'READY', samples: record.lines.rdy, t0: t0, dt: DT }
];
for (let b = 7; b >= 0; b--) {
signals.push({ id: 'a' + b, label: 'A' + (8 + b), samples: record.a[b], t0: t0, dt: DT });
}
for (let b = 7; b >= 0; b--) {
signals.push({ id: 'd' + b, label: 'AD' + b, samples: record.d[b], t0: t0, dt: DT });
}
capture.signals = signals;
capture.buses = [
{ id: 'ah', label: 'A[15:8]', lines: ['a7', 'a6', 'a5', 'a4', 'a3', 'a2', 'a1', 'a0'], format: 'hex' },
{ id: 'ad', label: 'AD[7:0]', lines: ['d7', 'd6', 'd5', 'd4', 'd3', 'd2', 'd1', 'd0'], format: 'hex' }
];
capture.showLines = false;
capture.label = 'Capture ' + captureNumber + ', trigger ' + triggerSource.value.toUpperCase();
const first = record.cycles[0];
capture.cursors = [
{ id: 'c1', label: 'C1', x: t0 + first.start * DT },
{ id: 'c2', label: 'C2', x: t0 + first.end * DT }
];
captureInfo.textContent = SAMPLES + ' samples at 100 MS/s, ' + (SAMPLES * DT * 1e6).toFixed(0) + ' µs, ' + record.cycles.length + ' cycles decoded';
cycleTable.innerHTML = record.cycles.map(function (cycle, i) {
return '<tr data-index="' + i + '" tabindex="0"><td>' + (i + 1) + '</td>' +
'<td class="mono">' + ((t0 + cycle.start * DT) * 1e9).toFixed(0) + ' ns</td>' +
'<td>' + (cycle.type === 'read' ? 'RD' : 'WR') + '</td>' +
'<td class="mono">0x' + cycle.address.toString(16).toUpperCase().padStart(4, '0') + '</td>' +
'<td class="mono">0x' + cycle.data.toString(16).toUpperCase().padStart(2, '0') + '</td>' +
'<td class="mono">' + (cycle.setup * DT * 1e9).toFixed(0) + ' ns</td>' +
'<td class="mono">' + ((cycle.end - cycle.start) * DT * 1e9).toFixed(0) + ' ns</td></tr>';
}).join('');
log.write('capture ' + captureNumber + ': ' + record.cycles.length + ' cycles, trigger on ' + triggerSource.value.toUpperCase() + ' at 0 ns', 'success');
record.cycles.forEach(function (cycle) {
log.write((cycle.type === 'read' ? 'RD ' : 'WR ') + '0x' + cycle.address.toString(16).toUpperCase().padStart(4, '0') + ' = 0x' + cycle.data.toString(16).toUpperCase().padStart(2, '0'), 'info');
});
cycleTable.querySelectorAll('tr').forEach(function (row) {
const place = function () {
const cycle = record.cycles[Number(row.getAttribute('data-index'))];
capture.cursors = [
{ id: 'c1', label: 'C1', x: t0 + cycle.start * DT },
{ id: 'c2', label: 'C2', x: t0 + cycle.end * DT }
];
capture.zoomTo(t0 + (cycle.aleStart - 10) * DT, t0 + (cycle.end + 20) * DT);
decodeNote.textContent = 'cycle ' + (Number(row.getAttribute('data-index')) + 1) + ': cursors on the strobe, view zoomed to the cycle';
};
row.addEventListener('click', place);
row.addEventListener('keydown', function (event) {
if (event.key === 'Enter' || event.key === ' ') {
place();
event.preventDefault();
}
});
});
}
capture.addEventListener('cursorChange', function (event) {
decodeNote.textContent = 'cursor ' + event.detail.id + ' at ' + (event.detail.x * 1e9).toFixed(0) + ' ns, AD[7:0] = 0x' +
(isFinite(capture.busValueAt('ad', event.detail.x)) ? capture.busValueAt('ad', event.detail.x).toString(16).toUpperCase().padStart(2, '0') : '??');
});
singleButton.addEventListener('click', function () {
if (running) {
clearInterval(running);
running = null;
runButton.textContent = 'Run';
}
state.setAttribute('data-state', 'running');
stateText.textContent = 'armed';
setTimeout(function () {
show();
state.setAttribute('data-state', 'ok');
stateText.textContent = 'triggered';
}, 300);
});
runButton.addEventListener('click', function () {
if (running) {
clearInterval(running);
running = null;
runButton.textContent = 'Run';
stateText.textContent = 'stopped';
state.setAttribute('data-state', 'ok');
return;
}
running = setInterval(show, 2000);
runButton.textContent = 'Stop';
stateText.textContent = 'running';
state.setAttribute('data-state', 'running');
});
log.write('Bench 2 logic analyser, 16 channels at 100 MS/s. Press Single to capture.', 'info');
applyTheme('industrial-dark');
show();
state.setAttribute('data-state', 'ok');
stateText.textContent = 'triggered';
};
styles.css
/* Logic analyser screen: the capture above, the decoded cycles and the log below. */
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);
}
.la-trigger {
flex: 0 0 auto;
}
.la-body {
display: grid;
grid-template-columns: minmax(0, 3fr) minmax(320px, 2fr);
grid-template-rows: auto minmax(0, 1fr);
gap: 10px;
min-height: 0;
}
.la-capture {
grid-column: 1 / 3;
}
.la-capture-content {
overflow: auto;
padding: 8px;
}
.la-info {
text-transform: none;
letter-spacing: 0;
font-weight: 400;
color: var(--smart-text-secondary-color);
font-variant-numeric: tabular-nums;
}
.la-decode-content {
overflow: auto;
}
.la-table {
width: 100%;
font-size: 12px;
}
.la-table tbody tr {
cursor: pointer;
}
.la-table tbody tr:hover,
.la-table tbody tr:focus-visible {
background: var(--smart-ui-state-hover);
outline: none;
}
.la-table .mono {
font-family: Consolas, "Courier New", monospace;
}
.la-log-content {
display: flex;
min-height: 0;
padding: 0;
}
.la-log-content smart-terminal {
flex: 1 1 auto;
height: 100%;
min-height: 160px;
--smart-terminal-height: auto;
}
@media (max-width: 1000px) {
body.viewport {
overflow: auto;
}
.hmi-screen {
height: auto;
}
.la-body {
grid-template-columns: 1fr;
grid-template-rows: none;
}
.la-capture {
grid-column: auto;
}
.la-log-content smart-terminal {
height: 220px;
}
}