Modular • Audit-Grade • Human-Friendly
Oleh: Ketut Kumajaya • 08 November 2025

TL;DR — 7 Function Block rancangan sendiri untuk SOE 32-channel di JX-300XP: konversi waktu ke epoch detik sejak 2000, bit-packing DWORD, first-out akurat, delta detik, sorting kronologi. Sudah lolos simulasi incremental, decremental, random (delay 2 detik), dan concurrent all-trip delta=0 detik.

Pendahuluan

Sequence of Event (SOE) adalah fitur penting dalam sistem DCS untuk mencatat urutan kejadian kritis seperti trip, alarm, dan aksi operator. Dengan presisi waktu tinggi, SOE membantu investigasi gangguan, analisis respon sistem, dan menyediakan jejak audit yang transparan.

Artikel ini menyajikan K_SOE32 Suite — implementasi SOE di Supcon DCS menggunakan rangkaian function block modular yang terdokumentasi secara audit-grade. Suite ini dirancang reusable antar plant dengan dokumentasi lengkap, memudahkan adaptasi lintas proyek.

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
Alur manajemen Sequence of Event (SOE) di Supcon JX-300XP

1. K_Epoch

Tujuan:
Mengonversi waktu sistem Supcon menjadi epoch detik sejak 1 Januari 2000 UTC, dengan handling leap year dan validasi ketat.

Alur Logika:

  • Jika Enable=FALSE, set Epoch=0 dan RETURN.
  • Init MonthDays manual (31 Jan, 28 Feb, dll.).
  • Ambil waktu: Year = CENTURY()*100 + YEAR(), Month=MONTH(), dll.
  • Validasi range (Year ≥2000, Month 1–12, dll.) → Epoch=0 jika invalid.
  • Hitung DaysSince2000 (jumlah hari lengkap sejak 1 Jan 2000):
    • Tahun penuh: FOR i:=2000 TO Year–1, tambah 365 (atau 366 kalau leap year: MOD 4=0 dan (MOD 100<>0 OR MOD 400=0)).
    • Bulan penuh tahun ini: Tambah hari bulan 1 TO Month–1 dari MonthDays[i-1].
    • Leap day: +1 kalau Month>2 dan Year leap (karena Feb sudah lewat).
  • Adjust Feb leap (j=1), validasi Day → Epoch=0 jika invalid.
  • DaysSince2000 += Day–1.
  • Epoch = DaysSince2000*86400 + Hour*3600 + Minute*60 + Second.

Nilai Audit/Operator:

  • Konsistensi UTC sejak 2000 dengan proteksi error (output 0).
  • Leap year akurat untuk SOE—verifikasi mudah via K_EpochToTime.

Test Case K_Epoch (Simulasi Logic 100%)

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

Tujuan:
Packing 16 input BOOL scalar menjadi WORD bitmask (0x0000–0xFFFF) untuk snapshot kanal sebelum merging ke DWORD di SOE.

Alur Logika:

  • Salin IN1..IN16 ke array internal Inputs[0..15] (IN1=Inputs[0]).
  • Init OUT1 = 0.
  • Loop FOR i:=0 TO 15: Jika Inputs[i]=TRUE, OUT1 = OR_WORD(OUT1, SHL_WORD(1,i)).
  • Output bitmask: Bit0=IN1 (LSB), Bit15=IN16 (MSB); input dari R_TRIG external pre-processing untuk rising edge.

Nilai Audit/Operator:

  • Snapshot 16 kanal kompak, mudah packing ke DWORD SOE.
  • Mapping jelas (IN1=Bit0, IN16=Bit15)—verifikasi bit set langsung dari HMI, traceability cepat di edge detection.

3. K_2WordToDWord

Tujuan:
Gabungkan dua WORD (HiInput & LoInput) menjadi satu DWORD untuk packing data panjang.

Alur Logika:

  • Konversi HiInput & LoInput ke ULONG (WORD_TO_UINT → UINT_TO_ULONG).
  • Geser HiInput kiri 16 bit: tempHi = SHL_DWORD(ULONG_TO_DWORD(tempHi), 16).
  • Gabungkan dengan OR: Output = OR_DWORD(tempHi shifted, tempLo).
  • Output DWORD = Hi<<16 | Lo (MSB Hi, LSB Lo).

Nilai Audit/Operator:

  • Packing kompak 32-bit dari dua 16-bit, tanpa konversi langsung WORD-DWORD.
  • Operasi bitwise transparan—mudah trace (e.g., verifikasi alignment Hi/Lo tanpa disassembly).

