Phase 0: wipe PrimeBAT template, SimHarness (sim+probe), lint + schema harvest tools

Gate G0 green: scan clean, lint 0 failures, clients 200, probe P0 facts captured,
simulator journaling. Probe locked: uuid row grouping, compound AlarmState strings,
java-exception bare-except gotcha, millis dates OK, ackUserName field.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-16 12:07:23 -05:00
parent 310b1b3b9e
commit af662f6fd2
78 changed files with 911 additions and 1957 deletions

View File

@@ -0,0 +1,9 @@
{
"pages": {
"/scratch": {
"title": "Scratch",
"viewPath": "Scratch/Current"
}
},
"sharedDocks": {}
}

View File

@@ -0,0 +1,10 @@
{
"scope": "G",
"version": 1,
"restricted": false,
"overridable": true,
"files": [
"config.json"
],
"attributes": {}
}

View File

@@ -0,0 +1,10 @@
{
"scope": "G",
"version": 1,
"restricted": false,
"overridable": true,
"files": [
"view.json"
],
"attributes": {}
}

View File

@@ -0,0 +1,27 @@
{
"custom": {},
"params": {},
"propConfig": {},
"props": {},
"root": {
"children": [
{
"meta": {
"name": "lblScratch"
},
"props": {
"text": "SimHarness scratch view - overwrite me to isolate a component under test."
},
"type": "ia.display.label"
}
],
"custom": {},
"meta": {
"name": "root"
},
"props": {
"direction": "column"
},
"type": "ia.container.flex"
}
}

View File

