"""Build the CI-Eng-Sample demo displays (CI Server R1.03.00, HTML mode = desktop + web). python build.py [] (default: the displays folder, ..) See README.md in this folder. Displays: CIENG_Header, CIENG_Overview, CIENG_Control, CIENG_Trends, CIENG_Alarms, CIENG_System, CIENG_AlarmBanner (frames of layout CIENG_Operator) and CIENG_TankFaceplate (layout CIENG_Popup). """ import os, sys sys.path.insert(0, os.path.dirname(__file__)) from cixml import * S = 'CI-Eng-Sample.' LEVEL, INFLOW, VOLUME = S + 'TANK1.LEVEL', S + 'TANK1.INFLOW', S + 'TANK1.CALC.VOLUME' LVL_COLORS = [(0, 'red'), (10, 'amber'), (20, 'teal'), (80, 'amber'), (90, 'red')] # LL/L/normal/H/HH # live-data feed, installed by feed\install_feed.ps1 into a fixed folder outside CI Server FEED_START = r'cmd /c "C:\Users\Public\CI-Eng-Sample\feed\cieng_feed_start.cmd"' W, H, HD = 1880, 760, 680 # content displays (tab frames): 1880 x 760 (about 2.5:1, between the # 16:9 and 16:10 content areas); coordinates below are designed for 680 high FACEPLATE = act_open('CIENG_TankFaceplate', 'CIENG_Popup', 'default') FACEPLATE_CLICK = act_open('CIENG_TankFaceplate', 'CIENG_Popup', 'default', event='Clicked b1') def card(d, l, t, w, h, title, sub=None): d.rect(l, t, w, h, fill='card', stroke='line', r=10) d.text(l + 20, t + 12, w - 40, 22, title.upper(), 11, 'muted', bold=True) if sub: d.text(l + 20, t + 34, w - 40, 18, sub, 10, 'dim') def kv(d, l, t, w, label, item, fmt, unit, size=22, color='text', extra=()): d.text(l, t, w, 16, label, 10, 'muted') d.number(l, t + 18, w - 50, 30, item, fmt, size, color, align='Left', extra=extra) d.text(l + w - 48, t + 26, 48, 18, unit, 11, 'muted') def level_color(prop='number.fillPaint'): return conn(prop, LEVEL, transform=tr_threshold(LVL_COLORS)) def gauge(d, cx, cy, r, item, mn, mx, fmt, unit, label, color='teal', thresholds=None, extra=()): d.ring_gauge(cx, cy, r, item, mn, mx, color, 12, thresholds) d.number(cx - r + 14, cy - 22, 2 * r - 28, 36, item, fmt, 26, align='Center', extra=extra) d.text(cx - 40, cy + 16, 80, 18, unit, 11, 'muted', align='Center') d.text(cx - r, cy + r - 6, 2 * r, 20, label, 12, 'text', bold=True, align='Center') def build_header(): d = Display('CIENG_Header', 1920, 96, 'Header', bg='panel') d.rect(0, 0, 1920, 96, fill='panel') d.line(0, 95, 1920, 95, 'line', 1) d.rect(20, 18, 60, 60, fill='blue', r=14) d.text(20, 34, 60, 28, 'CI', 22, 'white', bold=True, align='Center') d.text(96, 18, 380, 30, 'CI-Eng-Sample', 22, 'text', bold=True) d.text(96, 52, 400, 20, 'Operations dashboard | Tank farm demo', 12, 'muted') # KPI chips chips = [(520, 'TANK LEVEL', LEVEL, '999.9', '%', [level_color()]), (700, 'INFLOW', INFLOW, '9999.9', 'm3/h', []), (880, 'VOLUME', VOLUME, '9999.9', 'm3', [])] for x, label, item, fmt, unit, extra in chips: d.rect(x, 14, 168, 68, fill='card', stroke='line', r=8) d.text(x + 14, 22, 140, 16, label, 10, 'muted', bold=True) d.number(x + 14, 40, 100, 32, item, fmt, 22, align='Left', extra=extra) d.text(x + 118, 50, 46, 18, unit, 11, 'muted') x = 1060 d.rect(x, 14, 168, 68, fill='card', stroke='line', r=8) d.text(x + 14, 22, 140, 16, 'PUMP 1', 10, 'muted', bold=True) d.lamp(x + 14, 48, 18, S + 'PUMP1.RUNNING') for txt, col, on in (('RUNNING', 'green', True), ('STOPPED', 'muted', False)): d.text(x + 40, 44, 120, 24, txt, 14, col, bold=True) d.parts[-1] = d.parts[-1].replace('', data([conn('text.visible', S + 'PUMP1.RUNNING', transform=tr_threshold([(False, not on), (True, on)], 'Boolean', 'Boolean'))]) + '') x = 1240 d.rect(x, 14, 168, 68, fill='card', stroke='line', r=8) d.text(x + 14, 22, 140, 16, 'COMMUNICATION', 10, 'muted', bold=True) d.lamp(x + 14, 50, 14, S + 'PLC1.COMM_OK', off='red') d.text(x + 34, 46, 50, 20, 'PLC1', 12, 'text') d.lamp(x + 90, 50, 14, S + 'UA.COMM_OK', off='red') d.text(x + 110, 46, 50, 20, 'OPC UA', 12, 'text') # live data start/stop x = 1424 d.rect(x, 14, 250, 68, fill='card', stroke='line', r=8) d.text(x + 14, 22, 120, 16, 'LIVE DATA', 10, 'muted', bold=True) d.lamp(x + 112, 23, 12, S + 'DEMO.FEED_ON') d.button(x + 14, 44, 104, 30, 'Start', [act_set(S + 'DEMO.FEED_ON', 1), act_cmd(FEED_START)], fg='white', bg='green', tip='Start the live-data feed (cieng_feed.ps1) on the server') d.button(x + 130, 44, 104, 30, 'Stop', [act_set(S + 'DEMO.FEED_ON', 0)], fg='text', bg='card2', tip='Stop the feed (ends within 5 s)') # faceplate + clock d.button(1688, 14, 92, 68, 'Tank 1', [FACEPLATE], fg='white', bg='blue', tip='Open the Tank 1 faceplate popup') d.text(1792, 22, 120, 16, 'LAST UPDATE', 10, 'muted', bold=True) d.time_text(1792, 44, 122, 28, S + 'DEMO.COUNTER', 9) return d def build_overview(): d = Display('CIENG_Overview', W, H, 'Overview', design_h=HD) d.rect(0, 0, W, HD, fill='bg') card(d, 10, 10, 1110, 660, 'Process - Tank 1', 'Click the tank for its faceplate') # inflow pipe + flow indicator d.rect(60, 330, 380, 16, fill='track') d.rect(60, 333, 380, 10, fill='teal', actions=[conn('rectangle.visible', INFLOW, transform=tr_threshold([(0, False), (5, True)], 'Double', 'Boolean'))]) d.text(60, 250, 200, 16, 'INFLOW', 10, 'muted', bold=True) d.number(60, 268, 150, 40, INFLOW, '9999.9', 30, align='Left') d.text(214, 282, 60, 20, 'm3/h', 12, 'muted') # tank d.rect(440, 100, 240, 480, fill='panel', stroke='line', sw=2, r=18, actions=[FACEPLATE_CLICK], tip='Tank 1 - click for the faceplate') d.rect(450, 110, 220, 460, fill='blue', r=12, fill_amount=0.5, fill_dir='FillDirectionUp', actions=[conn('rectangle.fillAmount', LEVEL, transform=tr_linear(0, 100, 0, 1)), conn('rectangle.fillPaint', LEVEL, transform=tr_threshold( [(0, (185, 28, 28)), (10, (180, 83, 9)), (20, (37, 99, 235)), (80, (180, 83, 9)), (90, (185, 28, 28))])), FACEPLATE_CLICK]) d.text(450, 60, 220, 28, 'TANK 1', 16, 'text', bold=True, align='Center') d.number(460, 300, 200, 50, LEVEL, '999.9', 40, 'white', align='Center') d.text(460, 352, 200, 20, '% level', 12, 'text', align='Center') for pct, lab, col in ((90, 'HH 90', 'red'), (80, 'H 80', 'amber'), (20, 'L 20', 'amber'), (10, 'LL 10', 'red')): y = 570 - pct * 4.6 d.line(682, y, 700, y, col, 2) d.text(706, y - 9, 60, 18, lab, 10, col, bold=True) d.button(470, 594, 180, 32, 'Open faceplate', [FACEPLATE], fg='white', bg='blue') # outlet pipe + pump d.rect(680, 532, 150, 16, fill='track') d.rect(680, 535, 150, 10, fill='teal', actions=[conn('rectangle.visible', S + 'PUMP1.RUNNING')]) d.ellipse(830, 495, 90, 90, fill='off', stroke='line', sw=2, actions=[ conn('ellipse.fillPaint', S + 'PUMP1.RUNNING', transform=tr_threshold([(False, 'off'), (True, 'green')], 'Boolean'))]) d.text(830, 528, 90, 24, 'P1', 16, 'white', bold=True, align='Center') d.rect(920, 532, 170, 16, fill='track') d.rect(920, 535, 170, 10, fill='teal', actions=[conn('rectangle.visible', S + 'PUMP1.RUNNING')]) d.text(800, 600, 150, 18, 'PUMP 1', 11, 'muted', bold=True, align='Center') for txt, col, val in (('AUTO', 'teal', 1), ('MANUAL', 'amber', 0)): d.text(800, 620, 150, 20, txt, 13, col, bold=True, align='Center') d.parts[-1] = d.parts[-1].replace('', data([conn('text.visible', S + 'PUMP1.MODE', transform=tr_threshold( [(0, val == 0), (1, val == 1)], 'Double', 'Boolean'))]) + '') # info column x = 860 d.rect(x, 90, 230, 330, fill='panel', stroke='line', r=8) kv(d, x + 16, 104, 200, 'VOLUME (TANK1.CALC)', VOLUME, '9999.9', 'm3') kv(d, x + 16, 168, 200, 'FLOW PER SECOND (JFLOW)', S + 'TANK1.JFLOW.sPerMin', '999.99', 'm3/s') kv(d, x + 16, 232, 200, 'LEVEL MAXIMUM', S + 'TANK1.LEVEL.MAX', '999.9', '%') kv(d, x + 16, 296, 200, 'COUNTER (DEMO.INCR1)', S + 'DEMO.COUNTER', '999', '') d.databar(x + 16, 352, 198, 10, S + 'DEMO.COUNTER', 0, 100, 'violet', 'track', 'Right') d.text(x + 16, 376, 200, 16, 'PUMP RUN TIME', 10, 'muted') d.number(x + 16, 392, 150, 24, S + 'PUMP1.RUNNING.RUN_SECONDS', '9999999', 16, align='Left') d.text(x + 170, 396, 40, 18, 's', 11, 'muted') # gauges card(d, 1130, 10, 740, 330, 'Key figures') gauge(d, 1255, 175, 95, LEVEL, 0, 100, '999.9', '%', 'Level', thresholds=LVL_COLORS, extra=[level_color()]) gauge(d, 1500, 175, 95, INFLOW, 0, 500, '9999', 'm3/h', 'Inflow', 'violet') gauge(d, 1745, 175, 95, VOLUME, 0, 200, '999.9', 'm3', 'Volume', 'blue') card(d, 1130, 350, 740, 320, 'Last 10 minutes', 'Micro trend: level (teal, 0-100 %) and inflow (violet, 0-500 m3/h)') d.micro_trend(1150, 410, 700, 240, [(LEVEL, 0, 100, 'teal'), (INFLOW, 0, 500, 'violet')]) return d def build_control(): d = Display('CIENG_Control', W, H, 'Process control', design_h=HD) d.rect(0, 0, W, HD, fill='bg') # setpoint -> Modbus loopback card(d, 10, 10, 925, 325, 'Setpoint -> PLC1 (Modbus loopback)', 'Slider, spinner and input field write PLC1.SETPOINT; READBACK returns through Modbus') d.slider(40, 70, 600, 64, S + 'PLC1.SETPOINT', 0, 100, 10, 5) d.text(40, 150, 140, 16, 'SPINNER', 10, 'muted', bold=True) d.spinner(40, 170, 140, 36, S + 'PLC1.SETPOINT', 0, 100, 5) d.text(200, 150, 140, 16, 'INPUT FIELD', 10, 'muted', bold=True) d.number_field(200, 170, 140, 36, S + 'PLC1.SETPOINT', '999', 16) d.text(360, 150, 160, 16, 'SETPOINT', 10, 'muted', bold=True) d.number(360, 168, 120, 40, S + 'PLC1.SETPOINT', '999', 28, 'blue', align='Left') d.text(40, 226, 300, 16, 'READBACK (data bar + scale)', 10, 'muted', bold=True) d.databar(40, 248, 600, 18, S + 'PLC1.READBACK', 0, 100, 'blue', 'track', 'Right') d.rect_scale(40, 268, 600, 36, 0, 100, 10, 5, vertical=False) gauge(d, 790, 175, 95, S + 'PLC1.READBACK', 0, 100, '999', '', 'Readback', 'blue') d.lamp(700, 30, 12, S + 'PLC1.COMM_OK', off='red') d.text(718, 26, 200, 18, 'station CIENG_PLC', 11, 'muted') # digital command card(d, 945, 10, 925, 325, 'Digital command -> PLC1.CMD', 'Toggle and push buttons write the coil; FEEDBACK is read back from the input bit') d.toggle(975, 80, 200, 48, 'CMD (toggle)', S + 'PLC1.CMD', 14) d.button(975, 150, 95, 40, 'Set ON', [act_set(S + 'PLC1.CMD', 1)], fg='white', bg='green') d.button(1080, 150, 95, 40, 'Set OFF', [act_set(S + 'PLC1.CMD', 0)], fg='text', bg='card2') d.ellipse(1300, 80, 120, 120, fill='off', stroke='line', sw=3, actions=[ conn('ellipse.fillPaint', S + 'PLC1.FEEDBACK', transform=tr_threshold([(False, 'off'), (True, 'green')], 'Boolean'))]) d.text(1300, 128, 120, 24, 'FEEDBACK', 13, 'white', bold=True, align='Center') d.text(1460, 90, 380, 16, 'LAST CHANGE OF FEEDBACK', 10, 'muted', bold=True) d.datetime_field(1460, 112, 300, 30, S + 'PLC1.FEEDBACK', 'ItemUpdTime', 13) # date/time field (editable on the web) d.text(1460, 160, 380, 16, 'STATUS', 10, 'muted', bold=True) d.text(1460, 180, 380, 24, 'NORMAL', 16, 'text', bold=True) d.parts[-1] = d.parts[-1].replace('', data([conn('text.value', S + 'PLC1.FEEDBACK', 'ItemStatusMnemonic')]) + '') # pump card(d, 10, 345, 925, 325, 'Pump 1', 'Combo box, buttons and check box write PUMP1.MODE / PUMP1.RUNNING (mode changes are audited)') d.text(40, 400, 160, 16, 'MODE (COMBO BOX)', 10, 'muted', bold=True) d.combo(40, 420, 200, 34, S + 'PUMP1.MODE', '0;Manual;1;Auto', 14) d.button(260, 420, 110, 34, 'Manual', [act_set(S + 'PUMP1.MODE', 0)], fg='text', bg='card2') d.button(380, 420, 110, 34, 'Auto', [act_set(S + 'PUMP1.MODE', 1)], fg='white', bg='blue') d.text(40, 478, 300, 16, 'RUN COMMAND (CHECK BOX, MANUAL MODE)', 10, 'muted', bold=True) d.checkbox(40, 498, 240, 32, 'Pump running', S + 'PUMP1.RUNNING', 14) d.text(40, 552, 300, 16, 'DESCRIPTION (TEXT FIELD)', 10, 'muted', bold=True) d.text_field(40, 572, 450, 30, S + 'PUMP1.MODE', 'ItemDesc', 13) d.ellipse(600, 420, 110, 110, fill='off', stroke='line', sw=3, actions=[ conn('ellipse.fillPaint', S + 'PUMP1.RUNNING', transform=tr_threshold([(False, 'off'), (True, 'green')], 'Boolean'))]) d.text(600, 462, 110, 24, 'P1', 18, 'white', bold=True, align='Center') d.text(560, 548, 220, 16, 'RUN TIME (INTEGRATOR)', 10, 'muted', bold=True, align='Center') d.number(560, 566, 190, 34, S + 'PUMP1.RUNNING.RUN_SECONDS', '9999999', 24, align='Right') d.text(754, 576, 30, 18, 's', 12, 'muted') # alarm test card(d, 945, 345, 925, 325, 'Tank 1 - alarm test', 'Stop live data first, then drive TANK1.LEVEL across its limits (H 80 / HH 90 / L 20 / LL 10)') d.slider(975, 410, 560, 64, LEVEL, 0, 100, 10, 5) for i, (lab, v, col, fg) in enumerate((('Low-Low 5', 5, 'red', 'white'), ('Low 15', 15, 'amber', 'white'), ('Normal 50', 50, 'teal', 'white'), ('High 85', 85, 'amber', 'white'), ('High-High 95', 95, 'red', 'white'))): d.button(975 + i * 114, 500, 106, 38, lab, [act_set(LEVEL, v)], fg=fg, bg=col) d.text(975, 566, 200, 16, 'LEVEL', 10, 'muted', bold=True) d.number(975, 584, 160, 50, LEVEL, '999.9', 38, align='Left', extra=[level_color()]) d.text(1180, 566, 300, 16, 'ITEM STATUS', 10, 'muted', bold=True) d.text(1180, 590, 300, 34, 'NORMAL', 22, 'text', bold=True) d.parts[-1] = d.parts[-1].replace('', data([conn('text.value', LEVEL, 'ItemStatusMnemonic'), conn('text.fillPaint', LEVEL, transform=tr_threshold(LVL_COLORS))]) + '') d.ring_gauge(1720, 520, 90, LEVEL, 0, 100, 'teal', 14, LVL_COLORS) d.circ_scale(1720, 520, 120, 0, 100, 20, 5) d.number(1660, 500, 120, 40, LEVEL, '999.9', 26, align='Center') return d def build_trends(): d = Display('CIENG_Trends', W, H, 'Trends', design_h=HD) d.rect(0, 0, W, HD, fill='bg') d.rect(10, 10, 1380, 660, fill='panel', stroke='line', r=10) d.trend(20, 20, 1360, 640, [(LEVEL, 'teal', 0, 100, 'Tank 1 level (%)'), (INFLOW, 'violet', 0, 500, 'Tank 1 inflow (m3/h)')], 10, 'Tank 1 - level and inflow', web_spares=False) # user choice: hairline shows 2 values; the web main trend then draws no line for i, (title, item, mn, mx, col, fmt, unit) in enumerate(( ('Level', LEVEL, 0, 100, 'teal', '999.9', '%'), ('Inflow', INFLOW, 0, 500, 'violet', '9999.9', 'm3/h'), ('CPU load', S + 'SYSTEM.CPU_LOAD', 0, 100, 'blue', '999.9', '%'))): t = 10 + i * 223 card(d, 1400, t, 470, 213, title) d.number(1660, t + 8, 150, 30, item, fmt, 20, col, align='Right') d.text(1814, t + 14, 50, 18, unit, 11, 'muted') d.micro_trend(1420, t + 48, 430, 150, [(item, mn, mx, col)], history=(i < 2)) # CPU_LOAD has no history return d def build_alarms(): d = Display('CIENG_Alarms', W, H, 'Alarms', design_h=HD) d.rect(0, 0, W, HD, fill='bg') card(d, 10, 10, 1860, 390, 'Current alarms', 'All areas - acknowledge with the check box or right-click') d.alarm_overview(20, 60, 1840, 330, size=13) card(d, 10, 410, 1860, 260, 'Alarm history - Tank 1', 'Historical overview filtered on alarm selection area CIENG_TANK1') d.alarm_overview(20, 460, 1840, 200, 'HistoricalAlarms', asa='CIENG_TANK1', size=13) return d def build_system(): d = Display('CIENG_System', W, H, 'System', design_h=HD) d.rect(0, 0, W, HD, fill='bg') card(d, 10, 10, 600, 320, 'Server health', 'Real values of this PC, written by the live-data feed') th = [(0, 'teal'), (80, 'amber'), (90, 'red')] gauge(d, 160, 180, 100, S + 'SYSTEM.CPU_LOAD', 0, 100, '999.9', '%', 'CPU load', thresholds=th) gauge(d, 450, 180, 100, S + 'SYSTEM.DISK_C', 0, 100, '999.9', '%', 'Disk C: used', 'blue', thresholds=th) card(d, 620, 10, 600, 320, 'Communication and processes') rows = ((S + 'PLC1.COMM_OK', 'Modbus station CIENG_PLC', 'Boolean', None), (S + 'UA.COMM_OK', 'OPC UA client station CIENG_UA', 'Boolean', None), (S + 'UA.SRV_STATE', 'OPC UA server state (0 = running)', 'Double', [(0, 'green'), (0.5, 'red')]), (S + 'SYSTEM.PING_LOCALHOST', 'localhost reachable', 'Double', [(0, 'red'), (0.5, 'green')]), (S + 'SYSTEM.PROC_ALM', 'ALARM process (alm) running', 'Double', [(0, 'red'), (0.5, 'green')])) for i, (item, lab, inp, tbl) in enumerate(rows): y = 60 + i * 52 d.rect(640, y, 560, 44, fill='panel', r=6) if inp == 'Boolean': d.lamp(656, y + 13, 18, item, off='red') else: d.lamp(656, y + 13, 18, item, inp='Double', table=tbl) d.text(688, y + 12, 340, 20, lab, 13, 'text') d.text(1040, y + 12, 150, 20, 'NORMAL', 12, 'muted', align='Right') d.parts[-1] = d.parts[-1].replace('', data([conn('text.value', item, 'ItemStatusMnemonic')]) + '') card(d, 1230, 10, 640, 320, 'OPC UA loopback', 'TANK1.LEVEL read back through CI Server\'s own OPC UA server') for i, (lab, item, col) in enumerate((('TANK1.LEVEL', LEVEL, 'teal'), ('UA.LEVEL_COPY', S + 'UA.LEVEL_COPY', 'blue'))): x = 1260 + i * 300 d.text(x, 70, 260, 16, lab, 10, 'muted', bold=True) d.number(x, 90, 200, 44, item, '999.9', 32, col, align='Left') d.databar(x, 150, 260, 18, item, 0, 100, col, 'track', 'Right') d.text(x, 182, 260, 16, 'LAST UPDATE', 10, 'muted', bold=True) d.time_text(x, 202, 260, 28, item, 13) d.rect_scale(1260, 250, 560, 34, 0, 100, 10, 5, vertical=False) card(d, 10, 340, 1860, 330, 'Reports', 'Report component: select CIENG_TANK1 or CIENG_HTML, generate and view') d.report(20, 390, 1840, 270) return d def build_banner(): d = Display('CIENG_AlarmBanner', 1920, 96, 'Alarm banner', bg='panel') # Desktop: frames inside a ignore their zoom attribute; the layout command with a zoom argument # applies it. Only ONE command can run at a time (more give "IllegalStateException: A command is # already being executed"), so only the Overview tab is fitted here; the toolbar / menu / F-keys # open the other tabs with fitDisplay. Web: the plain open action (web ignores ACTION commands). d.rect(0, 0, 1920, 96, fill='panel', actions=[ act_open('CIENG_Overview', 'CIENG_Operator', 'ovw', event='On entry'), act_cmd('ACTION navigate to ovw CIENG_Overview stretchDisplay', server=False, event='On entry')]) d.line(0, 0, 1920, 0, 'line', 1) d.rect(10, 7, 170, 82, fill='card', stroke='line', r=8) d.text(24, 13, 70, 18, 'ALARMS', 12, 'muted', bold=True) d.ellipse(96, 14, 16, 16, fill='off', actions=[conn('ellipse.fillPaint', LEVEL, transform=tr_threshold( [(0, 'red'), (10, 'amber'), (20, 'green'), (80, 'amber'), (90, 'red')]))]) d.text(118, 13, 60, 18, 'Tank 1', 12, 'text') d.button(24, 44, 142, 34, 'Alarm list', [act_open('CIENG_Alarms', 'CIENG_Operator', 'alm')], fg='white', bg='blue', size=13) # 4 rows of 20 px (14 px font, web) + border: no empty white area below the rows when 4+ alarms d.alarm_overview(190, 7, 1722, 82, size=14, banner=True) return d def build_faceplate(): d = Display('CIENG_TankFaceplate', 420, 600, 'Tank 1 faceplate', bg='card') d.rect(0, 0, 420, 600, fill='card') d.rect(0, 0, 420, 52, fill='card2') d.text(20, 14, 300, 24, 'TANK 1 | LEVEL', 15, 'text', bold=True) d.button(370, 10, 36, 32, 'X', [act_close()], fg='text', bg='card', tip='Close') d.text(20, 64, 380, 18, 'Tank 1 level (%)', 11, 'muted') d.parts[-1] = d.parts[-1].replace('', data([conn('text.value', LEVEL, 'ItemDesc')]) + '') d.number(20, 86, 200, 56, LEVEL, '999.9', 44, align='Left', extra=[level_color()]) d.text(224, 106, 30, 24, '%', 16, 'muted') d.text(270, 92, 130, 16, 'STATUS', 10, 'muted', bold=True, align='Right') d.text(250, 110, 150, 26, 'NORMAL', 16, 'text', bold=True, align='Right') d.parts[-1] = d.parts[-1].replace('', data([conn('text.value', LEVEL, 'ItemStatusMnemonic'), conn('text.fillPaint', LEVEL, transform=tr_threshold(LVL_COLORS))]) + '') d.databar(40, 160, 60, 280, LEVEL, 0, 100, 'blue', 'track', 'Up') d.rect_scale(104, 160, 44, 280, 0, 100, 20, 5) d.text(190, 160, 210, 16, 'ALARM LIMITS', 10, 'muted', bold=True) for i, (lab, attr, col) in enumerate((('High-High', 'ItemHHL', 'red'), ('High', 'ItemHL', 'amber'), ('Low', 'ItemLL', 'amber'), ('Low-Low', 'ItemLLL', 'red'))): y = 186 + i * 40 d.rect(190, y, 210, 34, fill='panel', r=6) d.rect(190, y, 6, 34, fill=col) d.text(206, y + 8, 100, 18, lab, 12, 'text') d.number(300, y + 4, 90, 26, LEVEL, '999.9', 16, col, attr=attr) d.text(190, 352, 210, 16, 'MAXIMUM SEEN', 10, 'muted', bold=True) d.number(190, 370, 140, 32, S + 'TANK1.LEVEL.MAX', '999.9', 22, align='Left') d.text(190, 412, 210, 16, 'LAST UPDATE', 10, 'muted', bold=True) d.time_text(190, 430, 210, 24, LEVEL, 11) d.micro_trend(20, 460, 380, 80, [(LEVEL, 0, 100, 'teal')]) d.button(20, 552, 120, 34, 'Trends', [act_open('CIENG_Trends', 'CIENG_Operator', 'trd')], fg='text', bg='card2') d.button(150, 552, 120, 34, 'Alarms', [act_open('CIENG_Alarms', 'CIENG_Operator', 'alm')], fg='text', bg='card2') d.button(280, 552, 120, 34, 'Close', [act_close()], fg='white', bg='blue') return d if __name__ == '__main__': out = sys.argv[1] if len(sys.argv) > 1 else os.path.join(os.path.dirname(os.path.abspath(__file__)), '..') for b in (build_header, build_overview, build_control, build_trends, build_alarms, build_system, build_banner, build_faceplate): disp = b() disp.save(out) print(disp.name, len(disp.parts), 'components')