4. K_SOE32

Tujuan:
Ini adalah inti SOE: Menangani 32 kanal digital, me-latching trip, menyimpan timestamp, dan mendeteksi first-out secara otomatis.

Alur Logika:

  • Restore state dari input persisten (latch & timestamp global, disambungkan dari output siklus sebelumnya untuk kontinuitas data).
    Empat output yang di-loop back menjadi 4 input persisten melalui variabel eksternal:
    • TripIn/TripOut (INT): Kanal first trip (-1 if none) untuk validasi rebuild.
    • LockIn/LockOut (BOOL): Status trip aktif untuk kondisi awal siklus.
    • TripsIn/TripsOut (DWORD): Bitmask latched channels untuk restore dan update state.
    • TsIn/TsOut (struct32ULONG): Array timestamp[0..31] untuk simpan epoch per kanal dan cek status latched.
  • Rebuild first trip jika LockOut=TRUE dan TripOut invalid (e.g., -1 atau tidak match latched bit, dari kanal terendah yang latched via FOR loop).
  • Reset manual (via pulse Reset=TRUE) → kosongkan semua latch (set Latched=FALSE, TripsOut=0, TripOut=-1, TsInt=0, LockOut=FALSE).
  • Loop kanal (FOR i:=0 TO 31): Deteksi rising edge (Run bit i set AND NOT Latched[i], di mana Run adalah DWORD dari R_TRIG external pre-processing sebelum K_16BitsToWord), simpan TsInt[i]=Ts saat ini jika belum latched dan Ts>0 (skip glitch), set TripOut=i jika TripOut=-1. Update TripsOut dari Latched bits; LockOut = LockOut OR (TripsOut <> 0).
  • Map TsInt ke TsOut struct (via CASE Val1-Val32).
  • TripTs = TsInt[TripOut] jika TripOut >=0, else 0.

Nilai Audit/Operator:

  • Setiap trip dicatat dengan timestamp unik (ULONG epoch), cegah loss data post-restart berkat persistent loop-back eksplisit.
  • TripOut dan TripTs langsung tunjuk kanal pertama & waktunya—contoh: "Trip dimulai di kanal 5 pukul 14:23:45", memudahkan diagnosa cepat dan audit transparan tanpa rekonstruksi manual.

5. K_SOE32Sort

Tujuan:
Mengurutkan 32 timestamp hasil latching SOE secara naik, menghasilkan urutan kanal dan waktu kronologis yang konsisten.

Alur Logika:

  • Copy seluruh nilai TsIn.Val1..Val32 ke array kerja TsArr.
  • Bangun daftar indeks Order hanya untuk timestamp >0. Slot kosong diisi -1.
  • Terapkan bubble sort pada array Order:
    • Kriteria utama: nilai timestamp.
    • Tie-breaker: nomor kanal (lebih kecil didahulukan).
  • Hasil urutan indeks dipetakan ke struct Seq.Val1..Val32.
  • Timestamp terurut dipetakan ke struct TsOut.Val1..Val32. Slot kosong diisi nol.

Nilai Audit/Operator:

  • Kronologi kejadian dapat ditelusuri dengan presisi tanpa interpretasi manual.
  • Tie-breaker kanal mencegah ambiguitas saat dua event terjadi bersamaan.
  • Output rapi: operator langsung melihat urutan kanal dan waktu, auditor mudah memverifikasi kronologi.

6. K_SOE32Delta

Tujuan:
Menghitung selisih waktu (delta detik) tiap kanal terhadap base timestamp (event pertama yang valid), untuk menilai kecepatan respon sistem.

Alur Logika:

  • Copy seluruh nilai TsIn.Val1..Val32 ke array kerja TsArr.
  • Cari base timestamp pertama >0. Jika tidak ada, base=0.
  • Loop tiap kanal:
    • Jika base>0 dan TsArr[i] ≥ base, maka Diff[i] = TsArr[i] – base.
    • Jika tidak valid, Diff[i] = 0.
  • Hasil delta dipetakan ke struct Diff.Val1..Val32.

Nilai Audit/Operator:

  • Delta detik memberikan angka konkret jarak waktu antar event.
  • Operator dapat langsung membaca “Channel X trip N detik setelah Channel Y” tanpa kalkulasi manual.
  • Auditor dapat membandingkan delta dengan standar respon proteksi/interlock, meningkatkan transparansi audit.