@@ -0,0 +1,179 @@
# alarm_simulator.py — Build-a-Thon alarm activity generator (Jython, Ignition 8.3)
#
# Drives the Boolean memory tags in [default]BuildathonSim/ (import
# simulation_tags.json first). Designed to be called every 5 seconds:
#
# RECOMMENDED — Gateway timer script (survives Designer close):
# 1. Designer > Project Library: create a script named "alarmsim", paste this file.
# 2. Project > Gateway Events > Timer: new script, Delay 5000 ms,
# Fixed Delay, Dedicated thread. Body: alarmsim.tick()
# 3. Save the project. Alarm activity starts immediately and runs forever.
#
# ALTERNATIVE — Designer Script Console (blocks the console while running):
# Paste this whole file, then add at the bottom:
# run_console(minutes=30)
#
# Patterns produced (all timestamps land in the alarm journal):
# - Baseline: random alarms across 5 areas / 3 priorities, active 30 s - 5 min,
# averaging a few activations per minute
# - Chattering: *_Chatter tags re-trigger every 30-60 s (short 5-15 s actives)
# - Standing: *_Standing tags go active on the first tick and never clear
# - Fleeting: *_Fleeting tags are active < 10 s
# - Flood: roughly every 30-60 min (or on demand via force_flood()), one area
# bursts 20-40 activations over 10 minutes
#
# Manual helpers (Script Console, gateway scope via system.util.sendRequest not
# needed — just call from a console if running console mode, or temporarily from
# the timer script):
# alarmsim.force_flood() -> start a flood burst now
# alarmsim.reset() -> clear all sim tags and internal state
import random
import time
BASE = "[default]BuildathonSim"
BASELINE = {
"Intake": ["Pump1_Fault", "Pump2_Fault", "ScreenDiffPressHigh", "InletFlowLow",
"ValveV101_FailToOpen", "WetWellLevelHigh", "SampleTempHigh", "PowerMonitorAlarm"],
"BoilerHouse": ["FeedPumpA_Fault", "FeedPumpB_Fault", "SteamPressHigh", "DrumLevelLow",
"StackTempHigh", "FuelGasPressLow", "EconomizerDPHigh", "BlowdownConductivityHigh"],
"Packaging": ["LabelerFault", "CapperTorqueLow", "FillerLevelDeviation", "ConveyorOverload",
"CaseSealerJam", "PrinterInkLow", "RejectRateHigh", "GuardDoorOpen"],
"Utilities": ["AirCompA_Fault", "AirHeaderPressLow", "ChillerTripped", "CoolingTowerVibration",
"GlycolTempHigh", "N2PressLow", "UPSOnBattery", "WaterSoftenerFault"],
"TankFarm": ["T101_LevelHigh", "T102_LevelHigh", "T103_TempHigh", "TransferPumpFault",
"ContainmentSumpHigh", "VaporRecoveryFault", "T104_PressHigh", "ManifoldLeakDetect"],
}
CHATTER = [
"Intake/ConveyorJam_Chatter",
"Packaging/ShrinkTunnelTemp_Chatter",
"Utilities/AirDryerDewpoint_Chatter",
]
STANDING = [
"BoilerHouse/DrumLevelXmtrFail_Standing",
"TankFarm/T105_LevelXmtrFail_Standing",
]
FLEETING = [
"Intake/CommsGlitch_Fleeting",
"BoilerHouse/FlameScannerFlicker_Fleeting",
"Packaging/EStopPulse_Fleeting",
"TankFarm/PumpSealFlushLow_Fleeting",
]
# Tuning (per 5 s tick)
BASELINE_PROB = 0.30 # ~3-4 baseline activations/min across the plant
FLEETING_PROB = 0.08 # ~1 fleeting alarm/min
FLOOD_START_PROB = 0.0025 # expected flood every ~30-60 min
FLOOD_DURATION = 600 # 10 minutes
FLOOD_EVENT_PROB = 0.35 # per tick during flood -> ~25 activations / 10 min
def _path(rel):
return "%s/%s" % (BASE, rel)
def _state():
g = system.util.getGlobals()
if "buildathon_alarmsim" not in g:
g["buildathon_alarmsim"] = {
"clears": {}, # tag path -> epoch seconds to write False
"chatter_next": {}, # chatter path -> epoch seconds of next re-trigger
"flood": None, # {"area": name, "until": epoch} while flooding
"standing_set": False,
}
return g["buildathon_alarmsim"]
def _write(pairs):
if pairs:
system.tag.writeBlocking([p for p, v in pairs], [v for p, v in pairs])
def tick():
st = _state()
now = time.time()
writes = []
# 1. Standing alarms: activate once, never clear
if not st["standing_set"]:
writes += [(_path(p), True) for p in STANDING]
st["standing_set"] = True
# 2. Process scheduled clears
for path in list(st["clears"].keys()):
if now >= st["clears"][path]:
writes.append((path, False))
del st["clears"][path]
# 3. Baseline activity: random tag, active 30 s - 5 min
if random.random() < BASELINE_PROB:
area = random.choice(list(BASELINE.keys()))
path = _path("%s/%s" % (area, random.choice(BASELINE[area])))
if path not in st["clears"]:
writes.append((path, True))
st["clears"][path] = now + random.uniform(30, 300)
# 4. Chattering: re-trigger every 30-60 s, active 5-15 s each time
for rel in CHATTER:
path = _path(rel)
nxt = st["chatter_next"].get(path, 0)
if now >= nxt and path not in st["clears"]:
writes.append((path, True))
st["clears"][path] = now + random.uniform(5, 15)
st["chatter_next"][path] = now + random.uniform(30, 60)
# 5. Fleeting: active 2-8 s
if random.random() < FLEETING_PROB:
path = _path(random.choice(FLEETING))
if path not in st["clears"]:
writes.append((path, True))
st["clears"][path] = now + random.uniform(2, 8)
# 6. Flood burst: one area, 15+ activations inside 10 minutes
if st["flood"] is None:
if random.random() < FLOOD_START_PROB:
_start_flood(st, now)
elif now >= st["flood"]["until"]:
st["flood"] = None
else:
if random.random() < FLOOD_EVENT_PROB:
area = st["flood"]["area"]
path = _path("%s/%s" % (area, random.choice(BASELINE[area])))
# During a flood, alarms clear fast and may re-trigger, inflating the
# event rate the way a real upset does. Overwrite any pending clear.
writes.append((path, True))
st["clears"][path] = now + random.uniform(10, 45)
_write(writes)
def _start_flood(st, now):
area = random.choice(list(BASELINE.keys()))
st["flood"] = {"area": area, "until": now + FLOOD_DURATION}
system.util.getLogger("alarmsim").info("Flood burst started in area: %s" % area)
def force_flood():
"""Start a flood burst immediately (handy for demo/testing)."""
_start_flood(_state(), time.time())
def reset():
"""Clear all simulation tags and internal state."""
g = system.util.getGlobals()
g.pop("buildathon_alarmsim", None)
paths = []
for area, names in BASELINE.items():
paths += [_path("%s/%s" % (area, n)) for n in names]
paths += [_path(p) for p in CHATTER + STANDING + FLEETING]
system.tag.writeBlocking(paths, [False] * len(paths))
def run_console(minutes=30):
"""Script Console runner: calls tick() every 5 s for `minutes`. Blocks the console."""
end = time.time() + minutes * 60
while time.time() < end:
tick()
time.sleep(5)

