<bmx-hmi-panel>
A process display for operating plant: the mimic of a site - tanks, vessels, pumps, motors, fans, valves, pipes, instruments, breakers and controls - drawn from a JSON layout in the high-performance style of ISA-101 and brought to life by live tag values; alarms kept as ISA-18.2 describes them (limits with deadband and delay, priorities, acknowledge, shelve, the journal and its figures) with an alarm list and a flag on the equipment concerned; a faceplate for each item with its values, limits, a trend of the last minutes and the commands it offers; values of bad quality shown as such. The page feeds the values in (from OPC UA, MQTT, a historian - anything) and receives the operator's commands to write back: the panel talks to nothing itself.
11 properties · 6 events · 28 methods · 0 parts
Example
An invented booster pumping station, simulated here a second at a time. Open any item for its faceplate - values, limits, a trend of the last ten minutes and its commands - and start or stop the pumps or change the flow setpoint. Trip the duty pump or lose a signal to see alarms raised, flagged on the equipment and listed below, to acknowledge or shelve. Press Edit to change the display: add symbols from the palette, move, resize and turn them, draw pipes, bind them to tags and check the whole, with undo; the station keeps running as you edit.
Show markup
<bmx-hmi-panel id="ex-hmi" label="Booster pumping station" operator="Operator 1" locale="en-GB" stale-after="10" editable></bmx-hmi-panel>
<p class="note" id="ex-hmi-out" role="status">An invented booster pumping station, simulated here a second at a time. Open any item for its faceplate - values, limits, a trend of the last ten minutes and its commands - and start or stop the pumps or change the flow setpoint. Trip the duty pump or lose a signal to see alarms raised, flagged on the equipment and listed below, to acknowledge or shelve. Press Edit to change the display: add symbols from the palette, move, resize and turn them, draw pipes, bind them to tags and check the whole, with undo; the station keeps running as you edit.</p>
<div class="row" style="gap: 0.75rem; flex-wrap: wrap; align-items: center; margin-block: 0.5rem 1.5rem">
<button type="button" id="ex-hmi-trip">Trip pump P-101</button>
<button type="button" id="ex-hmi-bad">Lose the chlorine signal</button>
<button type="button" id="ex-hmi-reset">Reset the station</button>
</div>
<script type="module">
await customElements.whenDefined('bmx-hmi-panel');
const el = document.getElementById('ex-hmi');
const out = document.getElementById('ex-hmi-out');
const pipe = (id, points, flow, extra = {}) => ({ id, type: 'pipe', x: 0, y: 0, w: 1, h: 1, points, tags: flow ? { flow } : undefined, props: extra });
el.layout = {
version: 1,
title: 'Booster station BS-1',
width: 1120,
height: 600,
tags: [
{ name: 'LT-101', label: 'Raw water tank level', kind: 'analog', unit: '%', min: 0, max: 100, decimals: 1, limits: { lolo: 10, lo: 20, hi: 90, hihi: 95 }, deadband: 1, delay: 2 },
{ name: 'LT-102', label: 'Treated water tank level', kind: 'analog', unit: '%', min: 0, max: 100, decimals: 1, limits: { lo: 15, hi: 92 }, deadband: 1 },
{ name: 'P101.Run', label: 'Pump P-101 running', kind: 'digital', states: ['Stopped', 'Running'] },
{ name: 'P101.Fault', label: 'Pump P-101 fault', kind: 'digital', alarmOn: 1, alarmMessage: 'Pump P-101 tripped (motor overload)', priorities: { state: 1 } },
{ name: 'P101.Speed', label: 'Pump P-101 speed', kind: 'analog', unit: 'rpm', min: 0, max: 3000, decimals: 0 },
{ name: 'P102.Run', label: 'Pump P-102 running', kind: 'digital', states: ['Stopped', 'Running'] },
{ name: 'P102.Fault', label: 'Pump P-102 fault', kind: 'digital', alarmOn: 1, alarmMessage: 'Pump P-102 tripped', priorities: { state: 2 } },
{ name: 'FCV-101.Pos', label: 'Flow control valve position', kind: 'analog', unit: '%', min: 0, max: 100, decimals: 0 },
{ name: 'FT-101', label: 'Discharge flow', kind: 'analog', unit: 'm³/h', min: 0, max: 400, decimals: 0, limits: { lo: 60, hi: 350 }, priorities: { lo: 2 }, deadband: 5, delay: 3 },
{ name: 'PT-101', label: 'Discharge pressure', kind: 'analog', unit: 'bar', min: 0, max: 10, decimals: 2, limits: { hi: 7.5, hihi: 8.5 }, deadband: 0.1 },
{ name: 'FIC-101.SP', label: 'Flow setpoint', kind: 'analog', unit: 'm³/h', min: 0, max: 400, decimals: 0 },
{ name: 'CL-201', label: 'Chlorine residual', kind: 'analog', unit: 'mg/L', min: 0, max: 2, decimals: 2, limits: { lo: 0.2, hi: 1.5 }, qualityAlarm: true, priorities: { quality: 3 } },
{ name: 'P201.Run', label: 'Dosing pump running', kind: 'digital', states: ['Stopped', 'Running'] },
{ name: 'MCC.Live', label: 'Motor control centre live', kind: 'digital' },
{ name: 'CB-101', label: 'P-101 breaker', kind: 'digital', states: ['Open', 'Closed'] },
],
items: [
{ id: 'area-in', type: 'frame', x: 20, y: 60, w: 520, h: 360, label: 'Raw water and pumping' },
{ id: 'area-out', type: 'frame', x: 570, y: 60, w: 530, h: 360, label: 'Treatment and storage' },
{ id: 't101', type: 'tank', x: 60, y: 140, w: 90, h: 140, label: 'T-101', tags: { level: 'LT-101' } },
pipe('p1', [[150, 260], [200, 260], [200, 250], [232, 250]], 'P101.Run'),
pipe('p2', [[200, 260], [200, 360], [232, 360]], 'P102.Run', { arrow: true }),
{ id: 'p101', type: 'pump', x: 232, y: 222, w: 56, h: 56, label: 'P-101', tags: { run: 'P101.Run', fault: 'P101.Fault', speed: 'P101.Speed' }, props: { commands: [{ text: 'Start', tag: 'P101.Cmd', value: 1 }, { text: 'Stop', tag: 'P101.Cmd', value: 0 }, { text: 'Reset trip', tag: 'P101.Reset', value: 1, confirm: false }] } },
{ id: 'p102', type: 'pump', x: 232, y: 332, w: 56, h: 56, label: 'P-102', tags: { run: 'P102.Run', fault: 'P102.Fault' }, props: { commands: [{ text: 'Start', tag: 'P102.Cmd', value: 1 }, { text: 'Stop', tag: 'P102.Cmd', value: 0 }] } },
pipe('p3', [[288, 250], [340, 250], [340, 300], [410, 300]], 'P101.Run', { arrow: false }),
pipe('p4', [[288, 360], [340, 360], [340, 300]], 'P102.Run', { arrow: false }),
{ id: 'fcv101', type: 'control-valve', x: 410, y: 272, w: 44, h: 50, label: 'FCV-101', tags: { position: 'FCV-101.Pos' } },
pipe('p5', [[454, 300], [700, 300], [700, 260], [760, 260]], 'FT-101'),
{ id: 'ft101', type: 'transmitter', x: 480, y: 200, w: 52, h: 52, label: 'FT-101', tags: { value: 'FT-101' }, props: { code: 'FT' } },
{ id: 'pt101', type: 'transmitter', x: 380, y: 130, w: 52, h: 52, label: 'PT-101', tags: { value: 'PT-101' }, props: { code: 'PT' } },
{ id: 's1', type: 'signal', x: 0, y: 0, w: 1, h: 1, points: [[506, 252], [506, 300]] },
{ id: 's2', type: 'signal', x: 0, y: 0, w: 1, h: 1, points: [[406, 182], [406, 230], [340, 230], [340, 250]] },
{ id: 'cl201', type: 'transmitter', x: 600, y: 200, w: 52, h: 52, label: 'AT-201', tags: { value: 'CL-201' }, props: { code: 'AT' } },
{ id: 's3', type: 'signal', x: 0, y: 0, w: 1, h: 1, points: [[626, 252], [626, 300]] },
{ id: 'p201', type: 'pump', x: 640, y: 330, w: 40, h: 40, label: 'P-201', tags: { run: 'P201.Run' }, props: { showState: false } },
pipe('p6', [[660, 330], [660, 300]], 'P201.Run', { width: 3 }),
{ id: 't102', type: 'tank', x: 760, y: 140, w: 110, h: 160, label: 'T-102', tags: { level: 'LT-102' } },
pipe('p7', [[870, 280], [940, 280], [940, 330], [1080, 330]], 'FT-101'),
{ id: 'pt-gauge', type: 'gauge', x: 920, y: 120, w: 100, h: 100, label: 'Discharge pressure', tags: { value: 'PT-101' } },
{ id: 'sp', type: 'setpoint', x: 410, y: 382, w: 100, h: 34, label: 'Flow setpoint', tags: { value: 'FIC-101.SP' } },
{ id: 'trend-flow', type: 'trend', x: 600, y: 470, w: 230, h: 80, label: 'FT-101 flow', tags: { value: 'FT-101' }, props: { minutes: 5 } },
{ id: 'trend-level', type: 'trend', x: 860, y: 470, w: 230, h: 80, label: 'LT-102 level', tags: { value: 'LT-102' }, props: { minutes: 5 } },
{ id: 'bus', type: 'busbar', x: 40, y: 470, w: 360, h: 8, label: 'MCC-1 400 V', tags: { live: 'MCC.Live' } },
{ id: 'cb101', type: 'breaker', x: 80, y: 500, w: 30, h: 30, label: 'CB-101', tags: { closed: 'CB-101' } },
{ id: 'tx1', type: 'transformer', x: 300, y: 494, w: 36, h: 56, label: 'TX-1' },
{ id: 'lamp', type: 'lamp', x: 200, y: 505, w: 24, h: 24, label: 'MCC live', tags: { value: 'MCC.Live' }, props: { tone: 'success' } },
],
};
// A simple simulation of the station, one step a second.
const s = { lt101: 62, lt102: 48, run1: 1, run2: 0, fault1: 0, sp: 220, flow: 220, pos: 55, press: 5.2, cl: 0.62, bad: 0 };
function step() {
const pumping = (s.run1 && !s.fault1) || s.run2;
const target = pumping ? s.sp : 0;
s.flow += (target - s.flow) * 0.35 + (pumping ? (Math.random() - 0.5) * 8 : 0);
s.flow = Math.max(0, s.flow);
s.pos = Math.max(0, Math.min(100, pumping ? (s.flow / 400) * 100 + 8 : 0));
s.press = pumping ? 3.2 + s.flow / 70 + (Math.random() - 0.5) * 0.1 : 0.4;
s.lt101 = Math.max(0, Math.min(100, s.lt101 + 0.25 - s.flow / 2200));
s.lt102 = Math.max(0, Math.min(100, s.lt102 + s.flow / 2600 - 0.07));
s.cl = Math.max(0, Math.min(2, s.cl + (Math.random() - 0.5) * 0.03));
el.setTags({
'LT-101': s.lt101,
'LT-102': s.lt102,
'P101.Run': s.run1 && !s.fault1 ? 1 : 0,
'P101.Fault': s.fault1,
'P101.Speed': s.run1 && !s.fault1 ? 1480 + Math.round((Math.random() - 0.5) * 10) : 0,
'P102.Run': s.run2,
'P102.Fault': 0,
'FCV-101.Pos': s.pos,
'FT-101': s.flow,
'PT-101': s.press,
'FIC-101.SP': s.sp,
'CL-201': s.bad > 0 ? { value: null, quality: 'bad' } : s.cl,
'P201.Run': pumping ? 1 : 0,
'MCC.Live': 1,
'CB-101': s.fault1 ? 0 : 1,
});
if (s.bad > 0) s.bad--;
}
step();
setInterval(step, 1000);
// The operator's commands come back as events: here they drive the simulation.
el.addEventListener('bmxCommand', e => {
const { tag, value, action } = e.detail;
if (tag === 'P101.Cmd') s.run1 = value ? 1 : 0;
if (tag === 'P102.Cmd') s.run2 = value ? 1 : 0;
if (tag === 'P101.Reset') s.fault1 = 0;
if (tag === 'FIC-101.SP') s.sp = Math.max(0, Math.min(400, Number(value)));
out.textContent = `Command sent: ${action} - ${tag} = ${value}.`;
});
el.addEventListener('bmxAlarm', e => {
if (e.detail.kind === 'raised') out.textContent = `Alarm raised: ${e.detail.alarm.message} (priority ${e.detail.alarm.priority}).`;
if (e.detail.kind === 'acknowledged') out.textContent = `Acknowledged by ${e.detail.alarm.by ?? 'the operator'}: ${e.detail.alarm.message}.`;
});
document.getElementById('ex-hmi-trip').addEventListener('click', () => (s.fault1 = 1));
document.getElementById('ex-hmi-bad').addEventListener('click', () => (s.bad = 12));
document.getElementById('ex-hmi-reset').addEventListener('click', () => Object.assign(s, { run1: 1, run2: 0, fault1: 0, sp: 220, bad: 0, lt101: 62, lt102: 48 }));
</script>
<bmx-hmi-panel id="station"></bmx-hmi-panel>
<script>station.layout = layout; feed.on('data', v => station.setTags(v));</script>
Properties
| Property | Attribute | Type | Default | Description |
|---|---|---|---|---|
editable |
editable |
boolean |
false |
Offers an Edit button in the toolbar, to switch to editing and back. |
label |
label |
string |
— | The element's accessible name. |
layout |
layout |
BmxHmiLayout | string |
— | The display: its size, tags and items (an object or its JSON). |
locale |
locale |
string |
— | The locale for numbers and times. Unset: the page's lang. |
mode |
mode |
'run' | 'edit' |
'run' |
Operating the display (run) or editing it (edit). |
operator |
operator |
string |
— | Who is operating, written into acknowledgements and shelving. |
readOnly |
read-only |
boolean |
false |
No commands are offered: a view for those who watch but do not operate. |
showAlarms |
show-alarms |
boolean |
true |
The alarm list under the mimic. |
staleAfter |
stale-after |
number |
0 |
Seconds after which a value not updated is shown as stale. 0: never. |
strings |
strings |
Partial<BmxHmiPanelStrings> | string |
— | Replacements for any of the wording (an object, or its JSON). |
timeZone |
time-zone |
string |
— | The zone alarm times are shown in. Unset: the reader's. |
Events
| Event | Detail | Description |
|---|---|---|
bmxAlarm |
BmxHmiPanelAlarmDetail |
Fired as alarms are raised, cleared, acknowledged, shelved and unshelved. |
bmxCommand |
BmxHmiPanelCommandDetail |
Fired when the operator sends a command: write it to the control system. |
bmxItemOpen |
BmxHmiPanelItemDetail |
Fired when an item's faceplate is opened. |
bmxLayoutChange |
BmxHmiPanelLayoutDetail |
Fired after each change made in the editor (and on undo and redo), with the whole layout to store. |
bmxModeChange |
BmxHmiPanelModeDetail |
Fired when the panel switches between operating and editing. |
bmxSelectionChange |
BmxHmiPanelSelectionDetail |
Fired when the items chosen in the editor change. |
Methods
| Method | Signature | Description |
|---|---|---|
acknowledge |
acknowledge(id: string) => Promise<void> |
Acknowledges an alarm. |
acknowledgeAll |
acknowledgeAll() => Promise<void> |
Acknowledges every alarm shown. |
addItem |
addItem(type: string, x?: number, y?: number, init?: Partial<BmxHmiItem>) => Promise<string | null> |
Adds an item of a symbol (pump, tank ...), centred on a point of the drawing (its middle, unless given). Returns its id. |
checkLayout |
checkLayout() => Promise<BmxHmiIssue[]> |
What is wrong with the display, or worth a look: tags not defined, items bound to nothing, limits out of order and so on. |
clearHistory |
clearHistory() => Promise<void> |
Forgets the values kept for trends. |
closeFaceplate |
closeFaceplate() => Promise<void> |
Closes the faceplate. |
getAlarmEvents |
getAlarmEvents() => Promise<BmxHmiAlarmEvent[]> |
The alarm journal, oldest first. |
getAlarmMetrics |
getAlarmMetrics() => Promise<BmxHmiAlarmMetrics> |
The alarm system's figures (ISA-18.2): alarms in the last ten minutes, standing, chattering, unacknowledged by priority. |
getAlarms |
getAlarms() => Promise<BmxHmiAlarm[]> |
The alarms that are not normal (shelved and suppressed ones too), most urgent first. |
getLayout |
getLayout() => Promise<BmxHmiLayout> |
The display shown. |
getSelection |
getSelection() => Promise<string[]> |
The items chosen in the editor. |
getTag |
getTag(name: string) => Promise<BmxHmiValue | null> |
A tag's value as last set, or null. |
getTags |
getTags() => Promise<Record<string, BmxHmiValue>> |
Every tag's value as last set. |
loadLayoutFile |
loadLayoutFile(file: Blob) => Promise<boolean> |
Opens a display from a JSON file (read in the browser; it goes nowhere). Returns whether it was one. |
openFaceplate |
openFaceplate(id: string) => Promise<void> |
Opens an item's faceplate. |
redo |
redo() => Promise<boolean> |
Makes again the last change taken back. |
removeItems |
removeItems(ids: string[]) => Promise<void> |
Deletes items. |
saveLayoutFile |
saveLayoutFile(name?: string) => Promise<void> |
Saves the display as a JSON file (the reader's browser downloads it). |
select |
select(ids: string[]) => Promise<void> |
Chooses items in the editor (an empty list chooses none). |
setLayout |
setLayout(layout: BmxHmiLayout | string) => Promise<void> |
Shows a display. |
setMode |
setMode(mode: "run" | "edit") => Promise<void> |
Switches between operating the display (run) and editing it (edit). |
setTag |
setTag(name: string, value: number | string | boolean | null, options?: { quality?: BmxHmiQuality; time?: number; }) => Promise<void> |
Sets one tag's value (with its quality and time, if known). |
setTags |
setTags(values: Record<string, BmxHmiPanelTagInput> | Array<{ name: string; value: number | string | boolean | null; quality?: BmxHmiQuality; time?: number; }>) => Promise<void> |
Sets many tags at once: { 'LT-101': 72.4, 'P101.Run': { value: 1, quality: 'good' } }, or an array of { name, value, quality, time }. |
shelve |
shelve(id: string, minutes: number) => Promise<void> |
Shelves an alarm for some minutes. |
suppress |
suppress(tag: string, on: boolean) => Promise<void> |
Suppresses a tag's alarms (by design - while its equipment is out of use), or lets them through again. |
undo |
undo() => Promise<boolean> |
Takes back the last change made in the editor. |
unshelve |
unshelve(id: string) => Promise<void> |
Takes an alarm off the shelf. |
updateItem |
updateItem(id: string, patch: BmxHmiItemPatch) => Promise<void> |
Changes an item: position, size, turn, label, tags by role, settings or points; null removes a label, a role or a setting. |
CSS custom properties
| Property | Description |
|---|---|
--bmx-hmi-panel-alarm-height |
Most height the alarm list takes. |
--bmx-hmi-panel-background |
The mimic's ground. |
--bmx-hmi-panel-bad-quality |
The hatching over values that cannot be trusted. |
--bmx-hmi-panel-equipment |
Equipment at rest (stopped, closed, empty). |
--bmx-hmi-panel-fluid |
Levels in tanks and bars, and trend lines. |
--bmx-hmi-panel-height |
Height of the mimic. |
--bmx-hmi-panel-line |
Outlines, pipes and text on the mimic. |
--bmx-hmi-panel-priority-1 |
Critical alarms (a square flag). |
--bmx-hmi-panel-priority-2 |
High alarms (a triangle). |
--bmx-hmi-panel-priority-3 |
Medium alarms (an inverted triangle). |
--bmx-hmi-panel-priority-4 |
Low alarms (a diamond). |
--bmx-hmi-panel-radius |
Corner radius of the frame. |
--bmx-hmi-panel-running |
Equipment running, valves open, breakers closed. |