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.

flowchart TD subgraph ED2["Edge Detection Group 2"] DI2["DI 17-32 Raw"] --> RT2["R_TRIG 17-32"] RT2 --> P2["K_16BitsToWord"] end subgraph ED1["Edge Detection Group 1"] DI1["DI 1-16 Raw"] --> RT1["R_TRIG 1-16"] RT1 --> P1["K_16BitsToWord"] end subgraph PACKING["Packing Digital Input"] ED1 ED2 P1 --> MERGE["K_2WordToDWord"] P2 --> MERGE end TIME["System Time"] --> EP["K_Epoch"] MERGE --> SOE["K_SOE32"] EP --> SOE RESET["Reset"] --> TP["TP Pulse"] --> SOE SOE --> EPT["K_EpochToTime"] SOE --> SORT["K_SOE32Sort"] SORT --> DELTA["K_SOE32Delta"] SORT --> HMI[/"HMI • SCADA • Report"/] EPT --> HMI DELTA --> HMI %% ClassDef for Efficient Styling (Variasi Warna) classDef edge1 fill:#E3F2FD,stroke:#1E88E5,stroke-width:1px classDef edge2 fill:#C8E6C9,stroke:#2E7D32,stroke-width:1px classDef time fill:#E3F2FD,stroke:#1E88E5,stroke-width:2px classDef system fill:#FFF9C4,stroke:#FBC02D,stroke-width:2px classDef sort fill:#FFE0B2,stroke:#FB8C00,stroke-width:2px classDef hmi fill:#F3E5F5,stroke:#8E24AA,stroke-width:2px classDef pack fill:#E8F5E9,stroke:#43A047,stroke-width:2px %% Assign Classes class ED1,ED2 edge1 class PACKING pack class P1,P2,MERGE edge2 class EP,EPT time class DI1,DI2,RT1,RT2,TIME,RESET,TP system class SOE,SORT,DELTA sort class HMI hmi
Sequence of Event (SOE) Management Flow in Supcon JX-300XP

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, set Epoch=0 and RETURN.
  • Init MonthDays manual (31 Jan, 28 Feb, etc.).
  • Fetch time: Year = CENTURY()*100 + YEAR(), Month=MONTH(), etc.
  • Validate range (Year ≥2000, Month 1–12, etc.) → Epoch=0 if 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=0 if 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..IN16 to internal array Inputs[0..15] (IN1=Inputs[0]).
  • Init OUT1 = 0.
  • Loop FOR i:=0 TO 15: If Inputs[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=TRUE and TripOut invalid (e.g., -1 or does not match latched bit, from lowest latched channel via FOR loop).
  • Manual reset (via pulse Reset=TRUE) → clear all latches (set Latched=FALSE, TripsOut=0, TripOut=-1, TsInt=0, LockOut=FALSE).
  • Loop channels (FOR i:=0 TO 31): Detect rising edge (Run bit i set AND NOT Latched[i], where Run is DWORD from R_TRIG external pre-processing before K_16BitsToWord), store TsInt[i]=Ts current if not latched and Ts>0 (skip glitch), set TripOut=i if TripOut=-1. Update TripsOut from Latched bits; LockOut = LockOut OR (TripsOut <> 0).
  • Map TsInt to TsOut struct (via CASE Val1-Val32).
  • TripTs = TsInt[TripOut] if TripOut >=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.
  • TripOut and TripTs directly 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..Val32 to working array TsArr.
  • Build index list Order only for timestamps >0. Empty slots filled with -1.
  • Apply bubble sort on Order array:
    • Primary key: timestamp value.
    • Tie-breaker: channel number (lower prioritized).
  • Map sorted indices to Seq.Val1..Val32 struct.
  • Map sorted timestamps to TsOut.Val1..Val32 struct. 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..Val32 to working array TsArr.
  • Find base timestamp first >0. If none, base=0.
  • Loop each channel:
    • If base>0 and TsArr[i] ≥ base, then Diff[i] = TsArr[i] – base.
    • If invalid, Diff[i] = 0.
  • Map deltas to Diff.Val1..Val32 struct.

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=FALSE or Epoch > 4294967295 (max ULONG), set all outputs (Year..Second)=0 and RETURN.
  • Init MonthDays manual (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, WHILE Days ≥ 365/366 (leap if MOD 4=0 and (MOD 100<>0 OR MOD 400=0)), subtract Days, Year +=1.
  • Calculate month: FOR i:=0 TO 11, j=MonthDays[i], adjust Feb j=29 if leap, subtract Days if ≥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..IN8 to an array.
  • Loop from 0-7: Add +1 if 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