View File

@@ -0,0 +1,12 @@
{
"scope": "A",
"version": 1,
"restricted": false,
"overridable": true,
"files": [
"code.py"
],
"attributes": {
"hintScope": 2
}
}

View File

@@ -0,0 +1,179 @@
# probe - gateway-side API ground truth (SimHarness, dev-only, Jython 2.7).
#
# Runs inside the gateway via the ProbeTick timer (enable it, scan, wait <=20s,
# read the output, disable it). Writes JSON to the bind-mounted repo path:
# container: /usr/local/bin/ignition/data/projects/.probe/out.json
# host repo: ignition/gateway/projects/.probe/out.json
#
# Edit PROBE_NAME / add sections as needed; every section is try-wrapped so a
# single API surprise never kills the whole probe.
import json
OUT_DIR = "/usr/local/bin/ignition/data/projects/.probe"
PROBE_NAME = "P0"
def _safe(fn):
# Bare except is deliberate: in Jython, JAVA exceptions (e.g. bad kwargs
# into system.* calls) do NOT subclass Python's Exception and would fly
# straight past 'except Exception'.
try:
return {"ok": True, "value": fn()}
except:
import sys
et, ev = sys.exc_info()[0], sys.exc_info()[1]
return {"ok": False, "error": "%s: %s" % (getattr(et, "__name__", et), ev)}
def _write(result):
from java.io import File
File(OUT_DIR).mkdirs()
f = open(OUT_DIR + "/out.json", "w")
try:
f.write(json.dumps(result, indent=1, default=lambda o: repr(o)))
finally:
f.close()
def _prop(evt, names):
out = {}
for n in names:
try:
v = evt.get(n)
out[n] = {"cls": v is not None and v.__class__.__name__ or None,
"repr": repr(v)[:200]}
except Exception, e:
out[n] = {"error": "%s" % e}
return out
def _entry_facts(evt):
d = {}
d["cls"] = evt.__class__.__name__
try:
d["id"] = str(evt.getId())
except Exception, e:
d["id_error"] = "%s" % e
try:
s = evt.getState()
d["state_str"] = str(s)
d["state_cls"] = s is not None and s.__class__.__name__ or None
except Exception, e:
d["state_error"] = "%s" % e
try:
d["source"] = str(evt.getSource())
except Exception, e:
d["source_error"] = "%s" % e
try:
d["displayPath"] = repr(evt.getDisplayPath())
except Exception, e:
d["displayPath_error"] = "%s" % e
try:
p = evt.getPriority()
d["priority_cls"] = p is not None and p.__class__.__name__ or None
d["priority_str"] = str(p)
try:
d["priority_int"] = int(p.ordinal())
except Exception:
try:
d["priority_int"] = int(p)
except Exception:
d["priority_int"] = None
except Exception, e:
d["priority_error"] = "%s" % e
d["props"] = _prop(evt, ["eventTime", "eventType", "eventtype", "ackUser",
"ackUserName", "isSystemEvent", "systemEvent",
"eventValue", "name", "label"])
for getter in ("getActiveData", "getClearedData", "getAckData"):
try:
v = getattr(evt, getter)()
d[getter] = v is not None and repr(v)[:200] or None
except Exception, e:
d[getter] = "ERR %s" % e
try:
from java.util import Date
t = evt.get("eventTime")
if t is not None and isinstance(t, Date):
d["eventTime_ms"] = t.getTime()
except Exception:
pass
return d
def _p0():
result = {}
end = system.date.now()
start = system.date.addHours(end, -8)
def q_basic():
r = system.alarm.queryJournal(startDate=start, endDate=end)
entries = [e for e in r]
by_id = {}
for e in entries:
try:
k = str(e.getId())
except Exception:
k = "?"
by_id.setdefault(k, []).append(e)
multi = {}
n = 0
for k, evts in by_id.items():
if len(evts) > 1 and n < 5:
states = []
for e in evts:
try:
states.append(str(e.getState()))
except Exception:
states.append("?")
multi[k] = states
n += 1
return {
"result_cls": r.__class__.__name__,
"len_works": _safe(lambda: len(r)),
"dataset_attr": hasattr(r, "getDataset"),
"entry_count": len(entries),
"distinct_ids": len(by_id),
"multi_state_id_samples": multi,
"first_entries": [_entry_facts(e) for e in entries[:6]],
"system_rows": [_entry_facts(e) for e in entries
if str(_safe(lambda: str(e.getSource()))["value"]).startswith("evt:")][:3],
}
result["basic"] = _safe(q_basic)
result["no_kwargs_at_all"] = _safe(
lambda: len([e for e in system.alarm.queryJournal(
startDate=start, endDate=end)]))
result["with_journal_name"] = _safe(
lambda: len([e for e in system.alarm.queryJournal(
journalName="Journal", startDate=start, endDate=end)]))
result["bad_journal_name"] = _safe(
lambda: len([e for e in system.alarm.queryJournal(
journalName="NoSuchJournal", startDate=start, endDate=end)]))
result["long_millis_dates"] = _safe(
lambda: len([e for e in system.alarm.queryJournal(
startDate=system.date.toMillis(start),
endDate=system.date.toMillis(end))]))
result["include_system_false"] = _safe(
lambda: len([e for e in system.alarm.queryJournal(
startDate=start, endDate=end, includeSystemEvents=False)]))
result["include_system_false_alt"] = _safe(
lambda: len([e for e in system.alarm.queryJournal(
startDate=start, endDate=end, isSystem=False)]))
def q_status():
r = system.alarm.queryStatus(state=["ActiveUnacked", "ActiveAcked"])
entries = [e for e in r]
return {"count": len(entries),
"first_entries": [_entry_facts(e) for e in entries[:5]]}
result["status"] = _safe(q_status)
result["shelved"] = _safe(lambda: repr(system.alarm.getShelvedPaths()))
return result
def run():
result = {"probe": PROBE_NAME,
"ranAt": str(system.date.now())}
result["payload"] = _safe(_p0)
_write(result)
system.util.getLogger("probe").info("probe %s written to %s" % (PROBE_NAME, OUT_DIR))

View File

@@ -0,0 +1,12 @@
{
"scope": "A",
"version": 1,
"restricted": false,
"overridable": true,
"files": [
"code.py"
],
"attributes": {
"hintScope": 2
}
}

View File

@@ -0,0 +1,2 @@
def handleTimerEvent():
alarmsim.tick()

View File

@@ -0,0 +1,15 @@
{
"scope": "G",
"version": 1,
"restricted": false,
"overridable": true,
"files": [
"handleTimerEvent.py"
],
"attributes": {
"sharedThread": false,
"delay": 5000,
"fixedDelay": true,
"enabled": true
}
}

View File

@@ -0,0 +1,2 @@
def handleTimerEvent():
probe.run()

View File

@@ -0,0 +1,15 @@
{
"scope": "G",
"version": 1,
"restricted": false,
"overridable": true,
"files": [
"handleTimerEvent.py"
],
"attributes": {
"sharedThread": false,
"delay": 15000,
"fixedDelay": true,
"enabled": false
}
}

View File

@@ -0,0 +1,7 @@
{
"title": "SimHarness",
"description": "Dev-only: alarm simulator + gateway probes + scratch views. NOT part of the Build-a-Thon submission.",
"enabled": true,
"inheritable": false,
"parent": ""
}