Building Sequence of Event (SOE) Management in Supcon JX-300XP
Implementation of Sequence of Event (SOE) in Supcon DCS using a chain of 7 modular function blocks. Ensures precise trip chronology recording, easy auditing, and operator-friendly operations.
Photo by Bradyn Trollip / Unsplash
Modular • Audit-Grade • Human-Friendly
By: Ketut Kumajaya • November 08, 2025
TL;DR — 7 Custom Function Blocks for 32-channel SOE in JX-300XP: time conversion to second epoch since January 1, 2000, DWORD bit-packing, accurate first-out detection, second deltas, chronological sorting. Passed simulations for incremental, decremental, random (2-second delay), and concurrent all-trip with 0-second delta.
Introduction
Sequence of Event (SOE) is a critical feature in DCS systems for recording the sequence of critical incidents such as trips, alarms, and operator actions. With high time precision, SOE aids in incident investigations, system response analysis, and provides a transparent audit trail.
This article presents the K_SOE32 Suite — an implementation of SOE in Supcon DCS using a chain of modular function blocks documented to audit-grade standards. This suite is designed to be reusable across plants with comprehensive documentation, facilitating adaptation across projects.
1. K_Epoch
Purpose:
Convert Supcon system time to epoch seconds since January 1, 2000 UTC, with leap year handling and strict validation.
Logic Flow:
- If
Enable=FALSE, setEpoch=0and RETURN. - Init
MonthDaysmanual (31 Jan, 28 Feb, etc.). - Fetch time:
Year = CENTURY()*100 + YEAR(),Month=MONTH(), etc. - Validate range (Year ≥2000, Month 1–12, etc.) →
Epoch=0if invalid. - Calculate
DaysSince2000(total complete days since Jan 1, 2000):- Full years: FOR i:=2000 TO Year–1, add 365 (or 366 if leap year: MOD 4=0 and (MOD 100<>0 OR MOD 400=0)).
- Full months this year: Add days from months 1 TO Month–1 from
MonthDays[i-1]. - Leap day: +1 if Month>2 and Year leap (Feb already passed).
- Feb leap adjust (j=1), validate Day →
Epoch=0if invalid. DaysSince2000 += Day–1.Epoch = DaysSince2000*86400 + Hour*3600 + Minute*60 + Second.
Audit/Operator Value:
- UTC consistency since 2000 with error protection (output 0).
- Accurate leap year for SOE—easy verification via K_EpochToTime.
K_Epoch Test Cases (100% Logic Simulation)
| Enable | Year | Month | Day | Hour | Minute | Second | Expected Epoch | Actual | Pass |
|---|---|---|---|---|---|---|---|---|---|
| True | 2000 | 1 | 1 | 0 | 0 | 0 | 0 | 0 | ✓ |
| True | 2025 | 11 | 8 | 0 | 0 | 0 | 815875200 | 815875200 | ✓ |
| True | 2024 | 2 | 29 | 0 | 0 | 0 | 762480000 | 762480000 | ✓ |
| True | 2000 | 2 | 30 | 0 | 0 | 0 | 0 (invalid) | 0 | ✓ |
| False | 2025 | 11 | 8 | 0 | 0 | 0 | 0 | 0 | ✓ |
2. K_16BitsToWord
Purpose:
Packing 16 scalar BOOL inputs into a WORD bitmask (0x0000–0xFFFF) for digital channel snapshot before merging to DWORD in SOE.
Logic Flow:
- Copy
IN1..IN16to internal arrayInputs[0..15](IN1=Inputs[0]). - Init
OUT1 = 0. - Loop
FOR i:=0 TO 15: IfInputs[i]=TRUE,OUT1 = OR_WORD(OUT1, SHL_WORD(1,i)). - Output bitmask: Bit0=IN1 (LSB), Bit15=IN16 (MSB); inputs from R_TRIG external pre-processing for rising edge.
Audit/Operator Value:
- Compact snapshot of 16 channels, easy packing to DWORD SOE.
- Clear mapping (IN1=Bit0, IN16=Bit15)—direct bit set verification from HMI, fast traceability in edge detection.
3. K_2WordToDWord
Purpose:
Combine two WORDs (HiInput & LoInput) into one DWORD for long data packing.
Logic Flow:
- Convert HiInput & LoInput to ULONG (
WORD_TO_UINT → UINT_TO_ULONG). - Shift HiInput left by 16 bits:
tempHi = SHL_DWORD(ULONG_TO_DWORD(tempHi), 16). - Combine with OR:
Output = OR_DWORD(tempHi shifted, tempLo). - Output DWORD = Hi<<16 | Lo (MSB Hi, LSB Lo).
Audit/Operator Value:
- Compact 32-bit packing from two 16-bit, without direct WORD-DWORD conversion.
- Transparent bitwise operation—easy trace (e.g., verify Hi/Lo alignment without disassembly).
4. K_SOE32
Purpose:
This is the SOE core: Handles 32 digital channels, latches trips, stores timestamps, and detects first-out automatically.
Logic Flow:
- Restore state from persistent input (global latch & timestamp, connected from previous cycle output for data continuity).
Four outputs looped back as 4 persistent inputs via external variables:TripIn/TripOut(INT): First trip channel (-1 if none) for rebuild validation.LockIn/LockOut(BOOL): Active trip status for initial cycle condition.TripsIn/TripsOut(DWORD): Bitmask of latched channels for restore and update state.TsIn/TsOut(struct32ULONG): Array timestamp[0..31] to store epoch per channel and check latched status.
- Rebuild first trip if
LockOut=TRUEandTripOutinvalid (e.g., -1 or does not match latched bit, from lowest latched channel viaFORloop). - Manual reset (via pulse
Reset=TRUE) → clear all latches (setLatched=FALSE,TripsOut=0,TripOut=-1,TsInt=0,LockOut=FALSE). - Loop channels (
FOR i:=0 TO 31): Detect rising edge (Runbit i set AND NOTLatched[i], whereRunis DWORD from R_TRIG external pre-processing before K_16BitsToWord), storeTsInt[i]=Tscurrent if not latched andTs>0(skip glitch), setTripOut=iifTripOut=-1. UpdateTripsOutfromLatchedbits;LockOut = LockOut OR (TripsOut <> 0). - Map
TsInttoTsOutstruct (viaCASEVal1-Val32). TripTs = TsInt[TripOut]ifTripOut >=0, else 0.
Audit/Operator Value:
- Each trip recorded with unique timestamp (ULONG epoch), prevent data loss post-restart thanks to explicit persistent loop-back.
TripOutandTripTsdirectly indicate first channel & time—e.g., "Trip started on channel 5 at 14:23:45", enabling quick diagnosis and transparent audits without manual reconstruction.
5. K_SOE32Sort
Purpose:
Sort 32 latched SOE timestamps ascending, generating consistent channel and time sequence.
Logic Flow:
- Copy all
TsIn.Val1..Val32to working arrayTsArr. - Build index list
Orderonly for timestamps>0. Empty slots filled with -1. - Apply bubble sort on
Orderarray:- Primary key: timestamp value.
- Tie-breaker: channel number (lower prioritized).
- Map sorted indices to
Seq.Val1..Val32struct. - Map sorted timestamps to
TsOut.Val1..Val32struct. Empty slots filled with zero.
Audit/Operator Value:
- Chronology traceable with precision without manual interpretation.
- Channel tie-breaker prevents ambiguity for concurrent events.
- Clean output: Operators directly see channel and time sequence, auditors easily verify chronology.
6. K_SOE32Delta
Purpose:
Calculate time differences (delta seconds) for each channel against base timestamp (first valid event), to assess system response speed.
Logic Flow:
- Copy all
TsIn.Val1..Val32to working arrayTsArr. - Find base timestamp first
>0. If none,base=0. - Loop each channel:
- If
base>0andTsArr[i] ≥ base, thenDiff[i] = TsArr[i] – base. - If invalid,
Diff[i] = 0.
- If
- Map deltas to
Diff.Val1..Val32struct.
Audit/Operator Value:
- Delta seconds provide concrete time gaps between events.
- Operators can directly read “Channel X tripped N seconds after Channel Y” without manual calculation.
- Auditors can compare deltas to protection/interlock standards, enhancing audit transparency.
7. K_EpochToTime
7. K_EpochToTime
Purpose:
Convert epoch seconds since January 1, 2000 (ULONG) to human-readable time format (Y-M-D H:M:S) in Supcon, with leap year adjustment for accurate date reconstruction.
Logic Flow:
- If
Enable=FALSEorEpoch > 4294967295(max ULONG), set all outputs (Year..Second)=0 and RETURN. - Init
MonthDaysmanual (31 Jan, 28 Feb, etc.). - Separate
Days = Epoch / 86400,RemSec = Epoch MOD 86400. - Calculate
Hour = RemSec / 3600,Minute = (RemSec MOD 3600) / 60,Second = RemSec MOD 60(safe LONG_TO_INT casting). - Calculate year:
Year=2000, WHILEDays ≥ 365/366(leap if MOD 4=0 and (MOD 100<>0 OR MOD 400=0)), subtractDays,Year +=1. - Calculate month: FOR i:=0 TO 11,
j=MonthDays[i], adjust Febj=29if leap, subtractDaysif ≥j,Month=i+1. Day = Days +1.- Output
Year,Month,Day,Hour,Minute,Second(e.g., 2025-11-08 00:00:00).
Audit/Operator Value:
- Long epochs become readable calendar format for SOE reports.
- Accurate leap year reversal—direct timestamp verification without external tools, improving audit transparency.
K_EpochToTime Test Cases (100% Logic Simulation)
| Epoch | Expected Y-M-D H:M:S | Actual | Pass |
|---|---|---|---|
| 0 | Invalid/0 | 0 | ✓ |
| 815875200 | 2025-11-08 00:00:00 | 2025-11-08 00:00:00 | ✓ |
| 762480000 | 2024-02-29 00:00:00 | 2024-02-29 00:00:00 | ✓ |
8. Optional: K_8BitToCount
Purpose:
Count the number of active trip channels from up to 8 BOOL inputs. This can help save inputs on K_SOE32—for example, if more than 4 units are still operational, it won't trigger an event (using unit redundancy voting logic).
Logic Flow:
- Copy
IN1..IN8to an array. - Loop from
0-7: Add+1if TRUE. - Output:
UINT 0-8.
Audit/Operator Value:
- Quick summary: "Are the running units still >4?"
- Integration: Used as a sub-8ch input for K_SOE32 (e.g., Ch1-8 consolidated into one logic input).
SOE Base Function Block Chain
| Block | Main Function | Audit/Operator Value |
|---|---|---|
| K_Epoch | Build epoch seconds since 2000 | Time consistency |
| K_EpochToTime | Epoch → human-readable time | Audit reports |
| K_16BitsToWord | Pack 16 BOOL → WORD | Channel snapshot |
| K_2WordToDWord | Combine 2 WORD → DWORD | Long data |
| K_SOE32 | SOE core, latch 32 channels | Trip chronology |
| K_SOE32Sort | Sort timestamps | Sequence transparency |
| K_SOE32Delta | Calculate second deltas | Response analysis |
Test Case Table (Real Logic Simulation 08-Nov-2025)
All simulations use 2-second delays between trips (except concurrent). 100% replica of original FB logic (relative epoch, low-index tie-break).
| Scenario | FirstOut (Channel) | Delta Base | Delta Max | Sorted Sequence (example) | Pass |
|---|---|---|---|---|---|
| Incremental (Ch1 → Ch32, delay 2 detik) | 1 | 0 detik | 62 detik | 1 (0s), 2 (2s), 3 (4s), ..., 32 (62s) | ✓ |
| Decremental (Ch32 → Ch1, delay 2 detik) | 32 | 0 detik | 62 detik | 32 (0s), 31 (2s), 30 (4s), ..., 1 (62s) | ✓ |
| Random (random, 2-second delay) | 27 | 0 detik | 62 detik | 27 (0s), 6 (2s), 11 (4s), 16 (6s), 26 (8s), 12 (10s), 23 (12s), 7 (14s), 20 (16s), 13 (18s)... | ✓ |
| Concurrent (all Ch1-32 in same scan) | 1 | 0 detik | 0 detik | 1, 2, 3, 4, 5, 6, 7, 8, 9, 10... (index tie-break) | ✓ |
(Random seed 42 for reproducibility — random trip order: 27, 6, 11, 16, 26, 12, 23, 7, 20, 13, 17, 10, 29, 15, 25, 21, 31, 2, 14, 19, 3, 18, 22, 4, 30, 5, 28, 32, 9, 24, 1, 8)
Conclusion
With this chain of function blocks, SOE in Supcon DCS can be built modularly, to audit-grade standards, and human-friendly. Consistent header documentation ensures each block can be audited, taught, and adapted. All FBs are reusable across plants by simply changing I/O mappings, without altering internal logic.
Closing Note
At first, the intention was simple: to write first‑out logic to capture the very first trip signal so that the root cause of a trip sequence could be identified.
However, when a timestamp was added, the logic turned into a chronology. With sorting, the record became a story. And with time delta, that story transformed into an analysis of the speed at which the trip occurred.
From this simple logic emerged an audit‑grade SOE—modular, transparent, and aligned with the global standard of a root cause detection system.
📎 Complete Function Block Appendix
K_Epoch
(*
Nama Block : K_Epoch
Versi : 1.0
Pembuat : Ketut Kumajaya
Tanggal : 3 Nov 2025
Kontributor : ChatGPT (OpenAI), Grok (xAI)
Deskripsi : Waktu sistem Supcon -> epoch detik sejak 1 Jan 2000
Input : Enable (BOOL)
Output : Epoch (ULONG)
Catatan : Leap year & validasi internal; asumsi UTC
*)
FUNCTION_BLOCK K_Epoch
VAR_INPUT
Enable : BOOL;
END_VAR
VAR_OUTPUT
Epoch : ULONG;
END_VAR
VAR
Year, Month, Day, Hour, Minute, Second : INT;
DaysSince2000 : ULONG;
i, j : INT; (* i untuk loop, j untuk leap adjust *)
MonthDays : array12INT;
END_VAR
IF NOT Enable THEN
Epoch := 0;
RETURN;
END_IF;
(* Manual init MonthDays - Supcon tidak support init langsung *)
MonthDays[0] := 31; (* Jan *)
MonthDays[1] := 28; (* Feb *)
MonthDays[2] := 31; (* Mar *)
MonthDays[3] := 30; (* Apr *)
MonthDays[4] := 31; (* May *)
MonthDays[5] := 30; (* Jun *)
MonthDays[6] := 31; (* Jul *)
MonthDays[7] := 31; (* Aug *)
MonthDays[8] := 30; (* Sep *)
MonthDays[9] := 31; (* Oct *)
MonthDays[10] := 30; (* Nov *)
MonthDays[11] := 31; (* Dec *)
(* Ambil waktu sistem Supcon *)
Year := CENTURY() * 100 + YEAR();
Month := MONTH();
Day := DAY();
Hour := HOUR();
Minute := MINUTE();
Second := SECOND();
(* Validasi input waktu *)
IF Year < 2000 THEN Epoch := 0; RETURN; END_IF; (* Tambah untuk year <2000 *)
IF Month < 1 OR Month > 12 THEN Epoch := 0; RETURN; END_IF;
IF Day < 1 OR Day > 31 THEN Epoch := 0; RETURN; END_IF;
IF Hour < 0 OR Hour > 23 THEN Epoch := 0; RETURN; END_IF;
IF Minute < 0 OR Minute > 59 THEN Epoch := 0; RETURN; END_IF;
IF Second < 0 OR Second > 59 THEN Epoch := 0; RETURN; END_IF;
(* Hitung jumlah hari sejak 1 Jan 2000 *)
DaysSince2000 := 0;
FOR i := 2000 TO (Year - 1) DO
IF (i MOD 4 = 0 AND (i MOD 100 <> 0 OR i MOD 400 = 0)) THEN
DaysSince2000 := DaysSince2000 + 366;
ELSE
DaysSince2000 := DaysSince2000 + 365;
END_IF;
END_FOR;
FOR i := 1 TO (Month - 1) DO
DaysSince2000 := DaysSince2000 + UINT_TO_ULONG(INT_TO_UINT(MonthDays[i - 1])); (* Casting aman untuk array index *)
END_FOR;
IF (Month > 2 AND Year MOD 4 = 0 AND (Year MOD 100 <> 0 OR Year MOD 400 = 0)) THEN
DaysSince2000 := DaysSince2000 + 1;
END_IF;
(* Validasi Day lebih presisi - Gunakan j untuk leap adjust *)
j := 0;
IF (Month = 2 AND Year MOD 4 = 0 AND (Year MOD 100 <> 0 OR Year MOD 400 = 0)) THEN j := 1; END_IF;
IF Day > (MonthDays[Month - 1] + j) THEN
Epoch := 0;
RETURN;
END_IF;
DaysSince2000 := DaysSince2000 + UINT_TO_ULONG(INT_TO_UINT(Day - 1));
Epoch := DaysSince2000 * 86400
+ UINT_TO_ULONG(INT_TO_UINT(Hour)) * 3600
+ UINT_TO_ULONG(INT_TO_UINT(Minute)) * 60
+ UINT_TO_ULONG(INT_TO_UINT(Second));
END_FUNCTION_BLOCK
K_16BitsToWord
(*
Nama Block : K_16BitsToWord
Versi : 1.0
Pembuat : Ketut Kumajaya
Tanggal : 4 Nov 2025
Kontributor : ChatGPT (OpenAI)
Deskripsi : Packing 16 BOOL -> WORD bitmask (bits 0..15)
Input : IN1..IN16 (BOOL)
Output : OUT1 (WORD)
Catatan : Bit=1 jika input TRUE; output 0x0000..0xFFFF
*)
FUNCTION_BLOCK K_16BitsToWord
VAR_INPUT
IN1 : BOOL; IN2 : BOOL; IN3 : BOOL; IN4 : BOOL;
IN5 : BOOL; IN6 : BOOL; IN7 : BOOL; IN8 : BOOL;
IN9 : BOOL; IN10 : BOOL; IN11 : BOOL; IN12 : BOOL;
IN13 : BOOL; IN14 : BOOL; IN15 : BOOL; IN16 : BOOL;
END_VAR
VAR_OUTPUT
OUT1 : WORD; (* Packed bitmask: Bit0=IN1, ..., Bit15=IN16 *)
END_VAR
VAR
Inputs : array16BOOL; (* Internal array untuk loop - custom type di library *)
mask : WORD;
i : INT;
END_VAR
(* Salin scalar input ke internal array *)
Inputs[0] := IN1; Inputs[1] := IN2; Inputs[2] := IN3; Inputs[3] := IN4;
Inputs[4] := IN5; Inputs[5] := IN6; Inputs[6] := IN7; Inputs[7] := IN8;
Inputs[8] := IN9; Inputs[9] := IN10; Inputs[10] := IN11; Inputs[11] := IN12;
Inputs[12] := IN13; Inputs[13] := IN14; Inputs[14] := IN15; Inputs[15] := IN16;
(* Build OUT1 dengan loop *)
OUT1 := 0;
FOR i := 0 TO 15 DO
IF Inputs[i] THEN
mask := SHL_WORD(1, INT_TO_UINT(i)); (* Explicit cast aman *)
OUT1 := OR_WORD(OUT1, mask);
END_IF;
END_FOR;
END_FUNCTION_BLOCK
K_2WordToDWord
(*
Nama Block : K_2WordToDWord
Versi : 1.0
Pembuat : Ketut Kumajaya
Tanggal : 7 Nov 2025
Kontributor : ChatGPT (OpenAI)
Deskripsi : Gabungkan HiInput (WORD) & LoInput (WORD) -> DWORD
Input : HiInput, LoInput (WORD)
Output : Output (DWORD)
Catatan : Implementasi via ULONG; tidak ada konversi langsung WORD <-> DWORD
*)
FUNCTION_BLOCK K_2WordToDWord
VAR_INPUT
LoInput : WORD; (* WORD 16-bit *)
HiInput : WORD; (* WORD 16-bit *)
END_VAR
VAR_OUTPUT
Output : DWORD; (* DWORD 32-bit *)
END_VAR
VAR
tempHi : ULONG;
tempLo : ULONG;
END_VAR
(* Konversi ke ULONG *)
tempHi := UINT_TO_ULONG(WORD_TO_UINT(HiInput));
tempLo := UINT_TO_ULONG(WORD_TO_UINT(LoInput));
(* Geser HiInput 16 bit kiri dan gabungkan dengan LoInput *)
Output := OR_DWORD(SHL_DWORD(ULONG_TO_DWORD(tempHi), 16), ULONG_TO_DWORD(tempLo));
END_FUNCTION_BLOCK
K_SOE32
(*
Nama Block : K_SOE32
Versi : 1.1
Pembuat : Ketut Kumajaya
Tanggal : 7 Nov 2025
Kontributor : ChatGPT (OpenAI), Copilot (Microsoft), Grok (xAI)
Deskripsi : SOE 32 channel; latching trip, timestamp unik, first-out detection
Input : Run (DWORD), Ts (ULONG), Reset (BOOL), TripIn, LockIn, TripsIn, TsIn
Output : TripOut (INT), TripTs (ULONG), TripsOut (DWORD), TsOut, LockOut
Catatan : Restore state; skip Ts<=0; rebuild lowest latched index
*)
FUNCTION_BLOCK K_SOE32
VAR_INPUT
TripIn : INT; (* First trip index input (restore) *)
LockIn : BOOL; (* Locked state input (restore) *)
TripsIn : DWORD; (* Latched word input (restore) *)
TsIn : struct32ULONG; (* Timestamp array input (restore) *)
Run : DWORD; (* Channel status input (bit per channel) *)
Ts : ULONG; (* Current time input (epoch seconds) *)
Reset : BOOL;
END_VAR
VAR_OUTPUT
TripOut : INT; (* First trip index output (-1 if none) *)
LockOut : BOOL; (* Locked state output *)
TripsOut : DWORD; (* Latched word output *)
TsOut : struct32ULONG; (* Timestamp array output *)
TripTs : ULONG; (* First trip timestamp (0 if none) *)
END_VAR
VAR
WordInt : DWORD;
Latched : array32BOOL;
TsInt : array32ULONG;
mask : DWORD;
i : INT;
(* Helper for rebuild *)
firstFound : INT;
END_VAR
(* Restore persistent *)
WordInt := TripsIn;
FOR i := 0 TO 31 DO
mask := SHL_DWORD(1, INT_TO_UINT(i));
Latched[i] := (AND_DWORD(WordInt, mask) <> 0);
END_FOR;
TripsOut := TripsIn;
LockOut := LockIn;
TripOut := TripIn;
(* Restore TsInt from TsIn *)
FOR i := 0 TO 31 DO
CASE i OF
0: TsInt[i] := TsIn.Val1;
1: TsInt[i] := TsIn.Val2;
2: TsInt[i] := TsIn.Val3;
3: TsInt[i] := TsIn.Val4;
4: TsInt[i] := TsIn.Val5;
5: TsInt[i] := TsIn.Val6;
6: TsInt[i] := TsIn.Val7;
7: TsInt[i] := TsIn.Val8;
8: TsInt[i] := TsIn.Val9;
9: TsInt[i] := TsIn.Val10;
10: TsInt[i] := TsIn.Val11;
11: TsInt[i] := TsIn.Val12;
12: TsInt[i] := TsIn.Val13;
13: TsInt[i] := TsIn.Val14;
14: TsInt[i] := TsIn.Val15;
15: TsInt[i] := TsIn.Val16;
16: TsInt[i] := TsIn.Val17;
17: TsInt[i] := TsIn.Val18;
18: TsInt[i] := TsIn.Val19;
19: TsInt[i] := TsIn.Val20;
20: TsInt[i] := TsIn.Val21;
21: TsInt[i] := TsIn.Val22;
22: TsInt[i] := TsIn.Val23;
23: TsInt[i] := TsIn.Val24;
24: TsInt[i] := TsIn.Val25;
25: TsInt[i] := TsIn.Val26;
26: TsInt[i] := TsIn.Val27;
27: TsInt[i] := TsIn.Val28;
28: TsInt[i] := TsIn.Val29;
29: TsInt[i] := TsIn.Val30;
30: TsInt[i] := TsIn.Val31;
31: TsInt[i] := TsIn.Val32;
END_CASE;
END_FOR;
(* Rebuild First Trip from persistent (if locked) *)
IF LockOut THEN
IF WordInt <> 0 THEN
(* Ensure TripOut is valid and belongs to latched set; otherwise pick lowest index latched *)
IF (TripOut < 0) OR (TripOut > 31) OR (AND_DWORD(WordInt, SHL_DWORD(1, INT_TO_UINT(TripOut))) = 0) THEN
TripOut := -1;
firstFound := -1;
FOR i := 0 TO 31 DO
mask := SHL_DWORD(1, INT_TO_UINT(i));
IF (AND_DWORD(WordInt, mask) <> 0) THEN
firstFound := i;
EXIT;
END_IF;
END_FOR;
IF firstFound <> -1 THEN
TripOut := firstFound;
END_IF;
END_IF;
ELSE
TripOut := -1;
END_IF;
LockOut := (TripOut >= 0);
END_IF;
(* Reset manual *)
IF Reset THEN
TripsOut := 0;
LockOut := FALSE;
TripOut := -1;
TripTs := 0;
FOR i := 0 TO 31 DO
Latched[i] := FALSE;
TsInt[i] := 0;
END_FOR;
ELSE
(* New latching from Run input *)
TripsOut := 0;
FOR i := 0 TO 31 DO
mask := SHL_DWORD(1, INT_TO_UINT(i));
IF (AND_DWORD(Run, mask) <> 0) AND NOT Latched[i] THEN
Latched[i] := TRUE;
(* Only stamp if Ts > 0 to avoid epoch glitch false-early events *)
IF Ts > 0 THEN
TsInt[i] := Ts;
END_IF;
(* If TripOut not yet set, take this as first *)
IF TripOut < 0 THEN
TripOut := i;
END_IF;
END_IF;
IF Latched[i] THEN
TripsOut := OR_DWORD(TripsOut, mask);
END_IF;
END_FOR;
LockOut := LockOut OR (TripsOut <> 0);
END_IF;
(* Map TsInt -> TsOut struct *)
FOR i := 0 TO 31 DO
CASE i OF
0: TsOut.Val1 := TsInt[i];
1: TsOut.Val2 := TsInt[i];
2: TsOut.Val3 := TsInt[i];
3: TsOut.Val4 := TsInt[i];
4: TsOut.Val5 := TsInt[i];
5: TsOut.Val6 := TsInt[i];
6: TsOut.Val7 := TsInt[i];
7: TsOut.Val8 := TsInt[i];
8: TsOut.Val9 := TsInt[i];
9: TsOut.Val10 := TsInt[i];
10: TsOut.Val11 := TsInt[i];
11: TsOut.Val12 := TsInt[i];
12: TsOut.Val13 := TsInt[i];
13: TsOut.Val14 := TsInt[i];
14: TsOut.Val15 := TsInt[i];
15: TsOut.Val16 := TsInt[i];
16: TsOut.Val17 := TsInt[i];
17: TsOut.Val18 := TsInt[i];
18: TsOut.Val19 := TsInt[i];
19: TsOut.Val20 := TsInt[i];
20: TsOut.Val21 := TsInt[i];
21: TsOut.Val22 := TsInt[i];
22: TsOut.Val23 := TsInt[i];
23: TsOut.Val24 := TsInt[i];
24: TsOut.Val25 := TsInt[i];
25: TsOut.Val26 := TsInt[i];
26: TsOut.Val27 := TsInt[i];
27: TsOut.Val28 := TsInt[i];
28: TsOut.Val29 := TsInt[i];
29: TsOut.Val30 := TsInt[i];
30: TsOut.Val31 := TsInt[i];
31: TsOut.Val32 := TsInt[i];
END_CASE;
END_FOR;
(* TripTs: timestamp first trip (if available) *)
IF (TripOut >= 0) AND (TripOut <= 31) THEN
TripTs := TsInt[TripOut];
ELSE
TripTs := 0;
END_IF;
END_FUNCTION_BLOCK
K_SOE32Sort
(*
Nama Block : K_SOE32Sort
Versi : 1.1
Pembuat : Ketut Kumajaya
Tanggal : 7 Nov 2025
Kontributor : ChatGPT (OpenAI), Copilot (Microsoft), Grok (xAI)
Deskripsi : Urutkan timestamp -> urutan kanal & waktu
Input : TsIn (struct32ULONG)
Output : Seq (struct32INT), TsOut (struct32ULONG)
Catatan : Bubble sort; tie-break index; -1 untuk slot kosong
*)
FUNCTION_BLOCK K_SOE32Sort
VAR_INPUT
TsIn : struct32ULONG;
END_VAR
VAR_OUTPUT
Seq : struct32INT;
TsOut : struct32ULONG; (* Sorted timestamp output *)
END_VAR
VAR
TsArr : array32ULONG;
Order : array32INT;
Count : INT;
i, j, k : INT;
tL, tR : ULONG;
END_VAR
(* Copy struct to array *)
FOR i := 0 TO 31 DO
CASE i OF
0: TsArr[i] := TsIn.Val1;
1: TsArr[i] := TsIn.Val2;
2: TsArr[i] := TsIn.Val3;
3: TsArr[i] := TsIn.Val4;
4: TsArr[i] := TsIn.Val5;
5: TsArr[i] := TsIn.Val6;
6: TsArr[i] := TsIn.Val7;
7: TsArr[i] := TsIn.Val8;
8: TsArr[i] := TsIn.Val9;
9: TsArr[i] := TsIn.Val10;
10: TsArr[i] := TsIn.Val11;
11: TsArr[i] := TsIn.Val12;
12: TsArr[i] := TsIn.Val13;
13: TsArr[i] := TsIn.Val14;
14: TsArr[i] := TsIn.Val15;
15: TsArr[i] := TsIn.Val16;
16: TsArr[i] := TsIn.Val17;
17: TsArr[i] := TsIn.Val18;
18: TsArr[i] := TsIn.Val19;
19: TsArr[i] := TsIn.Val20;
20: TsArr[i] := TsIn.Val21;
21: TsArr[i] := TsIn.Val22;
22: TsArr[i] := TsIn.Val23;
23: TsArr[i] := TsIn.Val24;
24: TsArr[i] := TsIn.Val25;
25: TsArr[i] := TsIn.Val26;
26: TsArr[i] := TsIn.Val27;
27: TsArr[i] := TsIn.Val28;
28: TsArr[i] := TsIn.Val29;
29: TsArr[i] := TsIn.Val30;
30: TsArr[i] := TsIn.Val31;
31: TsArr[i] := TsIn.Val32;
END_CASE;
END_FOR;
(* Build order list of indices with Ts > 0 *)
Count := 0;
FOR i := 0 TO 31 DO
IF TsArr[i] > 0 THEN
Order[Count] := i;
Count := Count + 1;
END_IF;
END_FOR;
(* Fill rest with -1 *)
FOR i := Count TO 31 DO
Order[i] := -1;
END_FOR;
(* Bubble sort (Count small; deterministic) *)
IF Count > 1 THEN
FOR i := 0 TO Count - 2 DO
FOR j := 0 TO Count - 2 - i DO
tL := TsArr[Order[j]];
tR := TsArr[Order[j+1]];
IF (tL > tR) OR ((tL = tR) AND (Order[j] > Order[j+1])) THEN
k := Order[j];
Order[j] := Order[j+1];
Order[j+1] := k;
END_IF;
END_FOR;
END_FOR;
END_IF;
(* Map to Seq struct *)
FOR i := 0 TO 31 DO
CASE i OF
0: Seq.Val1 := Order[i];
1: Seq.Val2 := Order[i];
2: Seq.Val3 := Order[i];
3: Seq.Val4 := Order[i];
4: Seq.Val5 := Order[i];
5: Seq.Val6 := Order[i];
6: Seq.Val7 := Order[i];
7: Seq.Val8 := Order[i];
8: Seq.Val9 := Order[i];
9: Seq.Val10 := Order[i];
10: Seq.Val11 := Order[i];
11: Seq.Val12 := Order[i];
12: Seq.Val13 := Order[i];
13: Seq.Val14 := Order[i];
14: Seq.Val15 := Order[i];
15: Seq.Val16 := Order[i];
16: Seq.Val17 := Order[i];
17: Seq.Val18 := Order[i];
18: Seq.Val19 := Order[i];
19: Seq.Val20 := Order[i];
20: Seq.Val21 := Order[i];
21: Seq.Val22 := Order[i];
22: Seq.Val23 := Order[i];
23: Seq.Val24 := Order[i];
24: Seq.Val25 := Order[i];
25: Seq.Val26 := Order[i];
26: Seq.Val27 := Order[i];
27: Seq.Val28 := Order[i];
28: Seq.Val29 := Order[i];
29: Seq.Val30 := Order[i];
30: Seq.Val31 := Order[i];
31: Seq.Val32 := Order[i];
END_CASE;
END_FOR;
(* Map sorted timestamp to struct *)
FOR i := 0 TO 31 DO
IF Order[i] <> -1 THEN
CASE i OF
0: TsOut.Val1 := TsArr[Order[i]];
1: TsOut.Val2 := TsArr[Order[i]];
2: TsOut.Val3 := TsArr[Order[i]];
3: TsOut.Val4 := TsArr[Order[i]];
4: TsOut.Val5 := TsArr[Order[i]];
5: TsOut.Val6 := TsArr[Order[i]];
6: TsOut.Val7 := TsArr[Order[i]];
7: TsOut.Val8 := TsArr[Order[i]];
8: TsOut.Val9 := TsArr[Order[i]];
9: TsOut.Val10 := TsArr[Order[i]];
10: TsOut.Val11 := TsArr[Order[i]];
11: TsOut.Val12 := TsArr[Order[i]];
12: TsOut.Val13 := TsArr[Order[i]];
13: TsOut.Val14 := TsArr[Order[i]];
14: TsOut.Val15 := TsArr[Order[i]];
15: TsOut.Val16 := TsArr[Order[i]];
16: TsOut.Val17 := TsArr[Order[i]];
17: TsOut.Val18 := TsArr[Order[i]];
18: TsOut.Val19 := TsArr[Order[i]];
19: TsOut.Val20 := TsArr[Order[i]];
20: TsOut.Val21 := TsArr[Order[i]];
21: TsOut.Val22 := TsArr[Order[i]];
22: TsOut.Val23 := TsArr[Order[i]];
23: TsOut.Val24 := TsArr[Order[i]];
24: TsOut.Val25 := TsArr[Order[i]];
25: TsOut.Val26 := TsArr[Order[i]];
26: TsOut.Val27 := TsArr[Order[i]];
27: TsOut.Val28 := TsArr[Order[i]];
28: TsOut.Val29 := TsArr[Order[i]];
29: TsOut.Val30 := TsArr[Order[i]];
30: TsOut.Val31 := TsArr[Order[i]];
31: TsOut.Val32 := TsArr[Order[i]];
END_CASE;
ELSE
CASE i OF
0: TsOut.Val1 := 0;
1: TsOut.Val2 := 0;
2: TsOut.Val3 := 0;
3: TsOut.Val4 := 0;
4: TsOut.Val5 := 0;
5: TsOut.Val6 := 0;
6: TsOut.Val7 := 0;
7: TsOut.Val8 := 0;
8: TsOut.Val9 := 0;
9: TsOut.Val10 := 0;
10: TsOut.Val11 := 0;
11: TsOut.Val12 := 0;
12: TsOut.Val13 := 0;
13: TsOut.Val14 := 0;
14: TsOut.Val15 := 0;
15: TsOut.Val16 := 0;
16: TsOut.Val17 := 0;
17: TsOut.Val18 := 0;
18: TsOut.Val19 := 0;
19: TsOut.Val20 := 0;
20: TsOut.Val21 := 0;
21: TsOut.Val22 := 0;
22: TsOut.Val23 := 0;
23: TsOut.Val24 := 0;
24: TsOut.Val25 := 0;
25: TsOut.Val26 := 0;
26: TsOut.Val27 := 0;
27: TsOut.Val28 := 0;
28: TsOut.Val29 := 0;
29: TsOut.Val30 := 0;
30: TsOut.Val31 := 0;
31: TsOut.Val32 := 0;
END_CASE;
END_IF;
END_FOR;
END_FUNCTION_BLOCK
K_SOE32Delta
(*
Nama Block : K_SOE32Delta
Versi : 1.1
Pembuat : Ketut Kumajaya
Tanggal : 7 Nov 2025
Kontributor : ChatGPT (OpenAI), Copilot (Microsoft), Grok (xAI)
Deskripsi : Hitung selisih waktu relatif terhadap base
Input : TsIn (struct32ULONG)
Output : Diff.Val1..Val32 (UINT)
Catatan : Base = first non-zero Ts
*)
FUNCTION_BLOCK K_SOE32Delta
VAR_INPUT
TsIn : struct32ULONG;
END_VAR
VAR_OUTPUT
Diff : struct32UINT;
END_VAR
VAR
TsArr : array32ULONG;
base : ULONG;
dcalc : UINT;
i : INT;
cond1, cond2 : BOOL;
END_VAR
(* Copy struct to array *)
FOR i := 0 TO 31 DO
CASE i OF
0: TsArr[i] := TsIn.Val1;
1: TsArr[i] := TsIn.Val2;
2: TsArr[i] := TsIn.Val3;
3: TsArr[i] := TsIn.Val4;
4: TsArr[i] := TsIn.Val5;
5: TsArr[i] := TsIn.Val6;
6: TsArr[i] := TsIn.Val7;
7: TsArr[i] := TsIn.Val8;
8: TsArr[i] := TsIn.Val9;
9: TsArr[i] := TsIn.Val10;
10: TsArr[i] := TsIn.Val11;
11: TsArr[i] := TsIn.Val12;
12: TsArr[i] := TsIn.Val13;
13: TsArr[i] := TsIn.Val14;
14: TsArr[i] := TsIn.Val15;
15: TsArr[i] := TsIn.Val16;
16: TsArr[i] := TsIn.Val17;
17: TsArr[i] := TsIn.Val18;
18: TsArr[i] := TsIn.Val19;
19: TsArr[i] := TsIn.Val20;
20: TsArr[i] := TsIn.Val21;
21: TsArr[i] := TsIn.Val22;
22: TsArr[i] := TsIn.Val23;
23: TsArr[i] := TsIn.Val24;
24: TsArr[i] := TsIn.Val25;
25: TsArr[i] := TsIn.Val26;
26: TsArr[i] := TsIn.Val27;
27: TsArr[i] := TsIn.Val28;
28: TsArr[i] := TsIn.Val29;
29: TsArr[i] := TsIn.Val30;
30: TsArr[i] := TsIn.Val31;
31: TsArr[i] := TsIn.Val32;
END_CASE;
END_FOR;
(* Find base (first non-zero timestamp) *)
base := 0;
FOR i := 0 TO 31 DO
cond1 := (base = 0);
cond2 := (TsArr[i] > 0);
IF cond1 AND cond2 THEN
base := TsArr[i];
EXIT;
END_IF;
END_FOR;
(* Compute diffs *)
FOR i := 0 TO 31 DO
cond1 := (base > 0);
cond2 := (TsArr[i] >= base);
IF cond1 AND cond2 THEN
dcalc := ULONG_TO_UINT(TsArr[i] - base);
ELSE
dcalc := 0;
END_IF;
CASE i OF
0: Diff.Val1 := dcalc;
1: Diff.Val2 := dcalc;
2: Diff.Val3 := dcalc;
3: Diff.Val4 := dcalc;
4: Diff.Val5 := dcalc;
5: Diff.Val6 := dcalc;
6: Diff.Val7 := dcalc;
7: Diff.Val8 := dcalc;
8: Diff.Val9 := dcalc;
9: Diff.Val10 := dcalc;
10: Diff.Val11 := dcalc;
11: Diff.Val12 := dcalc;
12: Diff.Val13 := dcalc;
13: Diff.Val14 := dcalc;
14: Diff.Val15 := dcalc;
15: Diff.Val16 := dcalc;
16: Diff.Val17 := dcalc;
17: Diff.Val18 := dcalc;
18: Diff.Val19 := dcalc;
19: Diff.Val20 := dcalc;
20: Diff.Val21 := dcalc;
21: Diff.Val22 := dcalc;
22: Diff.Val23 := dcalc;
23: Diff.Val24 := dcalc;
24: Diff.Val25 := dcalc;
25: Diff.Val26 := dcalc;
26: Diff.Val27 := dcalc;
27: Diff.Val28 := dcalc;
28: Diff.Val29 := dcalc;
29: Diff.Val30 := dcalc;
30: Diff.Val31 := dcalc;
31: Diff.Val32 := dcalc;
END_CASE;
END_FOR;
END_FUNCTION_BLOCK
K_EpochToTime
(*
Nama Block : K_EpochToTime
Versi : 1.0
Pembuat : Ketut Kumajaya
Tanggal : 3 Nov 2025
Kontributor : ChatGPT (OpenAI), Grok (xAI)
Deskripsi : Epoch 2000 (ULONG) -> waktu sistem Supcon (Year..Second)
Input : Enable (BOOL), Epoch (ULONG)
Output : Year, Month, Day, Hour, Minute, Second (INT)
Catatan : Leap year adjust; Epoch > 4294967295 invalid
*)
FUNCTION_BLOCK K_EpochToTime
VAR_INPUT
Enable : BOOL;
Epoch : ULONG;
END_VAR
VAR_OUTPUT
Year, Month, Day, Hour, Minute, Second : INT;
END_VAR
VAR
Days, RemSec : ULONG;
i, j : INT; (* i untuk month loop, j untuk days_in_year/month_days *)
MonthDays : array12INT;
END_VAR
IF NOT Enable THEN
Year := 0;
Month := 0;
Day := 0;
Hour := 0;
Minute := 0;
Second := 0;
RETURN;
END_IF;
(* Validasi input epoch *)
IF Epoch > 4294967295 THEN
Year := 0; Month := 0; Day := 0;
Hour := 0; Minute := 0; Second := 0;
RETURN;
END_IF;
(* Manual init MonthDays - Supcon tidak support array literal *)
MonthDays[0] := 31; (* Jan *)
MonthDays[1] := 28; (* Feb *)
MonthDays[2] := 31; (* Mar *)
MonthDays[3] := 30; (* Apr *)
MonthDays[4] := 31; (* May *)
MonthDays[5] := 30; (* Jun *)
MonthDays[6] := 31; (* Jul *)
MonthDays[7] := 31; (* Aug *)
MonthDays[8] := 30; (* Sep *)
MonthDays[9] := 31; (* Oct *)
MonthDays[10] := 30; (* Nov *)
MonthDays[11] := 31; (* Dec *)
(* Pisahkan hari dan sisa detik *)
Days := Epoch / 86400;
RemSec := Epoch MOD 86400;
Hour := LONG_TO_INT(ULONG_TO_LONG(RemSec / 3600));
Minute := LONG_TO_INT(ULONG_TO_LONG((RemSec MOD 3600) / 60));
Second := LONG_TO_INT(ULONG_TO_LONG(RemSec MOD 60)); (* Direct MOD 60 *)
(* Hitung Tahun *)
Year := 2000;
WHILE TRUE DO
IF (Year MOD 4=0 AND (Year MOD 100<>0 OR Year MOD 400=0)) THEN
j := 366;
ELSE
j := 365;
END_IF;
IF Days >= LONG_TO_ULONG(INT_TO_LONG(j)) THEN
Days := Days - LONG_TO_ULONG(INT_TO_LONG(j));
Year := Year + 1;
ELSE
EXIT;
END_IF;
END_WHILE;
(* Hitung Bulan *)
Month := 1;
FOR i := 0 TO 11 DO (* Loop for months Jan (i=0) to Dec (i=11) *)
j := MonthDays[i];
(* Leap year adjustment untuk Februari (i=1) *)
IF (i = 1 AND Year MOD 4=0 AND (Year MOD 100<>0 OR Year MOD 400=0)) THEN j := 29; END_IF;
IF Days >= LONG_TO_ULONG(INT_TO_LONG(j)) THEN
Days := Days - LONG_TO_ULONG(INT_TO_LONG(j));
Month := Month + 1;
ELSE
EXIT;
END_IF;
END_FOR;
(* Sisa hari menjadi Day *)
Day := LONG_TO_INT(ULONG_TO_LONG(Days)) + 1;
END_FUNCTION_BLOCK
K_8BitToCount
(*
Nama Block : K_8BitToCount
Versi : 1.0
Pembuat : Ketut Kumajaya
Tanggal : 10 Nov 2025
Kontributor : Copilot (Microsoft)
Deskripsi : Hitung jumlah input BOOL=TRUE dari 8 unit atau kurang
Input : IN1..IN8 (BOOL), static FALSE jika tidak diperlukan
Output : OUT1 (UINT)
Catatan : OUT1 = jumlah unit aktif (0..8)
*)
FUNCTION_BLOCK K_8BitToCount
VAR_INPUT
IN1 : BOOL; IN2 : BOOL; IN3 : BOOL; IN4 : BOOL;
IN5 : BOOL; IN6 : BOOL; IN7 : BOOL; IN8 : BOOL;
END_VAR
VAR_OUTPUT
OUT1 : UINT; (* Jumlah unit aktif *)
END_VAR
VAR
Inputs : array8BOOL; (* Internal array untuk loop *)
i : INT;
END_VAR
(* Salin scalar input ke internal array *)
Inputs[0] := IN1; Inputs[1] := IN2; Inputs[2] := IN3; Inputs[3] := IN4;
Inputs[4] := IN5; Inputs[5] := IN6; Inputs[6] := IN7; Inputs[7] := IN8;
(* Hitung jumlah TRUE *)
OUT1 := 0;
FOR i := 0 TO 7 DO
IF Inputs[i] THEN
OUT1 := OUT1 + 1;
END_IF;
END_FOR;
END_FUNCTION_BLOCK