v3.0.0

<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

PropertyAttributeTypeDefaultDescription
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

EventDetailDescription
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

MethodSignatureDescription
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

PropertyDescription
--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.