7. K_EpochToTime

Tujuan:
Konversi epoch detik sejak 1 Januari 2000 (ULONG) menjadi format waktu manusiawi (Y-M-D H:M:S) di Supcon, dengan adjust leap year untuk rekonstruksi tanggal akurat.

Alur Logika:

  • Jika Enable=FALSE atau Epoch > 4294967295 (max ULONG), set semua output (Year..Second)=0 dan RETURN.
  • Init MonthDays manual (31 Jan, 28 Feb, dll.).
  • Pisah Days = Epoch / 86400, RemSec = Epoch MOD 86400.
  • Hitung Hour = RemSec / 3600, Minute = (RemSec MOD 3600) / 60, Second = RemSec MOD 60 (casting LONG_TO_INT aman).
  • Hitung tahun: Year=2000, WHILE Days ≥ 365/366 (leap if MOD 4=0 dan (MOD 100<>0 OR MOD 400=0)), kurangi Days, Year +=1.
  • Hitung bulan: FOR i:=0 TO 11, j=MonthDays[i], adjust Feb j=29 jika leap, kurangi Days jika ≥j, Month=i+1.
  • Day = Days +1.
  • Output Year, Month, Day, Hour, Minute, Second (e.g., 2025-11-08 00:00:00).

Nilai Audit/Operator:

  • Epoch panjang jadi format kalender readable untuk laporan SOE.
  • Leap year reversal akurat—verifikasi timestamp langsung tanpa tool eksternal, tingkatkan transparansi audit.

Test Case K_EpochToTime (Simulasi Logic 100%)

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

Tujuan:
Hitung jumlah channel trip aktif dari <=8 input BOOL. Bisa digunakan untuk menghemat input K_SOE32, misalnya jika unit beroperasi masih >4, maka tidak menjadi trigger (voting logic redundansi unit).

Alur Logika:

  • Copy IN1..IN8 ke array.
  • Loop 0-7: +1 kalau TRUE.
  • Output UINT 0-8.

Nilai Audit/Operator:

  • Summary cepat "Unit yang run masih >4?".
  • Integrasi: Digunakan untuk sub-8ch input K_SOE32 (misal Ch1-8 hanya menjadi satu logic input).

Rangkaian Function Block Dasar SOE

Block Main Function Audit/Operator Value
K_Epoch Bangun epoch detik sejak 2000 Konsistensi waktu
K_EpochToTime Epoch → waktu manusiawi Laporan audit
K_16BitsToWord Packing 16 BOOL → WORD Snapshot kanal
K_2WordToDWord Gabung 2 WORD → DWORD Data panjang
K_SOE32 Inti SOE, latching 32 kanal Kronologi trip
K_SOE32Sort Urutkan timestamp Transparansi urutan
K_SOE32Delta Hitung delta detik Analisis respon

Test Case Table (Simulasi Real Logic 08-Nov-2025)

Semua simulasi menggunakan delay 2 detik antar trip (kecuali concurrent). Logic 100% replika FB asli (epoch relatif, tie-break indeks rendah).

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 (acak, delay 2 detik) 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 (semua Ch1-32 di scan sama) 1 0 detik 0 detik 1, 2, 3, 4, 5, 6, 7, 8, 9, 10... (tie-break indeks)

(Random seed 42 untuk reproducible — urutan trip acak: 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)


Penutup

Dengan rangkaian function block ini, SOE di Supcon DCS dapat dibangun secara modular, audit-grade, dan human-friendly. Dokumentasi header yang konsisten memastikan setiap block dapat diaudit, diajarkan, dan diadaptasi. Seluruh FB ini dapat dipakai ulang lintas plant dengan hanya mengganti mapping I/O, tanpa perlu ubah logika internal.


Catatan Akhir

Awalnya, niatnya sederhana: menulis logika first-out untuk menangkap sinyal trip pertama agar penyebab awal dari rangkaian trip dapat ditemukan.

Namun, ketika timestamp ditambahkan, logika itu berubah menjadi kronologi. Saat pengurutan diterapkan, catatan itu menjadi cerita. Dan dengan delta waktu, cerita itu menjelma menjadi analisis kecepatan terjadinya trip.

Dari logika sederhana lahirlah sistem SOE audit-grade—modular, transparan, dan selaras dengan standar global root cause detection system.


📎 Lampiran Function Block Lengkap

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