Membangun Manajemen Sequence of Event (SOE) di Supcon JX-300XP
Implementasi Sequence of Event (SOE) di Supcon DCS dengan rangkaian 7 function block modular. Menjamin kronologi trip tercatat presisi, mudah diaudit, dan ramah operasional.
Photo by Bradyn Trollip / Unsplash
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.
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, setEpoch=0dan RETURN. - Init
MonthDaysmanual (31 Jan, 28 Feb, dll.). - Ambil waktu:
Year = CENTURY()*100 + YEAR(),Month=MONTH(), dll. - Validasi range (Year ≥2000, Month 1–12, dll.) →
Epoch=0jika 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=0jika 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..IN16ke array internalInputs[0..15](IN1=Inputs[0]). - Init
OUT1 = 0. - Loop
FOR i:=0 TO 15: JikaInputs[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=TRUEdanTripOutinvalid (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 (setLatched=FALSE,TripsOut=0,TripOut=-1,TsInt=0,LockOut=FALSE). - Loop kanal (FOR i:=0 TO 31): Deteksi rising edge (
Runbit i set AND NOTLatched[i], di manaRunadalah DWORD dari R_TRIG external pre-processing sebelum K_16BitsToWord), simpanTsInt[i]=Tssaat ini jika belum latched danTs>0(skip glitch), setTripOut=ijikaTripOut=-1. UpdateTripsOutdariLatchedbits;LockOut = LockOut OR (TripsOut <> 0). - Map
TsIntkeTsOutstruct (via CASE Val1-Val32). TripTs = TsInt[TripOut]jikaTripOut >=0, else 0.
Nilai Audit/Operator:
- Setiap trip dicatat dengan timestamp unik (ULONG epoch), cegah loss data post-restart berkat persistent loop-back eksplisit.
TripOutdanTripTslangsung 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..Val32ke array kerjaTsArr. - Bangun daftar indeks
Orderhanya 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..Val32ke array kerjaTsArr. - Cari base timestamp pertama
>0. Jika tidak ada,base=0. - Loop tiap kanal:
- Jika
base>0danTsArr[i] ≥ base, makaDiff[i] = TsArr[i] – base. - Jika tidak valid,
Diff[i] = 0.
- Jika
- 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=FALSEatauEpoch > 4294967295(max ULONG), set semua output (Year..Second)=0 dan RETURN. - Init
MonthDaysmanual (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, WHILEDays ≥ 365/366(leap if MOD 4=0 dan (MOD 100<>0 OR MOD 400=0)), kurangiDays,Year +=1. - Hitung bulan: FOR i:=0 TO 11,
j=MonthDays[i], adjust Febj=29jika leap, kurangiDaysjika ≥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..IN8ke array. - Loop
0-7:+1kalau 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