devagent-ignition 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- devagent_hmi.py +471 -0
- devagent_ignition-0.1.0.dist-info/METADATA +204 -0
- devagent_ignition-0.1.0.dist-info/RECORD +133 -0
- devagent_ignition-0.1.0.dist-info/WHEEL +4 -0
- devagent_ignition-0.1.0.dist-info/entry_points.txt +2 -0
- devagent_spatial/__init__.py +1 -0
- devagent_spatial/_resources/README.md +187 -0
- devagent_spatial/_resources/RELEASE_MANIFEST.json +312 -0
- devagent_spatial/_resources/SHA256SUMS.txt +270 -0
- devagent_spatial/_resources/SOURCE_PROVENANCE.json +186 -0
- devagent_spatial/_resources/assets/agent.js +102 -0
- devagent_spatial/_resources/assets/creator.css +247 -0
- devagent_spatial/_resources/assets/creator_2d.mjs +16 -0
- devagent_spatial/_resources/assets/creator_original_r9.mjs +58 -0
- devagent_spatial/_resources/assets/creator_original_v8.mjs +120 -0
- devagent_spatial/_resources/assets/creator_r9_diagnostics.mjs +53 -0
- devagent_spatial/_resources/assets/creator_scene.mjs +128 -0
- devagent_spatial/_resources/assets/creator_ui.mjs +60 -0
- devagent_spatial/_resources/assets/customer_documents.js +110 -0
- devagent_spatial/_resources/assets/equipment_controls.mjs +27 -0
- devagent_spatial/_resources/assets/facility2d.css +24 -0
- devagent_spatial/_resources/assets/facility2d_app.mjs +45 -0
- devagent_spatial/_resources/assets/facility2d_core.mjs +114 -0
- devagent_spatial/_resources/assets/facility2d_simulation.mjs +167 -0
- devagent_spatial/_resources/assets/facility2d_viewer.html +7 -0
- devagent_spatial/_resources/assets/fallback_scene.js +17 -0
- devagent_spatial/_resources/assets/flow_geometry.mjs +218 -0
- devagent_spatial/_resources/assets/harness.js +78 -0
- devagent_spatial/_resources/assets/live_design_review.js +129 -0
- devagent_spatial/_resources/assets/operations_ui.js +305 -0
- devagent_spatial/_resources/assets/operator_viewer.html +64 -0
- devagent_spatial/_resources/assets/project_package.js +52 -0
- devagent_spatial/_resources/assets/r9_transport.mjs +70 -0
- devagent_spatial/_resources/assets/runtime_feed.js +30 -0
- devagent_spatial/_resources/assets/studio.html +139 -0
- devagent_spatial/_resources/assets/style.css +191 -0
- devagent_spatial/_resources/assets/test_workspace.html +195 -0
- devagent_spatial/_resources/assets/three_scene.js +442 -0
- devagent_spatial/_resources/assets/unified_engineer.js +101 -0
- devagent_spatial/_resources/assets/v076_fat_ui.js +40 -0
- devagent_spatial/_resources/assets/v078_command_ui.js +44 -0
- devagent_spatial/_resources/assets/viewer.html +45 -0
- devagent_spatial/_resources/assets/viewer.js +112 -0
- devagent_spatial/_resources/design-system.json +130 -0
- devagent_spatial/_resources/examples/CV101-command-contract-PENDING.json +30 -0
- devagent_spatial/_resources/examples/cv101_guided.json +272 -0
- devagent_spatial/_resources/examples/make_examples.py +36 -0
- devagent_spatial/_resources/examples/single_floor.json +444 -0
- devagent_spatial/_resources/examples/v0771_training_wizard.json +11 -0
- devagent_spatial/_resources/examples/warehouse-3floor-standard-2d.json +708 -0
- devagent_spatial/_resources/examples/warehouse-operations-synthetic.json +1186 -0
- devagent_spatial/_resources/examples/warehouse-v07-lab-bindings-PENDING.json +372 -0
- devagent_spatial/_resources/examples/warehouse.json +708 -0
- devagent_spatial/_resources/examples/water.json +288 -0
- devagent_spatial/_resources/reference/r9-golden/REFERENCE_SHA256.json +13 -0
- devagent_spatial/_resources/reference/r9-golden/web/app.mjs +745 -0
- devagent_spatial/_resources/reference/r9-golden/web/index.html +213 -0
- devagent_spatial/_resources/reference/r9-golden/web/lab_panel.mjs +53 -0
- devagent_spatial/_resources/reference/r9-golden/web/model.mjs +367 -0
- devagent_spatial/_resources/reference/r9-golden/web/state_adapter.mjs +78 -0
- devagent_spatial/_resources/reference/r9-golden/web/styles.css +76 -0
- devagent_spatial/_resources/reference/r9-original/README_R9.md +27 -0
- devagent_spatial/_resources/reference/r9-original/SHA256SUMS.txt +63 -0
- devagent_spatial/_resources/reference/r9-original/SNAPSHOT_SHA256SUMS.txt +16 -0
- devagent_spatial/_resources/reference/r9-original/flow_routes.json +527 -0
- devagent_spatial/_resources/reference/r9-original/flow_sim.py +117 -0
- devagent_spatial/_resources/reference/r9-original/lab_control.py +73 -0
- devagent_spatial/_resources/reference/r9-original/web/app.mjs +745 -0
- devagent_spatial/_resources/reference/r9-original/web/index.html +213 -0
- devagent_spatial/_resources/reference/r9-original/web/lab_panel.mjs +53 -0
- devagent_spatial/_resources/reference/r9-original/web/model.mjs +367 -0
- devagent_spatial/_resources/reference/r9-original/web/state_adapter.mjs +78 -0
- devagent_spatial/_resources/reference/r9-original/web/styles.css +76 -0
- devagent_spatial/_resources/reference/r9-original/web/vendor/LICENSE-three.txt +21 -0
- devagent_spatial/_resources/reference/r9-original/web/vendor/SHA256SUMS.txt +4 -0
- devagent_spatial/_resources/reference/r9-original/web/vendor/addons/controls/OrbitControls.js +1860 -0
- devagent_spatial/_resources/reference/r9-original/web/vendor/three.core.js +58773 -0
- devagent_spatial/_resources/reference/r9-original/web/vendor/three.module.js +18251 -0
- devagent_spatial/_resources/reference/v8-original/README.md +36 -0
- devagent_spatial/_resources/reference/v8-original/SHA256SUMS.txt +12 -0
- devagent_spatial/_resources/reference/v8-original/model.py +124 -0
- devagent_spatial/_resources/reference/v8-original/run_demo.sh +4 -0
- devagent_spatial/_resources/reference/v8-original/server.py +100 -0
- devagent_spatial/_resources/reference/v8-original/tests/browser_smoke.py +50 -0
- devagent_spatial/_resources/reference/v8-original/tests/test_v8.py +144 -0
- devagent_spatial/_resources/reference/v8-original/topology.json +24 -0
- devagent_spatial/_resources/reference/v8-original/web/app.js +114 -0
- devagent_spatial/_resources/reference/v8-original/web/index.html +22 -0
- devagent_spatial/_resources/reference/v8-original/web/simulator.html +1 -0
- devagent_spatial/_resources/reference/v8-original/web/simulator.js +6 -0
- devagent_spatial/_resources/reference/v8-original/web/style.css +16 -0
- devagent_spatial/_resources/sample_customer_inputs/Plant_Equipment_Register.csv +4 -0
- devagent_spatial/_resources/sample_customer_inputs/Plant_Flow_Requirements.txt +5 -0
- devagent_spatial/_resources/sample_customer_inputs/Raw_PLC_Tag_Export.csv +4 -0
- devagent_spatial/_resources/templates/warehouse_connections.csv +12 -0
- devagent_spatial/_resources/templates/warehouse_equipment.csv +21 -0
- devagent_spatial/_resources/templates/warehouse_signals.csv +34 -0
- devagent_spatial/_resources/templates/water_connections.csv +7 -0
- devagent_spatial/_resources/templates/water_equipment.csv +10 -0
- devagent_spatial/_resources/templates/water_signals.csv +12 -0
- devagent_spatial/agent_harness.py +299 -0
- devagent_spatial/agent_runtime.py +314 -0
- devagent_spatial/contract_opc_fat.py +216 -0
- devagent_spatial/contract_opc_source.py +265 -0
- devagent_spatial/control_intents.py +38 -0
- devagent_spatial/customer_intake.py +235 -0
- devagent_spatial/engineering_expertise.py +114 -0
- devagent_spatial/generator.py +201 -0
- devagent_spatial/ignition_native.py +234 -0
- devagent_spatial/ignition_project.py +144 -0
- devagent_spatial/live_lab_binding.py +128 -0
- devagent_spatial/model.py +297 -0
- devagent_spatial/native_reference.py +69 -0
- devagent_spatial/opc_snapshot.py +87 -0
- devagent_spatial/operations.py +284 -0
- devagent_spatial/operations_capture.py +47 -0
- devagent_spatial/operations_lab.py +64 -0
- devagent_spatial/original_r9.py +169 -0
- devagent_spatial/original_v8.py +81 -0
- devagent_spatial/preflight.py +140 -0
- devagent_spatial/preview_simulation.py +189 -0
- devagent_spatial/project_package.py +101 -0
- devagent_spatial/qualification.py +152 -0
- devagent_spatial/strict_json.py +29 -0
- devagent_spatial/studio.py +894 -0
- devagent_spatial/studio_workflow.py +277 -0
- devagent_spatial/test_reporting.py +87 -0
- devagent_spatial/v076_opc_fat.py +180 -0
- devagent_spatial/v078_commands.py +277 -0
- devagent_spatial/v078_native_commands.py +161 -0
- devagent_spatial/v07_fat.py +110 -0
- devagent_spatial/v07_native.py +318 -0
- devagent_spatial/v083_native.py +349 -0
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
"""Native operator candidate, versioned separately so R2 remains reproducible.
|
|
2
|
+
|
|
3
|
+
Reuses the V07 model/manifest/reference validators and deterministic ZIP envelope.
|
|
4
|
+
Only generated UI-local selection scripts are allowed. No tag/OPC writes or
|
|
5
|
+
Gateway deployment. Mapped telemetry requires explicit freshness policy.
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import hashlib
|
|
10
|
+
import json
|
|
11
|
+
import os
|
|
12
|
+
import tempfile
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
from .engineering_expertise import review
|
|
16
|
+
from .generator import ROOT
|
|
17
|
+
from .ignition_native import NativeHold, _flex, _label, sha
|
|
18
|
+
from .ignition_project import _zip_blob
|
|
19
|
+
from .strict_json import loads
|
|
20
|
+
from .v07_native import canonical, expected_members as base_members, model_sha, validate_manifest, _walk
|
|
21
|
+
|
|
22
|
+
SCHEMA = 'devagent.ignition.v083.operator_policy.v1'
|
|
23
|
+
CONFIRM = 'ISOLATED_OFFLINE_OPERATOR_CANDIDATE_ONLY'
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def policy_skeleton(model):
|
|
27
|
+
return {'schema': SCHEMA, 'model_sha256': model_sha(model), 'engineer': '',
|
|
28
|
+
'approval': 'PENDING_ENGINEER_REVIEW',
|
|
29
|
+
'signals': {s['id']: {'max_age_ms': None} for s in model['signals']},
|
|
30
|
+
'equipment': {e['id']: {'pe_in': None, 'pe_out': None} for e in model['equipment']}}
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def validate_policy(model, policy):
|
|
34
|
+
expected = policy_skeleton(model)
|
|
35
|
+
if not isinstance(policy, dict) or set(policy) != set(expected):
|
|
36
|
+
raise NativeHold('OPERATOR_POLICY_SCHEMA_INVALID')
|
|
37
|
+
if policy['schema'] != SCHEMA or policy['model_sha256'] != model_sha(model):
|
|
38
|
+
raise NativeHold('OPERATOR_POLICY_MODEL_MISMATCH')
|
|
39
|
+
if policy['approval'] not in ('PENDING_ENGINEER_REVIEW', 'APPROVED_FOR_ISOLATED_FAT_ONLY'):
|
|
40
|
+
raise NativeHold('OPERATOR_POLICY_APPROVAL_INVALID')
|
|
41
|
+
if not isinstance(policy['engineer'], str) or len(policy['engineer']) > 120:
|
|
42
|
+
raise NativeHold('OPERATOR_POLICY_ENGINEER_INVALID')
|
|
43
|
+
approved = policy['approval'] == 'APPROVED_FOR_ISOLATED_FAT_ONLY'
|
|
44
|
+
if approved and len(policy['engineer'].strip()) < 3:
|
|
45
|
+
raise NativeHold('OPERATOR_POLICY_ENGINEER_REQUIRED')
|
|
46
|
+
signals = {s['id']: s for s in model['signals']}
|
|
47
|
+
equipment = {e['id']: e for e in model['equipment']}
|
|
48
|
+
for key in ('signals', 'equipment'):
|
|
49
|
+
if not isinstance(policy[key], dict) or set(policy[key]) != set(expected[key]):
|
|
50
|
+
raise NativeHold('OPERATOR_POLICY_COVERAGE_MISMATCH')
|
|
51
|
+
for item in policy['signals'].values():
|
|
52
|
+
if not isinstance(item, dict) or set(item) != {'max_age_ms'}:
|
|
53
|
+
raise NativeHold('OPERATOR_FRESHNESS_SCHEMA_INVALID')
|
|
54
|
+
age = item['max_age_ms']
|
|
55
|
+
if age is not None and (not approved or type(age) is not int or not 100 <= age <= 86400000):
|
|
56
|
+
raise NativeHold('OPERATOR_FRESHNESS_REVIEW_REQUIRED')
|
|
57
|
+
associations = set()
|
|
58
|
+
for eid, item in policy['equipment'].items():
|
|
59
|
+
if not isinstance(item, dict) or set(item) != {'pe_in', 'pe_out'}:
|
|
60
|
+
raise NativeHold('OPERATOR_PE_SCHEMA_INVALID')
|
|
61
|
+
for sid in item.values():
|
|
62
|
+
if sid is None:
|
|
63
|
+
continue
|
|
64
|
+
if not isinstance(sid, str) or not approved or sid not in signals or signals[sid]['role'] != 'photoeye_blocked':
|
|
65
|
+
raise NativeHold('OPERATOR_PE_REVIEW_REQUIRED')
|
|
66
|
+
sensor = equipment.get(signals[sid]['asset_id'])
|
|
67
|
+
if sensor is None or sensor.get('floor', 1) != equipment[eid].get('floor', 1):
|
|
68
|
+
raise NativeHold('OPERATOR_PE_CROSS_FLOOR_RELATIONSHIP_UNVERIFIED')
|
|
69
|
+
if sid in associations:
|
|
70
|
+
raise NativeHold('OPERATOR_PE_ASSOCIATION_DUPLICATE')
|
|
71
|
+
associations.add(sid)
|
|
72
|
+
return policy
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _expr(node, target, expression):
|
|
76
|
+
node.setdefault('propConfig', {})[target] = {
|
|
77
|
+
'binding': {'type': 'expr', 'config': {'expression': expression}}}
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _button(name, text, palette, script=None):
|
|
81
|
+
node = {'type': 'ia.input.button', 'version': 0, 'meta': {'name': name},
|
|
82
|
+
'props': {'text': text, 'enabled': script is not None,
|
|
83
|
+
'style': {'backgroundColor': palette['deep'], 'color': '#ecf5ff',
|
|
84
|
+
'border': '1px solid '+palette['border'], 'borderRadius': '6px',
|
|
85
|
+
'fontSize': '14px', 'minHeight': '44px', 'padding': '10px'}},
|
|
86
|
+
'position': {'basis': 'auto', 'grow': 0, 'shrink': 0}}
|
|
87
|
+
if script is not None:
|
|
88
|
+
node['events'] = {'component': {'onActionPerformed': {
|
|
89
|
+
'type': 'script', 'scope': 'G', 'config': {'script': script}}}}
|
|
90
|
+
return node
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _floor_script(floor):
|
|
94
|
+
return '\tself.view.custom.selectedFloor = '+str(floor)+'\n\tself.view.custom.selectedAsset = ""\n'
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _asset_script(asset):
|
|
98
|
+
return '\tself.view.custom.selectedAsset = '+json.dumps(asset)+'\n'
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def signal_expressions(signal, mapped, max_age_ms):
|
|
102
|
+
"""Generate fixed expressions; bad/old/missing data never becomes a state.
|
|
103
|
+
|
|
104
|
+
Timestamp-only changes and source-time propagation must be checked in FAT.
|
|
105
|
+
No freshness interval is guessed by this compiler.
|
|
106
|
+
"""
|
|
107
|
+
if not mapped:
|
|
108
|
+
return {'value': '"NOT MAPPED"', 'quality': '"NOT MAPPED"', 'timestamp': '"UNKNOWN"'}
|
|
109
|
+
raw = '{view.custom.raw.'+signal['id']+'}'
|
|
110
|
+
quality = 'toStr(qualityOf('+raw+'))'
|
|
111
|
+
timestamp = 'toStr(timestampOf('+raw+'))'
|
|
112
|
+
if max_age_ms is None:
|
|
113
|
+
return {'value': '"UNKNOWN - STALE LIMIT NOT CONFIGURED"',
|
|
114
|
+
'quality': quality, 'timestamp': timestamp}
|
|
115
|
+
age = '(toMillis(now(1000)) - toMillis(timestampOf('+raw+')))'
|
|
116
|
+
fresh = '('+age+' >= 0 && '+age+' <= '+str(max_age_ms)+')'
|
|
117
|
+
value = 'toStr('+raw+')'
|
|
118
|
+
role = signal['role']
|
|
119
|
+
if role in ('run_feedback', 'fault', 'photoeye_blocked'):
|
|
120
|
+
active = raw if signal['polarity'] == 'active_high' else '!'+raw
|
|
121
|
+
active_text, inactive_text = {
|
|
122
|
+
'run_feedback': ('RUNNING', 'STOPPED'),
|
|
123
|
+
'fault': ('FAULT ACTIVE', 'FAULT INACTIVE'),
|
|
124
|
+
'photoeye_blocked': ('BLOCKED', 'CLEAR')}[role]
|
|
125
|
+
value = 'if('+active+', '+json.dumps(active_text)+', '+json.dumps(inactive_text)+')'
|
|
126
|
+
elif role == 'run_command':
|
|
127
|
+
value = 'if('+raw+', "COMMAND ON", "COMMAND OFF")'
|
|
128
|
+
return {
|
|
129
|
+
'value': 'if(isNull('+raw+'), "UNKNOWN", if(!isGood('+raw+'), "UNKNOWN", if(!'+fresh+', "UNKNOWN - STALE", '+value+')))',
|
|
130
|
+
'quality': 'if(!isGood('+raw+'), '+quality+', if(!'+fresh+', "STALE", '+quality+'))',
|
|
131
|
+
'timestamp': timestamp}
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def operator_view(model, manifest, policy, *, _skip_audit=False):
|
|
135
|
+
approved = validate_manifest(model, manifest)
|
|
136
|
+
validate_policy(model, policy)
|
|
137
|
+
if review(model)['role_asset_mismatches']:
|
|
138
|
+
raise NativeHold('ENGINEERING_ROLE_ASSET_KIND_MISMATCH')
|
|
139
|
+
design = loads((ROOT/'design-system.json').read_text())
|
|
140
|
+
palette = design['palettes'][model.get('palette', 'r9_cyan')]
|
|
141
|
+
by_id = {s['id']: s for s in model['signals']}
|
|
142
|
+
by_asset = {e['id']: {} for e in model['equipment']}
|
|
143
|
+
for signal in model['signals']:
|
|
144
|
+
by_asset[signal['asset_id']][signal['role']] = signal
|
|
145
|
+
floors = sorted({e.get('floor', 1) for e in model['equipment']})
|
|
146
|
+
view = {'custom': {'selectedFloor': 0, 'selectedAsset': '',
|
|
147
|
+
'raw': {sid: None for sid in approved}},
|
|
148
|
+
'params': {}, 'propConfig': {},
|
|
149
|
+
'props': {'defaultSize': {'width': 1600, 'height': 1000}}}
|
|
150
|
+
for sid, path in approved.items():
|
|
151
|
+
view['propConfig']['custom.raw.'+sid] = {
|
|
152
|
+
'binding': {'type': 'tag', 'config': {'mode': 'direct', 'tagPath': path}}}
|
|
153
|
+
|
|
154
|
+
def label(text, name, size=14, color='#ecf5ff'):
|
|
155
|
+
return _label(text, size, False, color, name=name)
|
|
156
|
+
|
|
157
|
+
def diagnostic(title, signal, name):
|
|
158
|
+
value = label('NOT MAPPED', name+'Value', color='#e6bd70')
|
|
159
|
+
quality = label('NOT MAPPED', name+'Quality', 11, palette['muted'])
|
|
160
|
+
timestamp = label('UNKNOWN', name+'Timestamp', 10, palette['muted'])
|
|
161
|
+
if signal:
|
|
162
|
+
sid = signal['id']
|
|
163
|
+
exprs = signal_expressions(signal, sid in approved, policy['signals'][sid]['max_age_ms'])
|
|
164
|
+
for node, key in ((value, 'value'), (quality, 'quality'), (timestamp, 'timestamp')):
|
|
165
|
+
_expr(node, 'props.text', exprs[key])
|
|
166
|
+
title += ' | '+sid+(' ['+signal['unit']+']' if signal.get('unit') else '')
|
|
167
|
+
return _flex([label(title, name+'Title', 12), value, quality, timestamp], name=name,
|
|
168
|
+
style={'borderBottom': '1px solid '+palette['border'], 'padding': '8px', 'gap': '3px'})
|
|
169
|
+
|
|
170
|
+
nav = [_button('FloorAll', 'WAREHOUSE OVERVIEW', palette, _floor_script(0))]
|
|
171
|
+
nav += [_button('Floor'+str(f), 'FLOOR '+str(f), palette, _floor_script(f)) for f in floors]
|
|
172
|
+
groups = []
|
|
173
|
+
for floor in floors:
|
|
174
|
+
cards = []
|
|
175
|
+
for equipment in model['equipment']:
|
|
176
|
+
if equipment.get('floor', 1) != floor:
|
|
177
|
+
continue
|
|
178
|
+
eid = equipment['id']
|
|
179
|
+
select = _button('Select_'+eid, eid+' | '+equipment['name'], palette, _asset_script(eid))
|
|
180
|
+
_expr(select, 'props.style.borderColor', 'if({view.custom.selectedAsset} = '+json.dumps(eid)+', '+json.dumps(palette['accent'])+', '+json.dumps(palette['border'])+')')
|
|
181
|
+
rows = [select, label(equipment['kind'].upper(), 'Kind_'+eid, 11, palette['muted'])]
|
|
182
|
+
for role in ('run_feedback', 'fault'):
|
|
183
|
+
signal = by_asset[eid].get(role)
|
|
184
|
+
if signal:
|
|
185
|
+
status = label('NOT MAPPED', role+'_'+eid)
|
|
186
|
+
_expr(status, 'props.text', signal_expressions(signal, signal['id'] in approved,
|
|
187
|
+
policy['signals'][signal['id']]['max_age_ms'])['value'])
|
|
188
|
+
rows.append(status)
|
|
189
|
+
destinations = [c['to'] for c in model.get('connections', []) if c['from'] == eid]
|
|
190
|
+
if destinations:
|
|
191
|
+
rows.append(label('TO: '+', '.join(destinations), 'Flow_'+eid, 11, palette['muted']))
|
|
192
|
+
card = _flex(rows, name='Equipment_'+eid, style={'backgroundColor': palette['panel'],
|
|
193
|
+
'padding': '10px', 'gap': '6px', 'borderRadius': '8px'})
|
|
194
|
+
card['position'] = {'basis': '260px', 'grow': 1, 'shrink': 0}
|
|
195
|
+
cards.append(card)
|
|
196
|
+
group = _flex([label('FLOOR '+str(floor), 'FloorTitle'+str(floor), 20),
|
|
197
|
+
_flex(cards, name='EquipmentGrid'+str(floor), direction='row', style={'gap': '12px'})],
|
|
198
|
+
name='FloorGroup'+str(floor), style={'padding': '12px', 'gap': '12px'})
|
|
199
|
+
_expr(group, 'meta.visible', '{view.custom.selectedFloor} = 0 || {view.custom.selectedFloor} = '+str(floor))
|
|
200
|
+
groups.append(group)
|
|
201
|
+
overview = _flex(groups, name='WarehouseOverview', style={'overflow': 'auto', 'minWidth': '400px'}, grow=1)
|
|
202
|
+
overview['position']['basis'] = '0px'
|
|
203
|
+
|
|
204
|
+
empty = label('SELECT EQUIPMENT', 'InspectorEmpty', 20)
|
|
205
|
+
_expr(empty, 'meta.visible', '{view.custom.selectedAsset} = ""')
|
|
206
|
+
inspectors = [empty]
|
|
207
|
+
for equipment in model['equipment']:
|
|
208
|
+
eid = equipment['id']
|
|
209
|
+
roles = by_asset[eid]
|
|
210
|
+
rows = [label(eid+' | '+equipment['name'], 'InspectorTitle_'+eid, 20),
|
|
211
|
+
label('FLOOR '+str(equipment.get('floor', 1))+' | '+equipment.get('zone', ''), 'Location_'+eid, 12)]
|
|
212
|
+
for role, title in (('run_command', 'MOTOR COMMAND (READBACK)'), ('run_feedback', 'RUN FEEDBACK'),
|
|
213
|
+
('motor_speed', 'MOTOR SPEED'), ('vfd_frequency', 'VFD FREQUENCY'),
|
|
214
|
+
('fault', 'FAULT'), ('fault_code', 'FAULT CODE')):
|
|
215
|
+
rows.append(diagnostic(title, roles.get(role), 'Diag_'+eid+'_'+role))
|
|
216
|
+
consumed = {'run_command', 'run_feedback', 'motor_speed', 'vfd_frequency', 'fault', 'fault_code'}
|
|
217
|
+
for direction in ('pe_in', 'pe_out'):
|
|
218
|
+
sid = policy['equipment'][eid][direction]
|
|
219
|
+
rows.append(diagnostic(direction.upper().replace('_', ' '), by_id.get(sid), 'Diag_'+eid+'_'+direction))
|
|
220
|
+
for role, signal in sorted(roles.items()):
|
|
221
|
+
if role not in consumed:
|
|
222
|
+
rows.append(diagnostic('DECLARED '+role.upper().replace('_', ' '), signal, 'Diag_'+eid+'_'+role))
|
|
223
|
+
rows += [label('ALARM: NOT MAPPED | lifecycle not commissioned', 'Alarm_'+eid, 12, '#e6bd70'),
|
|
224
|
+
label('QUALITY / SOURCE TIME shown per signal', 'QualityNotice_'+eid, 11, palette['muted']),
|
|
225
|
+
_flex([_button('Command_'+eid+'_'+action, action, palette) for action in ('START', 'STOP', 'RESET')],
|
|
226
|
+
name='MotorCommands_'+eid, direction='row', style={'gap': '8px'}),
|
|
227
|
+
label('CONTROL LOCKED | no command authority', 'ControlLock_'+eid, 12, '#e6bd70')]
|
|
228
|
+
panel = _flex(rows, name='Inspector_'+eid, style={'gap': '5px'})
|
|
229
|
+
_expr(panel, 'meta.visible', '{view.custom.selectedAsset} = '+json.dumps(eid))
|
|
230
|
+
inspectors.append(panel)
|
|
231
|
+
inspector = _flex(inspectors, name='EquipmentInspector', style={
|
|
232
|
+
'backgroundColor': palette['panel'], 'padding': '16px', 'overflow': 'auto', 'minWidth': '340px'})
|
|
233
|
+
inspector['position'] = {'basis': '400px', 'grow': 0, 'shrink': 0}
|
|
234
|
+
body = _flex([overview, inspector], name='OperationsBody', direction='row', grow=1,
|
|
235
|
+
style={'gap': '14px', 'minHeight': '600px', 'alignItems': 'stretch'})
|
|
236
|
+
body['props']['wrap'] = 'nowrap'
|
|
237
|
+
root = _flex([label(model['name'], 'FacilityTitle', 26),
|
|
238
|
+
label('ISOLATED LAB | BINDINGS NOT PHYSICALLY VERIFIED | PRODUCTION HOLD', 'Qualification', 12, '#e6bd70'),
|
|
239
|
+
_flex(nav, name='FloorNavigation', direction='row', style={'gap': '10px'}), body],
|
|
240
|
+
name='DevAgentOperatorRoot', style={'backgroundColor': palette['background'],
|
|
241
|
+
'color': '#ecf5ff', 'padding': '16px', 'gap': '14px', 'overflow': 'auto'})
|
|
242
|
+
root.pop('position', None)
|
|
243
|
+
view['root'] = root
|
|
244
|
+
if not _skip_audit:
|
|
245
|
+
audit_view(model, manifest, view, policy=policy)
|
|
246
|
+
return view
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def audit_view(model, manifest, view, *, policy=None):
|
|
250
|
+
approved = validate_manifest(model, manifest)
|
|
251
|
+
floor_scripts = {'FloorAll': _floor_script(0)}
|
|
252
|
+
floor_scripts.update({'Floor'+str(f): _floor_script(f)
|
|
253
|
+
for f in {e.get('floor', 1) for e in model['equipment']}})
|
|
254
|
+
asset_scripts = {'Select_'+e['id']: _asset_script(e['id']) for e in model['equipment']}
|
|
255
|
+
allowed_script_by_name = {**floor_scripts, **asset_scripts}
|
|
256
|
+
nodes = list(_walk(view['root']))
|
|
257
|
+
if len(nodes) > 12000:
|
|
258
|
+
raise NativeHold('OPERATOR_COMPONENT_LIMIT')
|
|
259
|
+
actual_tags = []
|
|
260
|
+
for node in [view, *nodes]:
|
|
261
|
+
if node is not view and node.get('type') not in ('ia.container.flex', 'ia.display.label', 'ia.input.button'):
|
|
262
|
+
raise NativeHold('OPERATOR_COMPONENT_NOT_ALLOWED')
|
|
263
|
+
if any(k in node for k in ('scripts', 'customMethods', 'onChange')):
|
|
264
|
+
raise NativeHold('OPERATOR_ACTIVE_FEATURE_REJECTED')
|
|
265
|
+
if 'events' in node:
|
|
266
|
+
event = node['events']
|
|
267
|
+
action = event.get('component', {}).get('onActionPerformed', {})
|
|
268
|
+
script = action.get('config', {}).get('script', '')
|
|
269
|
+
expected = {'component': {'onActionPerformed': {'type': 'script', 'scope': 'G', 'config': {'script': script}}}}
|
|
270
|
+
if (event != expected or node.get('type') != 'ia.input.button' or
|
|
271
|
+
script != allowed_script_by_name.get(node.get('meta', {}).get('name'))):
|
|
272
|
+
raise NativeHold('OPERATOR_EVENT_NOT_UI_LOCAL')
|
|
273
|
+
if node.get('meta', {}).get('name', '').startswith('Command_'):
|
|
274
|
+
if node['props']['enabled'] is not False or 'events' in node or 'propConfig' in node:
|
|
275
|
+
raise NativeHold('OPERATOR_COMMAND_AUTHORITY_REJECTED')
|
|
276
|
+
for target, cfg in node.get('propConfig', {}).items():
|
|
277
|
+
if set(cfg) != {'binding'}:
|
|
278
|
+
raise NativeHold('OPERATOR_BINDING_FEATURE_REJECTED')
|
|
279
|
+
binding = cfg['binding']
|
|
280
|
+
if set(binding) != {'type', 'config'}:
|
|
281
|
+
raise NativeHold('OPERATOR_BINDING_FEATURE_REJECTED')
|
|
282
|
+
if binding['type'] == 'tag':
|
|
283
|
+
sid = target.removeprefix('custom.raw.')
|
|
284
|
+
if node is not view or sid not in approved or binding['config'] != {'mode': 'direct', 'tagPath': approved[sid]}:
|
|
285
|
+
raise NativeHold('OPERATOR_TAG_NOT_APPROVED')
|
|
286
|
+
actual_tags.append(sid)
|
|
287
|
+
elif binding['type'] == 'expr':
|
|
288
|
+
if set(binding['config']) != {'expression'}:
|
|
289
|
+
raise NativeHold('OPERATOR_EXPRESSION_CONFIG_INVALID')
|
|
290
|
+
expression = binding['config']['expression']
|
|
291
|
+
# All telemetry reads must flow through the approved raw bindings.
|
|
292
|
+
if any(token in expression.lower() for token in ('runscript', 'tag(', '{[', 'system.', 'http', 'forcequality', 'qualifiedvalue')):
|
|
293
|
+
raise NativeHold('OPERATOR_EXPRESSION_ACCESS_REJECTED')
|
|
294
|
+
else:
|
|
295
|
+
raise NativeHold('OPERATOR_BINDING_TYPE_REJECTED')
|
|
296
|
+
if sorted(actual_tags) != sorted(approved):
|
|
297
|
+
raise NativeHold('OPERATOR_BINDING_COVERAGE_INVALID')
|
|
298
|
+
if policy is None:
|
|
299
|
+
policy = policy_skeleton(model)
|
|
300
|
+
expected_view = operator_view(model, manifest, policy, _skip_audit=True)
|
|
301
|
+
if canonical(view) != canonical(expected_view):
|
|
302
|
+
raise NativeHold('OPERATOR_VIEW_SEMANTIC_DRIFT')
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
def expected_members(model, manifest, policy, reference, template_view, reference_sha, name):
|
|
306
|
+
members, meta = base_members(model, manifest, reference, template_view, reference_sha, name)
|
|
307
|
+
view_path = next(path for path in members if path.endswith('/view.json'))
|
|
308
|
+
members[view_path] = canonical(operator_view(model, manifest, policy))
|
|
309
|
+
return members, meta
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
def candidate(model, manifest, policy, reference, template_view, reference_sha, name, target, *, verify=False, confirm=''):
|
|
313
|
+
if not verify and confirm != CONFIRM:
|
|
314
|
+
raise NativeHold('OPERATOR_COMPILER_CONFIRMATION_REQUIRED')
|
|
315
|
+
target = Path(target).absolute()
|
|
316
|
+
if target.is_symlink() or target.suffix != '.zip' or not target.parent.is_dir():
|
|
317
|
+
raise NativeHold('OPERATOR_TARGET_INVALID')
|
|
318
|
+
members, meta = expected_members(model, manifest, policy, reference, template_view, reference_sha, name)
|
|
319
|
+
payload = _zip_blob(members)
|
|
320
|
+
if len(payload) > 4_000_000:
|
|
321
|
+
raise NativeHold('OPERATOR_CANDIDATE_TOO_LARGE')
|
|
322
|
+
if verify:
|
|
323
|
+
if not target.is_file() or target.read_bytes() != payload:
|
|
324
|
+
raise NativeHold('OPERATOR_REPRODUCIBILITY_MISMATCH')
|
|
325
|
+
else:
|
|
326
|
+
if target.exists():
|
|
327
|
+
raise NativeHold('OPERATOR_CREATE_ONLY_TARGET_EXISTS')
|
|
328
|
+
fd, temp = tempfile.mkstemp(prefix='.v083-native-', dir=target.parent)
|
|
329
|
+
try:
|
|
330
|
+
with os.fdopen(fd, 'wb') as stream:
|
|
331
|
+
stream.write(payload)
|
|
332
|
+
stream.flush()
|
|
333
|
+
os.fsync(stream.fileno())
|
|
334
|
+
os.chmod(temp, 0o600)
|
|
335
|
+
try:
|
|
336
|
+
os.link(temp, target)
|
|
337
|
+
except FileExistsError:
|
|
338
|
+
raise NativeHold('OPERATOR_CREATE_ONLY_TARGET_EXISTS') from None
|
|
339
|
+
finally:
|
|
340
|
+
Path(temp).unlink(missing_ok=True)
|
|
341
|
+
return {'schema': 'devagent.ignition.v083.operator_candidate.v1',
|
|
342
|
+
'status': 'PASS_OFFLINE_OPERATOR_REPRODUCIBILITY_ONLY' if verify else 'PASS_OFFLINE_OPERATOR_CANDIDATE_ONLY',
|
|
343
|
+
'project_zip': str(target), 'project_sha256': sha(target), 'reference_sha256': reference_sha,
|
|
344
|
+
'model_sha256': model_sha(model), 'manifest_sha256': hashlib.sha256(canonical(manifest)).hexdigest(),
|
|
345
|
+
'policy_sha256': hashlib.sha256(canonical(policy)).hexdigest(), **meta,
|
|
346
|
+
'OFFLINE_COMPILER': 'PASS', 'AUTHENTICATED_GATEWAY_IMPORT': 'HOLD_NOT_EXECUTED',
|
|
347
|
+
'NATIVE_PERSPECTIVE_RENDER': 'HOLD_NOT_EXECUTED', 'NATIVE_EQUIPMENT_INTERACTION': 'HOLD_NOT_EXECUTED',
|
|
348
|
+
'GATEWAY_OPC_FAT': 'HOLD_NOT_EXECUTED', 'REAL_OPC_SOURCE_VALIDATION': 'HOLD_NOT_VERIFIED',
|
|
349
|
+
'PRODUCTION_APPROVAL': 'HOLD', 'command_authority': 'NONE', 'r9_native_parity': 'NOT_VERIFIED'}
|